/* odrac admin UI stylesheet.
 *
 * Served at /static/admin.css. Linked from every HTML page so the CSP can
 * use a strict style-src 'self' (no 'unsafe-inline'). When you need a tiny
 * one-off rule, prefer adding a utility class here over an inline style
 * attribute — those are blocked by CSP.
 *
 * Layout:
 *   1. Design tokens (CSS custom properties)
 *   2. Reset & base
 *   3. Layout (header, nav, main container)
 *   4. Scope badge + impersonation banner
 *   5. Typography & breadcrumbs
 *   6. Tables (legacy + .data-table polish + .table-wrap + toolbar)
 *   7. Badges
 *   8. Forms (native control restyles — see also section 10 `.card-block`)
 *   9. Buttons (.btn family + legacy button.link)
 *  10. Flashes, cards, sections, empty-state
 *  11. Auth pages
 *  12. Utility classes
 *  13. Responsive (@media)
 */

/* ---------- 1. Design tokens ---------- */

:root {
  /* Dark theme is the default. To honor OS preference + explicit toggle:
   *   - default :root → dark
   *   - `@media (prefers-color-scheme: light)` → light, unless user
   *     forced `data-theme="dark"`
   *   - `:root[data-theme="light"]` → forced light regardless of OS
   *
   * The server emits `data-theme="light"` or `data-theme="dark"` on
   * <html> when the operator picks an explicit preference from /me; no
   * cookie means no attribute, so OS preference wins. No client-side
   * FOUC because the attribute is set at render time, not by JS.
   *
   * color-scheme hints to the browser so native form controls,
   * scrollbars, and the document background follow the theme. */
  color-scheme: dark;

  /* Surfaces */
  --bg: #1a1a1a;
  --surface: #222;
  --surface-2: #2a2a2a;
  --surface-3: #333;
  --border: #2a2a2a;
  --border-strong: #444;

  /* Text */
  --text: #e0e0e0;
  --text-muted: #a8a8a8;     /* >= 4.5:1 on --bg, WCAG AA */
  --text-dim: #888;
  --heading: #cfc;
  --heading-2: #bdc;

  /* Accents */
  --accent: #5d9;
  --accent-hover: #7fc;
  --accent-bg: #28704a;
  --accent-bg-hover: #327d56;

  /* Status colors */
  --danger: #c55;
  --danger-bg: #a44;
  --danger-text: #fca5a5;
  --danger-text-strong: #fcc;  /* hover lift on danger text links */
  --danger-surface: #3a1d1d;
  --danger-border: #8a3333;
  --warn: #fde68a;
  --warn-surface: #3a2f1a;
  --warn-border: #6e5d3d;
  --ok: #bfd;
  --ok-surface: #1d3a25;
  --ok-border: #3d6e4c;
  --info-surface: #2a3a55;
  --info-text: #cde;
  --info-border: #3d5680;

  /* Hover / row tints — derived from --accent + neutral white, so when we
   * eventually flip to a light theme these can be re-defined once and every
   * "translucent hover" surface follows. Until then they unify the .08/.10/.12
   * magic-number scatter that grew across the file. */
  --surface-hover: rgba(93, 221, 153, .08);
  --surface-hover-strong: rgba(93, 221, 153, .12);
  --surface-row-alt: rgba(255, 255, 255, .02);
  --surface-row-hover: rgba(93, 221, 153, .06);
  --border-subtle: rgba(255, 255, 255, .04);

  /* Banner colours — used by the impersonation banner. Lower-saturation than
   * the prior #5a2222 so it insists rather than yells. */
  --banner-danger-bg: #4a1f1f;
  --banner-danger-text: #fcc;
  --banner-danger-text-strong: #fff;
  --banner-danger-border: #8a3333;

  /* Scope-tenant badge palette (kept distinct from --ok-* so the badge family
   * doesn't blur into the same green as success states). */
  --scope-tenant-bg: #2a4a30;
  --scope-tenant-text: #cfe2cd;
  --scope-tenant-border: #3d6e4c;
  --scope-tenant-hover-bg: #3a5e40;

  /* OIDC sign-in button — neutral blue-grey distinct from the green accent so
   * it reads as an alternative login path rather than a primary action. */
  --oidc-bg: #334455;
  --oidc-bg-hover: #445566;

  /* Danger-zone border (the destructive-actions group on detail pages). */
  --danger-zone-border: #5a2828;

  /* On-accent text — kept as a token so we can flip it for a future light
   * theme that uses a darker accent. */
  --on-accent: #fff;

  /* Popup elevation (power menu, column popover) + auth-card lift. One token
   * so any future overlay/modal can share the same depth cue. */
  --shadow-popup: 0 4px 12px rgba(0, 0, 0, .25);

  /* Radii */
  --radius: 6px;
  --radius-sm: 4px;
  --radius-lg: 8px;
  --radius-pill: 999px;

  /* Spacing scale */
  --space-1: .25rem;
  --space-2: .5rem;
  --space-3: .75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;

  /* Type scale — four sizes only, semantic names. .875rem matches the
   * common 14px reference and reads better in tight chrome (table cells,
   * helper text) than the prior .85rem; --text-lg lifts to 1.25rem so
   * page titles read as titles. The prior --font-size-md (1.05rem) was
   * sub-perceptual from --text-base and is folded away. */
  --text-xs: .75rem;
  --text-sm: .875rem;
  --text-base: 1rem;
  --text-lg: 1.25rem;

  /* Containers — the shell (sidebar + main) caps at --shell-max-width
   * and centres horizontally on the viewport so 4K monitors don't leave
   * the content pinned to the left third. The header above the shell
   * uses the same cap on its inner content so the logo/user-block
   * align with the sidebar and main column below. The shell width sits
   * generously above the sum of sidebar (220px) + gap (1.5rem) + main
   * (≈100rem readable) so wide tables (audit, access matrix) still get
   * their pixels.
   *
   * Individual primitives still cap themselves when there's a
   * readability reason (.card-block caps form lines at 80rem); tables,
   * matrices, grids, and the stat strip use the full main column.
   *
   * The `wide: true` flag on layout() is now informational only — kept
   * as a parameter so existing call sites don't break. */
  --shell-max-width: 120rem;
}

/* Light theme — values swapped per token; structure preserved so every
 * component, badge, banner, and surface picks up the new palette without
 * any markup changes. The accent darkens from mint to forest so it
 * contrasts on white at WCAG AA. Two blocks below: an `@media` for
 * auto-pickup when no override is set, and an explicit `[data-theme]`
 * for forced selection. The values match between the two — keep them
 * in sync. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    color-scheme: light;

    /* Surfaces */
    --bg: #f6f7f9;
    --surface: #ffffff;
    --surface-2: #f0f2f5;
    --surface-3: #e3e7ec;
    --border: #d6dade;
    --border-strong: #b8bec5;

    /* Text */
    --text: #1a1d22;
    --text-muted: #5a6068;
    --text-dim: #82888f;
    --heading: #144d2b;
    --heading-2: #1f6038;

    /* Accents */
    --accent: #1a7048;
    --accent-hover: #14572f;
    --accent-bg: #1a7048;
    --accent-bg-hover: #14572f;

    /* Status colors */
    --danger: #b02a2a;
    --danger-bg: #a82323;
    --danger-text: #8b1a1a;
    --danger-text-strong: #6b0e0e;
    --danger-surface: #fbeaea;
    --danger-border: #d09595;
    --warn: #8a5a00;
    --warn-surface: #fff7e0;
    --warn-border: #c8a868;
    --ok: #1f6e30;
    --ok-surface: #e3f3e3;
    --ok-border: #9ec99e;
    --info-surface: #e1edf8;
    --info-text: #0d4d80;
    --info-border: #98c0e2;

    /* Hover / row tints */
    --surface-hover: rgba(26, 112, 72, .08);
    --surface-hover-strong: rgba(26, 112, 72, .14);
    --surface-row-alt: rgba(0, 0, 0, .025);
    --surface-row-hover: rgba(26, 112, 72, .06);
    --border-subtle: rgba(0, 0, 0, .06);

    /* Banner + scope */
    --banner-danger-bg: #fbeaea;
    --banner-danger-text: #8b1a1a;
    --banner-danger-text-strong: #5a0e0e;
    --banner-danger-border: #d09595;
    --scope-tenant-bg: #d2e9d6;
    --scope-tenant-text: #144d2b;
    --scope-tenant-border: #88b890;
    --scope-tenant-hover-bg: #b6dab9;

    /* OIDC button */
    --oidc-bg: #e8eef5;
    --oidc-bg-hover: #d6dfeb;

    --danger-zone-border: #d09595;

    /* On-accent text stays white (accent is a dark green on light). */
    --on-accent: #ffffff;

    /* Popup elevation — softer shadow on a light surface. */
    --shadow-popup: 0 4px 12px rgba(0, 0, 0, .12);
  }
}

/* Forced light — wins regardless of OS preference. Duplicate of the
 * @media block above; keep them in sync when iterating on values. */
:root[data-theme="light"] {
  color-scheme: light;

  --bg: #f6f7f9;
  --surface: #ffffff;
  --surface-2: #f0f2f5;
  --surface-3: #e3e7ec;
  --border: #d6dade;
  --border-strong: #b8bec5;

  --text: #1a1d22;
  --text-muted: #5a6068;
  --text-dim: #82888f;
  --heading: #144d2b;
  --heading-2: #1f6038;

  --accent: #1a7048;
  --accent-hover: #14572f;
  --accent-bg: #1a7048;
  --accent-bg-hover: #14572f;

  --danger: #b02a2a;
  --danger-bg: #a82323;
  --danger-text: #8b1a1a;
  --danger-text-strong: #6b0e0e;
  --danger-surface: #fbeaea;
  --danger-border: #d09595;
  --warn: #8a5a00;
  --warn-surface: #fff7e0;
  --warn-border: #c8a868;
  --ok: #1f6e30;
  --ok-surface: #e3f3e3;
  --ok-border: #9ec99e;
  --info-surface: #e1edf8;
  --info-text: #0d4d80;
  --info-border: #98c0e2;

  --surface-hover: rgba(26, 112, 72, .08);
  --surface-hover-strong: rgba(26, 112, 72, .14);
  --surface-row-alt: rgba(0, 0, 0, .025);
  --surface-row-hover: rgba(26, 112, 72, .06);
  --border-subtle: rgba(0, 0, 0, .06);

  --banner-danger-bg: #fbeaea;
  --banner-danger-text: #8b1a1a;
  --banner-danger-text-strong: #5a0e0e;
  --banner-danger-border: #d09595;
  --scope-tenant-bg: #d2e9d6;
  --scope-tenant-text: #144d2b;
  --scope-tenant-border: #88b890;
  --scope-tenant-hover-bg: #b6dab9;

  --oidc-bg: #e8eef5;
  --oidc-bg-hover: #d6dfeb;

  --danger-zone-border: #d09595;

  --on-accent: #ffffff;

  --shadow-popup: 0 4px 12px rgba(0, 0, 0, .12);
}

/* ---------- 2. Reset & base ---------- */

* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; line-height: 1.4; }
a { color: var(--accent); }

/* Global focus indicator — WCAG 2.4.7. */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---------- 3. Layout ---------- */

/* Header is the slim top strip — logo, hamburger (mobile), scope badge,
 * user block. Primary navigation lives in the sidebar (`.sidebar`) below
 * the header on desktop; on mobile it collapses into a hamburger-toggled
 * overlay.
 *
 * The background spans the full viewport so it reads as a single bar
 * across the top; the inner `.header-inner` flex row caps at the shell
 * width and centres so the logo lines up with the sidebar below on
 * ultrawide / 4K displays. */
header { background: var(--surface); border-bottom: 1px solid var(--surface-3); }
.header-inner {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
  max-width: var(--shell-max-width);
  margin: 0 auto;
  padding: .75rem var(--space-6);
}
header h1 { font-size: 1.1rem; font-weight: 600; }
header h1 a { color: var(--text); text-decoration: none; }
.user { font-size: var(--text-sm); color: var(--text-muted); margin-left: auto; }
.user code { background: var(--surface-3); padding: .1rem .4rem; border-radius: var(--radius-sm); color: var(--heading-2); }

/* Header search box. Visible to admins only (renderHeaderSearch gates
 * by role) and sits between the scope badge and the user block; on
 * mobile it falls below them via the flex-wrap on `header`. The `/`
 * keyboard shortcut (search.js) focuses this input.
 *
 * The box is narrow by default to leave room for the scope badge and
 * user block; on hover/focus it widens so longer queries are readable. */
.header-search { display: flex; align-items: center; }
.header-search input {
  width: 18rem;
  max-width: 100%;
  padding: .3rem .55rem;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--radius-pill);
  font: inherit;
  font-size: var(--text-sm);
}
.header-search input:focus {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}

/* Hamburger toggle — visible only ≤960px (see responsive block at end).
 * Native <button> styling stripped so it reads as a hint icon. */
.nav-toggle {
  display: none;
  background: transparent;
  border: 1px solid transparent;
  color: var(--text);
  padding: .25rem .55rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: 1.2rem;
  line-height: 1;
}
.nav-toggle:hover { background: var(--surface-hover); }
.nav-toggle[aria-expanded="true"] { background: var(--surface-hover-strong); color: var(--accent); }

/* The app shell — the meat of the page. CSS grid splits a 220px sidebar
 * from a flexible main column. The shell spans the full viewport on
 * every page; side padding provides the gutters. Individual primitives
 * cap themselves when readability calls for it (see .card-block max-
 * width below); tables / matrices / dashboards use every pixel. */
.app-shell {
  display: grid;
  grid-template-columns: 220px minmax(0, 1fr);
  gap: var(--space-5);
  max-width: var(--shell-max-width);
  margin: 0 auto;
  padding: var(--space-5) var(--space-6);
  align-items: start;
}

/* Sidebar — sticks to top of viewport while the main column scrolls.
 * `--header-h` is approximate (no need to measure exactly); the offset
 * just keeps the active link below the header strip. */
.sidebar {
  position: sticky;
  top: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.sidebar > nav {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.sidebar-section { display: flex; flex-direction: column; gap: .1rem; }
/* Section title: visually weightier than the links below so the four
 * group dividers (Operate / Configure / Observe / Account) read as
 * headers rather than just-another-link. Bumps from the previous
 * muted-grey treatment: stronger color (--heading-2), heavier weight,
 * wider letter-spacing, and a hairline under the label that physically
 * separates the heading from its links. */
.sidebar-section-title {
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--heading-2);
  padding: 0 .65rem .4rem;
  margin: 0 .35rem .35rem;
  border-bottom: 1px solid var(--border);
}
.sidebar-link {
  display: block;
  padding: .4rem .65rem;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  text-decoration: none;
  font-size: var(--text-sm);
  border-left: 2px solid transparent;
  transition: background .12s, color .12s, border-color .12s;
}
.sidebar-link:hover { background: var(--surface-hover); color: var(--text); }
.sidebar-link.active {
  background: var(--surface-hover-strong);
  color: var(--accent);
  border-left-color: var(--accent);
  font-weight: 500;
}

/* main fills its grid track inside .app-shell. The shell itself caps at
 * --shell-max-width so main's effective width is shell-cap minus
 * sidebar (220px) minus gap — about 100rem of readable column on a
 * wide monitor. No per-element max-width needed here. */
main {
  padding: 0;
  min-width: 0;          /* fix grid-shrinking so wide tables don't bust the col */
}
main.wide { /* hint preserved for back-compat; .app-shell already provides container */ }
main:focus { outline: none; } /* tabindex="-1" target — focus ring is on the skip link, not main */

/* "On this page" rail — layout()'s `toc` option on long detail pages.
 * Sticky anchor list beside the main column; page-toc.js adds .active
 * scroll-spy and opens collapsed <details> targets on jump. Hidden under
 * 1200px where the rail would squeeze the content column. */
.main-with-toc { display: grid; grid-template-columns: minmax(0, 1fr) 11rem; gap: 0 1.5rem; align-items: start; }
.main-with-toc > .main-col { min-width: 0; }
.page-toc { position: sticky; top: 1rem; margin-top: 1rem; font-size: var(--text-sm); border-left: 1px solid var(--border); }
.page-toc-title { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: .08em; color: var(--text-dim); padding: 0 .85rem .35rem; }
.page-toc a { display: block; padding: .25rem .85rem; color: var(--text-muted); text-decoration: none; border-left: 2px solid transparent; margin-left: -1px; }
.page-toc a:hover { color: var(--accent); }
.page-toc a.active { color: var(--accent); border-left-color: var(--accent); }
/* Anchor jumps land with a little air above the section heading. */
.section[id], details[id], .danger-zone[id] { scroll-margin-top: 1rem; }
@media (max-width: 1199px) {
  .main-with-toc { display: block; }
  .page-toc { display: none; }
}

/* Tiny site-wide footer so the body has a closing edge. */
.app-footer { text-align: center; color: var(--text-muted); font-size: var(--text-xs); margin-top: 1rem; padding: 1rem; }

/* Keyboard / screen-reader skip link. Hidden via .sr-only until focused,
 * then pops into the top-left so the user can jump straight to <main>. */
.skip-link:focus {
  position: fixed;
  top: .5rem;
  left: .5rem;
  width: auto;
  height: auto;
  padding: .5rem .75rem;
  margin: 0;
  overflow: visible;
  clip: auto;
  white-space: normal;
  background: var(--accent-bg);
  color: var(--on-accent);
  border-radius: var(--radius-sm);
  text-decoration: none;
  z-index: 1000;
}

/* ---------- 4. Scope badge + impersonation banner ---------- */

.scope { font-size: var(--text-xs); padding: .15rem .5rem; border-radius: var(--radius-pill); font-family: ui-monospace, monospace; letter-spacing: .03em; text-transform: uppercase; }
.scope-system { background: var(--info-surface); color: var(--info-text); border: 1px solid var(--info-border); }
.scope-tenant { background: var(--scope-tenant-bg); color: var(--scope-tenant-text); border: 1px solid var(--scope-tenant-border); }
/* Scope switcher — for system_admin the scope badge is a <details> dropdown
 * listing /system plus every active tenant (scope-switcher.js fills it
 * lazily on first open). Mirrors the .pw-menu pattern, but the header has
 * no overflow clipping so a plain absolute popup anchors fine. */
.scope-switcher { position: relative; display: inline-block; }
.scope-switcher > summary { cursor: pointer; list-style: none; user-select: none; display: inline-flex; align-items: center; gap: .3rem; }
.scope-switcher > summary::-webkit-details-marker { display: none; }
.scope-switcher > summary.scope-tenant:hover,
.scope-switcher[open] > summary.scope-tenant { background: var(--scope-tenant-hover-bg); }
.scope-switcher > summary.scope-system:hover,
.scope-switcher[open] > summary.scope-system { filter: brightness(1.1); }
.scope-caret { font-size: .7em; opacity: .8; }
.scope-menu {
  position: absolute;
  top: calc(100% + .4rem);
  left: 0;
  z-index: 30;
  display: flex;
  flex-direction: column;
  min-width: 13rem;
  max-height: min(60vh, 24rem);
  overflow-y: auto;
  padding: .25rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-popup);
}
.scope-menu-item {
  display: flex;
  align-items: baseline;
  gap: .5rem;
  color: var(--text);
  text-decoration: none;
  padding: .35rem .55rem;
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  white-space: nowrap;
}
.scope-menu-item:hover { background: var(--surface-hover); color: var(--accent-hover); }
.scope-menu-item.is-current { background: var(--surface-hover-strong); }
/* `display: flex` above outranks the UA's [hidden] rule — restate it so the
 * filter can actually hide non-matching entries. */
.scope-menu-item[hidden] { display: none; }
/* Type-to-filter box, revealed by scope-switcher.js only when the tenant
 * list is long enough to need it. */
.scope-menu-filter {
  margin: .15rem .3rem .35rem;
  padding: .3rem .55rem;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: var(--text-sm);
}
.scope-menu-filter:focus {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}
.scope-menu-item code { font-size: var(--text-xs); color: var(--text-muted); }
.scope-menu-pinned { border-bottom: 1px solid var(--border); border-radius: var(--radius-sm) var(--radius-sm) 0 0; margin-bottom: .25rem; }
.scope-menu-status { padding: .35rem .55rem; font-size: var(--text-sm); color: var(--text-muted); white-space: nowrap; }

.impersonation-banner {
  background: var(--banner-danger-bg);
  color: var(--banner-danger-text);
  padding: .5rem 1.5rem;
  border-bottom: 1px solid var(--banner-danger-border);
  font-size: .9rem;
  display: flex;
  align-items: center;
  gap: 1rem;
}
.impersonation-banner strong { color: var(--banner-danger-text-strong); }
.impersonation-banner button.link { color: var(--banner-danger-text); }
.impersonation-banner button.link:hover { color: var(--banner-danger-text-strong); }
.impersonation-banner input { background: var(--danger-surface); color: var(--banner-danger-text-strong); border: 1px solid var(--banner-danger-border); padding: .15rem .4rem; border-radius: var(--radius-sm); font: inherit; width: 6em; letter-spacing: .15em; text-align: center; }
.impersonation-banner .remaining { font-family: ui-monospace, monospace; font-size: var(--text-sm); color: var(--banner-danger-text); }
.impersonation-banner .remaining.low { color: var(--warn); }
.impersonation-banner .remaining.expired { color: var(--banner-danger-text-strong); font-weight: 600; }

/* ---------- 5. Typography & breadcrumbs ---------- */

h2 { font-size: 1.1rem; margin: 1rem 0 .5rem; color: var(--heading); }
h3 { font-size: .95rem; margin: 1rem 0 .5rem; color: var(--heading-2); }
.subhead { color: var(--text-dim); font-size: var(--text-sm); margin: .75rem 0 .25rem; text-transform: uppercase; letter-spacing: .05em; }
.crumbs { font-size: var(--text-sm); color: var(--text-dim); margin-bottom: .5rem; }
.crumbs a { color: var(--accent); }

code { font-family: ui-monospace, monospace; background: var(--surface-2); padding: .1rem .3rem; border-radius: var(--radius-sm); color: var(--heading-2); }
/* Probe error inside the iDRAC summary "Reachability" row — read as
 * danger-coloured prose, not as a neutral identifier. */
code.probe-error { color: var(--danger-text); background: var(--danger-surface); }

/* Page header pattern: title on left, action buttons on right. */
.page-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1rem;
  flex-wrap: wrap;
}
.page-header .page-title { font-size: var(--text-lg); color: var(--heading); margin: 0; }
.page-header .page-actions { display: flex; gap: .5rem; align-items: center; }
/* Subtitle-bearing variant — page-heading wraps the h2 + a one-line muted
 * subtitle. Stacks vertically; the parent .page-header keeps its flex
 * layout so [heading][actions] still split across the row. */
.page-header .page-heading { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
.page-header .page-subtitle { font-size: var(--text-sm); color: var(--text-muted); margin: 0; }

/* ---------- 6. Tables ---------- */

table { border-collapse: collapse; width: 100%; margin: .5rem 0; background: var(--surface); border-radius: var(--radius); overflow: hidden; }
th, td { padding: .5rem .75rem; text-align: left; border-bottom: 1px solid var(--border); vertical-align: top; }
th { background: var(--surface-2); font-weight: 600; font-size: var(--text-sm); color: var(--heading-2); }
tr:last-child td { border-bottom: 0; }

/* Wrapper to make wide tables scroll inside the layout rather than burst it. */
.table-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border-radius: var(--radius);
  background: var(--surface);
  margin: .5rem 0;
}
.table-wrap > table { margin: 0; border-radius: 0; }


/* Polished modifier — additive on <table>. Adds zebra + hover + subtle row spacing.
 * The sticky-th box-shadow gives the header a hairline detach when the body
 * scrolls behind it — without it the green column titles bleed into the first
 * row at the moment the sticky boundary engages. */
.data-table tbody tr:nth-child(even) td { background: var(--surface-row-alt); }
.data-table tbody tr:hover td { background: var(--surface-row-hover); }
.data-table th { position: sticky; top: 0; z-index: 1; box-shadow: 0 1px 0 var(--border-subtle); }
.data-table td { font-size: .9rem; }

/* Column helpers. */
.col-shrink { white-space: nowrap; width: 1%; }
.col-truncate { max-width: 22ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.col-truncate-lg { max-width: 40ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Flexible truncating column: the th soaks up the table's spare width (a
 * hard ch cap would leave it as a dead gap on wide monitors) and the tds
 * clip with an ellipsis only when content outgrows the allocation. One
 * per table — two 100% columns fight over the slack. */
th.col-stretch { width: 100%; }
td.col-stretch { max-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.col-num { text-align: right; font-variant-numeric: tabular-nums; }
.col-actions { text-align: right; white-space: nowrap; width: 1%; }
.col-mono { font-family: ui-monospace, monospace; font-size: var(--text-sm); }
.col-power { white-space: nowrap; width: 1%; }
/* Bulk row-selection checkbox column: keep it tight so it doesn't steal width
 * from the data columns. */
.col-select { width: 1%; white-space: nowrap; text-align: center; }
.col-select input { margin: 0; vertical-align: middle; cursor: pointer; }

/* Bulk action bar under a selectable table (tenant iDRAC list). */
.bulk-actions {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex-wrap: wrap;
  margin: .5rem 0 0;
}
.bulk-actions .bulk-count { margin-right: auto; font-size: var(--text-sm); }

/* Client-side pagination controls (table.js data-table-paginate). Sits
 * directly under the table; hidden when the filtered set fits one page. */
.table-pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .75rem;
  margin: .5rem 0 0;
}
.table-pager[hidden] { display: none; }
.table-pager-status { font-size: var(--text-sm); color: var(--muted, inherit); font-variant-numeric: tabular-nums; }

/* Power state badge + inline action buttons used in the consoles list.
 * The dot mirrors the console viewer's `.pw-dot` so on/off are visually
 * consistent across both pages. consoles-page.js fills `.pw-label` after
 * polling /api/consoles/:id/power. */
.pw-badge {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  font-size: var(--text-xs);
  color: var(--text-muted);
  margin-right: .5rem;
}
.pw-badge .pw-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--text-muted);
  flex: 0 0 8px;
}
.pw-badge .pw-dot.on { background: var(--ok); box-shadow: 0 0 4px var(--ok); }
.pw-badge .pw-dot.off { background: var(--danger); }

/* Collapsed power-actions menu in the consoles list. Native <details> drives
 * open/close; consoles-page.js closes others when one opens and closes the
 * popup after an action runs. position:relative on the <details> anchors the
 * absolute popup so the row doesn't grow when expanded. */
.pw-menu { position: relative; display: inline-block; }
.pw-menu-summary {
  display: inline-flex;
  align-items: center;
  gap: .2rem;
  cursor: pointer;
  list-style: none;
  user-select: none;
  padding: .15rem .45rem;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--accent);
  font-size: var(--text-xs);
  line-height: 1;
}
.pw-menu-summary::-webkit-details-marker { display: none; }
.pw-menu-summary:hover { background: var(--surface-hover); color: var(--accent-hover); }
.pw-menu[open] > .pw-menu-summary { background: var(--surface-hover-strong); color: var(--accent-hover); border-color: var(--border); }
.pw-menu-caret { font-size: .7em; opacity: .8; }
/* Fixed positioning lets the popup escape `.table-wrap`'s overflow-x:auto
 * (which the CSS spec forces overflow-y to `auto` for too, clipping any
 * absolute child). consoles-page.js sets top/left from the summary's
 * bounding rect on each toggle. */
.pw-menu-popup {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  min-width: 9rem;
  padding: .25rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-popup);
}
.pw-menu-item {
  background: transparent;
  border: 0;
  color: var(--text);
  text-align: left;
  padding: .35rem .55rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font: inherit;
  font-size: var(--text-sm);
  white-space: nowrap;
}
.pw-menu-item:hover { background: var(--surface-hover); color: var(--accent-hover); }
.pw-menu-item:disabled { opacity: .5; cursor: not-allowed; }
.pw-menu-item-danger { color: var(--danger-text); }
.pw-menu-item-danger:hover { background: var(--danger-surface); color: var(--danger-text); }

/* PDU mains control — deliberately distinct from the BMC power menu above so
 * operators never confuse "reboot via BMC" with "cut mains via PDU". The
 * summary carries a plug glyph and a danger-tinted border; the menu items use
 * stronger warning/danger styling than the BMC equivalents. Selectors are
 * `pdu-*` (never `pw-*`) so pdu-page.js and consoles-page.js never cross-fire. */
.pdu-badge {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  font-size: var(--text-xs);
  color: var(--text-muted);
}
.pdu-badge .pdu-dot {
  display: inline-block;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--text-muted);
  flex: 0 0 8px;
}
.pdu-badge .pdu-dot.on { background: var(--ok); box-shadow: 0 0 4px var(--ok); }
.pdu-badge .pdu-dot.off { background: var(--danger); }

.pdu-menu { position: relative; display: inline-block; }
.pdu-menu-summary {
  display: inline-flex;
  align-items: center;
  gap: .2rem;
  cursor: pointer;
  list-style: none;
  user-select: none;
  padding: .15rem .45rem;
  border: 1px solid var(--danger-surface);
  border-radius: var(--radius-sm);
  color: var(--danger-text);
  font-size: var(--text-xs);
  line-height: 1;
}
.pdu-menu-summary::-webkit-details-marker { display: none; }
.pdu-menu-summary:hover { background: var(--danger-surface); }
.pdu-menu[open] > .pdu-menu-summary { background: var(--danger-surface); border-color: var(--danger); }
.pdu-menu-caret { font-size: .7em; opacity: .8; }
.pdu-menu-popup {
  position: fixed;
  top: 0; left: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  min-width: 11rem;
  padding: .25rem;
  background: var(--surface);
  border: 1px solid var(--danger);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-popup);
}
.pdu-menu-item {
  background: transparent;
  border: 0;
  color: var(--text);
  text-align: left;
  padding: .35rem .55rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font: inherit;
  font-size: var(--text-sm);
  white-space: nowrap;
}
.pdu-menu-item:hover { background: var(--surface-hover); }
.pdu-menu-item:disabled { opacity: .5; cursor: not-allowed; }
.pdu-menu-item-warn { color: var(--warn-text, var(--danger-text)); }
.pdu-menu-item-danger { color: var(--danger-text); font-weight: 600; }
.pdu-menu-item-danger:hover { background: var(--danger-surface); }
.data-table .pdu-menu-popup { z-index: 40; }

/* Soft-deleted / orphaned outlet rows render muted. */
.row-muted { opacity: .55; }

/* Icon-only action link used in table action columns (consoles list "open",
 * iDRAC list "manage grants & creds", etc.). Mirrors .pw-menu-summary so
 * icon links and power controls read as a matched set of icon buttons. The
 * font-size is bumped a notch so the glyph reads as a distinct icon rather
 * than tiny text. */
.icon-link {
  display: inline-flex;
  align-items: center;
  padding: .15rem .45rem;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--accent);
  font-size: var(--text-base);
  line-height: 1;
  text-decoration: none;
}
.icon-link:hover { background: var(--surface-hover); color: var(--accent-hover); }

/* Toolbar above a table. Layout is: [row count] … [filter toggle] [actions].
 * The filter is hidden behind a small icon toggle by default to keep list
 * views uncluttered — clicking it expands an input that pushes the count
 * leftward. The toolbar stays visible (it carries the row count) but is
 * deliberately quiet: no border, no surface, just the text + toggle. */
.table-toolbar {
  display: flex;
  align-items: center;
  gap: .5rem;
  margin: .35rem 0;
  flex-wrap: wrap;
  min-height: 1.6rem;
  font-size: var(--text-sm);
}
.table-toolbar .table-count { color: var(--text-muted); font-variant-numeric: tabular-nums; }
.table-toolbar .table-filter-wrap { margin-left: auto; display: flex; align-items: center; gap: .35rem; }
.table-toolbar .table-filter-toggle {
  background: transparent;
  border: 1px solid transparent;
  color: var(--text-muted);
  padding: .15rem .45rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font: inherit;
  font-size: var(--text-sm);
  line-height: 1.2;
}
.table-toolbar .table-filter-toggle:hover { color: var(--text); border-color: var(--border-strong); }
.table-toolbar .table-filter-toggle[aria-expanded="true"] { color: var(--accent); border-color: var(--border-strong); background: var(--surface); }
.table-toolbar .table-filter[hidden] { display: none; }
.table-toolbar .table-filter {
  width: 14rem;
  padding: .25rem .5rem;
  border: 1px solid var(--border-strong);
  background: var(--bg);
  color: var(--text);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: var(--text-sm);
}
/* Collapsible filter region: dimension selects (moved in by table.js) + the
 * text search box, revealed together by the toggle. The explicit [hidden]
 * display:none is needed because the flex display would otherwise override
 * the UA hidden behaviour. The selects mirror .table-filter's box so they
 * line up; the global select{} rule supplies the dropdown arrow + right pad. */
.table-toolbar .table-filter-fields { display: flex; align-items: center; gap: .35rem; flex-wrap: wrap; }
.table-toolbar .table-filter-fields[hidden] { display: none; }
.table-toolbar .table-filter-fields select {
  padding: .25rem .5rem;
  border: 1px solid var(--border-strong);
  background: var(--bg);
  color: var(--text);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: var(--text-sm);
}
.table-toolbar .actions { display: flex; gap: .5rem; }

/* Sortable column headers — JS toggles aria-sort; CSS draws the chevron.
 * The chevron is hidden until the column is hovered (affordance hint) or
 * actively sorted — a permanent triangle on every column was just noise. */
th.sortable, th[aria-sort] { cursor: pointer; user-select: none; position: relative; padding-right: 1.4rem; }
th.sortable:hover, th[aria-sort]:hover { color: var(--accent); }
th.sortable::after,
th[aria-sort]::after {
  content: '';
  position: absolute;
  right: .55rem;
  top: 50%;
  width: 0;
  height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-bottom: 5px solid currentColor;
  opacity: 0;
  transform: translateY(-50%);
}
th.sortable:hover::after, th[aria-sort]:hover::after { opacity: .45; }
th[aria-sort="ascending"]::after { opacity: 1; }
th[aria-sort="descending"]::after {
  border-bottom: 0;
  border-top: 5px solid currentColor;
  opacity: 1;
}

.idrac-row td:first-child { font-weight: 600; }

/* ---------- 6b. Column controls (opt-in via data-table-columns) ----------
 * The ⚙ Columns pill sits beside the ⌕ Filter pill and opens a popover for
 * show/hide + drag-to-reorder. Headers also grow a drag grip (reorder) and a
 * right-edge handle (resize). All desktop-only — table.js skips wiring them
 * under the 720px card breakpoint, and the block below also hides them. */
.table-toolbar .table-columns-toggle {
  background: transparent;
  border: 1px solid transparent;
  color: var(--text-muted);
  padding: .15rem .45rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font: inherit;
  font-size: var(--text-sm);
  line-height: 1.2;
}
.table-toolbar .table-columns-toggle:hover { color: var(--text); border-color: var(--border-strong); }
.table-toolbar .table-columns-toggle[aria-expanded="true"] { color: var(--accent); border-color: var(--border-strong); background: var(--surface); }

/* Popover is position:fixed (set by table.js from the pill's rect) so it
 * escapes .table-wrap's overflow-x:auto, same trick as .pw-menu-popup. */
.table-columns-popover {
  position: fixed;
  z-index: 30;
  min-width: 12rem;
  max-height: 70vh;
  overflow-y: auto;
  padding: .35rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-popup);
  font-size: var(--text-sm);
}
.table-columns-list { display: flex; flex-direction: column; gap: .1rem; }
.table-columns-item {
  display: flex;
  align-items: center;
  gap: .4rem;
  padding: .25rem .35rem;
  border-radius: var(--radius-sm);
  cursor: grab;
}
.table-columns-item:hover { background: var(--surface-hover); }
.table-columns-item.col-drop-target { box-shadow: inset 0 2px 0 var(--accent); }
.table-columns-item.col-dragging { opacity: .5; }
.table-columns-label { display: flex; align-items: center; gap: .4rem; cursor: pointer; flex: 1; }
.table-columns-label input { cursor: pointer; }

/* Header drag grip + resize handle live inside the <th>. Reserve a little
 * left pad for the grip; the resize handle straddles the right border.
 * The grip stays invisible (but in layout, so columns don't shift) until
 * the header is hovered or the grip is keyboard-focused. */
th .col-grip {
  cursor: grab;
  color: var(--text-muted);
  opacity: 0;
  margin-right: .35rem;
  user-select: none;
  font-size: .85em;
}
th:hover .col-grip { opacity: .55; }
th .col-grip:hover { opacity: 1; color: var(--accent); }
th .col-grip:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; opacity: 1; }
th.col-drop-target { box-shadow: inset 2px 0 0 var(--accent); }
th.col-dragging { opacity: .5; }
th .col-resize {
  position: absolute;
  top: 0;
  right: 0;
  width: 7px;
  height: 100%;
  cursor: col-resize;
  user-select: none;
}
th .col-resize:hover { background: var(--accent); opacity: .4; }
/* Honour explicit <col> widths only once the user has resized something. */
.data-table[data-table-resized] { table-layout: fixed; }
.data-table[data-table-resized] td { overflow: hidden; text-overflow: ellipsis; }
body.col-resizing { cursor: col-resize; user-select: none; }

/* ---------- 7. Badges (state pills) ---------- */

.badge {
  display: inline-block;
  padding: .1rem .55rem;
  border-radius: var(--radius-pill);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: .03em;
  text-transform: uppercase;
  background: var(--surface-2);
  color: var(--text-muted);
  border: 1px solid var(--border-strong);
  line-height: 1.4;
  white-space: nowrap;
}
.badge.ok { background: var(--ok-surface); color: var(--ok); border-color: var(--ok-border); }
.badge.warn { background: var(--warn-surface); color: var(--warn); border-color: var(--warn-border); }
.badge.err { background: var(--danger-surface); color: var(--danger-text); border-color: var(--danger-border); }
.badge.info { background: var(--info-surface); color: var(--info-text); border-color: var(--info-border); }
.badge.muted { background: var(--surface-2); color: var(--text-muted); border-color: var(--border-strong); }

/* ---------- 8. Forms ---------- */

/* Canonical form layout is the vertical `.card-block` stack (section 10).
 * Inputs/selects/textareas inside `.card-block` pick up their styling from
 * the `.card-block > label …` rules below; this block adds the same
 * styling to any `.row-grid` (two short fields side-by-side) so a form
 * that mixes stacked + 2-col rows reads uniformly. */

/* Two-column row inside a .card-block. The canonical form layout is the
 * vertical .card-block stack; reach for .row-grid only when two short
 * fields read better side-by-side (Name + Slug, Host + Port, etc.). The
 * responsive block at the bottom collapses this to a single column ≤720px. */
.row-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: .5rem;
}
.row-grid > .full { grid-column: 1 / -1; }
.row-grid > label { display: flex; flex-direction: column; gap: .25rem; }

/* Helper text under inputs. */
.form-help { font-size: var(--text-xs); color: var(--text-muted); margin-top: .15rem; }

/* Toggle-group + toggle-row: a reusable "system-admin-only settings"
 * cluster that drops into any .card-block form. Each option is a
 * `<label class="toggle-row">` containing
 * an `<input type="checkbox">` and a `<span class="toggle-body">` with
 * `.toggle-title` + `.form-help`. The group is wrapped in `<fieldset
 * class="toggle-group">` so multiple toggles share a header without each
 * needing its own panel. */
fieldset.toggle-group {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: .5rem .75rem .65rem;
  margin: .25rem 0;
  display: flex;
  flex-direction: column;
  gap: .4rem;
  background: var(--surface-2);
}
fieldset.toggle-group > legend {
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--text-muted);
  padding: 0 .35rem;
}
/* The `.card-block label` rule further down (`display: flex;
 * flex-direction: column`) would otherwise win on specificity ties and
 * stack the input above the body — we want the checkbox/radio inline on
 * the LEFT with the body wrapping to its right. Repeating `label.toggle-row`
 * twice bumps the selector to (0,2,1) so it beats `.card-block label`
 * (0,1,1) inside card-block forms too. */
label.toggle-row,
label.toggle-row.toggle-row {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: .15rem .65rem;
  align-items: start;
  padding: .35rem 0;
  font-size: var(--text-sm);
  color: var(--text);
  cursor: pointer;
}
label.toggle-row > input[type="checkbox"],
label.toggle-row > input[type="radio"] {
  margin-top: .15rem;
  grid-row: 1 / span 2;
  /* Pre-size so grid measures the column even before the input renders. */
  width: 16px;
  flex: 0 0 auto;
}
label.toggle-row .toggle-body {
  display: flex;
  flex-direction: column;
  gap: .15rem;
  min-width: 0;
}
label.toggle-row .toggle-title { font-weight: 500; }
label.toggle-row .form-help { margin-top: 0; }

/* Required-field marker. Rendered inline next to the label text via
 * `<span class="req" aria-hidden="true">*</span>` — aria-hidden because the
 * underlying input carries `required`, which is what screen readers announce. */
.req { color: var(--danger); margin-left: .15rem; font-weight: 600; }

/* Footer row that holds e.g. cancel (left) + submit (right). */
.form-actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: .5rem;
  margin-top: .75rem;
  grid-column: 1 / -1;
  flex-wrap: wrap;
}
.form-actions .left { display: flex; gap: .5rem; }
.form-actions .right { display: flex; gap: .5rem; margin-left: auto; }

/* Native control restyling so they fit the dark theme. */
input[type="checkbox"], input[type="radio"] {
  appearance: none;
  -webkit-appearance: none;
  width: 1rem;
  height: 1rem;
  border: 1px solid var(--border-strong);
  background: var(--bg);
  display: inline-grid;
  place-content: center;
  cursor: pointer;
  vertical-align: middle;
  flex-shrink: 0;
}
input[type="checkbox"] { border-radius: 3px; }
input[type="radio"] { border-radius: 50%; }
input[type="checkbox"]:checked, input[type="radio"]:checked { border-color: var(--accent); background: var(--accent-bg); }
input[type="checkbox"]:checked::after {
  content: '';
  width: .55rem;
  height: .3rem;
  border-left: 2px solid var(--on-accent);
  border-bottom: 2px solid var(--on-accent);
  transform: rotate(-45deg) translate(1px, -1px);
}
input[type="radio"]:checked::after {
  content: '';
  width: .45rem;
  height: .45rem;
  border-radius: 50%;
  background: var(--on-accent);
}
input[type="checkbox"]:disabled, input[type="radio"]:disabled { opacity: .5; cursor: not-allowed; }

/* Selects: SVG-free caret using CSS gradients. */
select {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  padding-right: 2rem !important;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--accent) 50%),
    linear-gradient(135deg, var(--accent) 50%, transparent 50%);
  background-position:
    calc(100% - 1rem) 55%,
    calc(100% - .65rem) 55%;
  background-size: .35rem .35rem, .35rem .35rem;
  background-repeat: no-repeat;
}
select:focus { background-image:
  linear-gradient(45deg, transparent 50%, var(--accent-hover) 50%),
  linear-gradient(135deg, var(--accent-hover) 50%, transparent 50%);
}

/* Number inputs: tighter, tabular. */
input[type="number"] { font-variant-numeric: tabular-nums; }

/* ---------- 9. Buttons ---------- */

/* .btn family — buttons for table actions, page headers, and inside the
 * formCard() helper's actions row.
 * a.btn and button.btn are both valid. */
.btn {
  display: inline-block;
  padding: .45rem .9rem;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 500;
  text-decoration: none;
  text-align: center;
  line-height: 1.3;
  background: transparent;
  color: var(--text);
  white-space: nowrap;
  transition: background .12s, border-color .12s, color .12s;
}
.btn:hover { text-decoration: none; }
.btn:disabled, .btn[aria-disabled="true"] { opacity: .5; cursor: not-allowed; }

.btn-primary { background: var(--accent-bg); color: var(--on-accent); border-color: var(--accent-bg); }
.btn-primary:hover { background: var(--accent-bg-hover); border-color: var(--accent-bg-hover); color: var(--on-accent); }

.btn-secondary { background: transparent; color: var(--accent); border-color: var(--accent); }
.btn-secondary:hover { background: var(--surface-hover); color: var(--accent-hover); border-color: var(--accent-hover); }

.btn-ghost { background: transparent; color: var(--accent); border-color: transparent; padding-left: .5rem; padding-right: .5rem; }
.btn-ghost:hover { background: var(--surface-hover); color: var(--accent-hover); }

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

.btn-sm { padding: .25rem .55rem; font-size: var(--text-xs); }

/* Legacy text-button — preserved for existing handlers. */
button.link { background: none; border: 0; color: var(--accent); cursor: pointer; font: inherit; padding: 0; margin-left: .5rem; }
button.link:hover { text-decoration: underline; }
button.link.danger { color: var(--danger-text); }
button.link.danger:hover { color: var(--danger-text-strong); }

.btn-row form { display: inline; }
.btn-row button { margin-right: .25rem; }

/* ---------- 10. Flashes, cards, sections, empty-state ---------- */

.flash { padding: .6rem 1rem; border-radius: var(--radius-sm); margin: .5rem 0; font-size: .9rem; }
.flash.ok { background: var(--ok-surface); color: var(--ok); }
.flash.warn { background: var(--warn-surface); color: var(--warn); }
.flash.err { background: var(--danger-surface); color: var(--danger-text); }

/* Leading attention glyph for can't-undo-this flash lines (one-time
 * secret reveals). Amber against the green ok-surface on purpose —
 * the flash says "success", the icon says "but act NOW". */
.flash .warn-icon { color: var(--warn); font-size: 1.15em; font-weight: 700; }

/* One-time secret reveal (site-agent enrolment token). Deliberately loud:
 * this render is the only one that will ever show the plaintext, so the
 * token gets a full-width block at body size instead of inline <code>.
 * user-select: all -> one click selects the whole token; the adjacent
 * button (copy.js, data-copy) does the clipboard write. */
.token-reveal { display: flex; align-items: stretch; gap: .5rem; margin: .5rem 0; }
.token-reveal code { flex: 1; display: flex; align-items: center; font-size: 1rem; padding: .55rem .75rem; background: var(--surface-2); color: var(--heading-2); border: 1px solid var(--border); border-radius: var(--radius-sm); user-select: all; overflow-x: auto; white-space: nowrap; }
.token-reveal .btn { flex-shrink: 0; }

/* Step-by-step content inside a flash (agent install quickstart on the
 * site-detail enrolment reveal). The global reset zeroes list padding,
 * which clips the ol markers — restore room for them; give command/config
 * snippets a neutral block so they read (and select) as copy-paste units. */
.flash ol { padding-left: 1.4rem; margin: .35rem 0; display: flex; flex-direction: column; gap: .45rem; }
.flash pre { background: var(--surface-2); color: var(--heading-2); padding: .5rem .75rem; border-radius: var(--radius-sm); margin: .3rem 0; overflow-x: auto; font-size: var(--text-sm); }
.flash details > summary { cursor: pointer; font-weight: 600; user-select: none; }
.flash details { margin-top: .35rem; }

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

/* Generic content card (non-form). Stacks children with a consistent gap so
 * an h3 → paragraph → label/input → actions row never collides. Children
 * that need a different rhythm should set their own margins; the gap is
 * the floor.
 *
 * The card surface spans the main column so its edges line up with the
 * tables/grids it sits between — a capped card beside a full-width table
 * reads as an alignment bug once the column outgrows the cap (≥2K
 * monitors). Input "line length" stays readable via the max-width on
 * text-like inputs below, not by narrowing the surface. */
.card-block {
  background: var(--surface);
  border-radius: var(--radius);
  padding: 1rem;
  margin: 1rem 0;
  display: flex;
  flex-direction: column;
  gap: .75rem;
}
/* Any label inside a card-block stacks input under text and reads muted —
 * applies to direct children of card-block AND nested labels inside
 * `.row-grid`, `<fieldset>`, `<details>`, etc. */
.card-block label { font-size: var(--text-sm); color: var(--text-muted); display: flex; flex-direction: column; gap: .25rem; }
/* Same for text-like inputs at any depth inside a card-block. Checkboxes,
 * radios, and hidden inputs are excluded — they have their own appearance
 * rules in section 8 (or no visible chrome at all). */
.card-block input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]),
.card-block select,
.card-block textarea {
  padding: .45rem;
  border: 1px solid var(--border-strong);
  background: var(--bg);
  color: var(--text);
  border-radius: var(--radius-sm);
  font: inherit;
}
/* Readability cap — a single-line field wider than ~40rem stops reading as
 * a field (the old 80rem card cap served this job; now the surface spans
 * the column and the cap lives on the controls). Textareas are exempt:
 * cert paste and bulk import genuinely want the width. */
.card-block input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]),
.card-block select {
  max-width: 40rem;
}
/* Actions row at the bottom of a form (formCard helper renders this). */
.card-block .actions { text-align: right; margin-top: .25rem; display: flex; gap: .5rem; justify-content: flex-end; flex-wrap: wrap; }

/* Section wrapper for visual rhythm between page parts. */
.section { margin: 2rem 0 1rem; }
.section > h2:first-child, .section > h3:first-child { margin-top: 0; }

/* Hardware-inventory tile spacing on the iDRAC detail page. Each
 * controller renders its physical + virtual disks as nested tables;
 * these classes give them air without using inline `style=` (which CSP
 * blocks under `style-src 'self'`). */
.hw-controller-block { margin-top: .75rem; }
.hw-group-subhead { margin: .75rem 0 .25rem; }
.hw-staleness { font-size: .75em; font-weight: normal; }

/* Access matrix (`/admin/tenants/:id/access`). A pivoted table where
 * rows are subjects (users + user_groups) and columns are targets
 * (iDRACs + idrac_groups). Cells hold one-letter cap chips. Wide
 * matrices scroll horizontally; the first column (subject label) is
 * sticky so the operator never loses context while scrolling right. */
.access-matrix-wrap { overflow-x: auto; }
.access-matrix { min-width: max-content; }
.access-matrix th, .access-matrix td {
  vertical-align: middle;
  text-align: center;
}
.access-matrix thead th {
  /* Tilt the target column headers so labels fit without ballooning
   * column width. Sticky so they ride along under any future toolbar. */
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--surface-2);
  white-space: nowrap;
  font-size: var(--text-xs);
}
.access-matrix thead th a { color: var(--text); text-decoration: none; }
.access-matrix thead th a:hover { color: var(--accent); }
.access-matrix th.matrix-corner {
  text-align: left;
  z-index: 3;
  position: sticky;
  left: 0;
  background: var(--surface-2);
  color: var(--text-muted);
}
.access-matrix th.matrix-subject {
  text-align: left;
  position: sticky;
  left: 0;
  background: var(--surface);
  font-weight: 500;
  z-index: 1;
  padding-right: 1rem;
  display: flex;
  align-items: baseline;
  gap: .5rem;
  flex-wrap: wrap;
  /* Keep the subject column from collapsing too narrow when the table
   * fights for space. */
  min-width: 14rem;
}
.access-matrix th.matrix-subject a { color: var(--heading); text-decoration: none; }
.access-matrix th.matrix-subject a:hover { color: var(--accent); }
.access-matrix .matrix-subject-label { font-weight: 600; }
.access-matrix td {
  font-size: var(--text-xs);
  padding: .25rem .35rem;
  min-width: 3.2rem;
}
.access-matrix td a {
  display: block;
  padding: .1rem;
  border-radius: var(--radius-sm);
  text-decoration: none;
  color: inherit;
}
.access-matrix td a:hover { background: var(--surface-hover); }
.access-matrix td a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -1px;
}
/* Access matrix toolbar — sits above the matrix when there's content
 * to filter. The group-only toggle is a server-rendered link (URL
 * state); the Subjects / Targets inputs are wired live by
 * /static/access-matrix.js. The counters update on every keystroke. */
.access-matrix-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: .75rem 1rem;
  margin: .75rem 0;
  padding: .75rem 1rem;
  background: var(--surface);
  border-radius: var(--radius);
}
.am-filter-field {
  display: inline-flex;
  flex-direction: column;
  gap: .25rem;
  min-width: 14rem;
  flex: 1 1 14rem;
}
.am-filter-label {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--text-dim);
}
.am-filter-field input[type="search"] {
  padding: .35rem .55rem;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  font-size: var(--text-sm);
}
.am-filter-field input[type="search"]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.am-counter {
  font-size: var(--text-xs);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.cap-chips { display: inline-flex; gap: .15rem; flex-wrap: wrap; justify-content: center; }
.cap-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.1rem;
  height: 1.1rem;
  border-radius: var(--radius-sm);
  background: var(--ok-surface);
  color: var(--ok);
  font-size: var(--text-xs);
  font-weight: 600;
  font-family: ui-monospace, monospace;
  line-height: 1;
}

/* Search results list — used by the /search route. Each section
 * (Tenants / Users / iDRACs) renders a heading + a ul of clickable rows.
 * Rows mimic the table-card pattern from the mobile reflow so the search
 * results page reads cohesively with the rest of the admin. */
.search-results { list-style: none; padding: 0; margin: .25rem 0; display: flex; flex-direction: column; gap: .35rem; }
.search-results li > a {
  display: flex;
  flex-wrap: wrap;
  gap: .65rem;
  align-items: baseline;
  padding: .55rem .85rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  text-decoration: none;
  transition: border-color .12s, background .12s;
}
.search-results li > a:hover {
  border-color: var(--accent);
  background: var(--surface-hover);
}
.search-results .search-label { font-weight: 500; color: var(--heading); }

/* Dashboard stat strip — a row of small summary cards (label + value) on
 * /home above the iDRAC table. Auto-fit keeps cards comfortably sized:
 * 6-up on a 27" monitor, 4-up on standard desktop, 2-up on a tablet,
 * 1-up on narrow viewports. The wider minmax (220px) keeps each card
 * from looking lonely on a 3000px-wide screen — they grow into the
 * extra space rather than leaving the row half-empty.
 *
 * Cards become links when an href is provided so an operator can deep-link
 * into the filtered list with one click. */
.stat-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: .75rem;
  margin: 0 0 1rem;
}
.stat-card {
  display: flex;
  flex-direction: column;
  gap: .25rem;
  padding: .75rem 1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  text-decoration: none;
  transition: border-color .12s;
}
.stat-card:hover { border-color: var(--border-strong); }
.stat-card .stat-label { font-size: var(--text-xs); color: var(--text-muted); text-transform: uppercase; letter-spacing: .05em; }
.stat-card .stat-value { font-size: var(--text-lg); font-weight: 600; color: var(--heading); font-variant-numeric: tabular-nums; }
.stat-card.stat-ok .stat-value { color: var(--ok); }
.stat-card.stat-warn .stat-value { color: var(--warn); }
.stat-card.stat-danger .stat-value { color: var(--danger-text); }

/* Empty states. */
.empty { color: var(--text-muted); font-style: italic; padding: .75rem; }
.empty-state {
  text-align: center;
  padding: 3rem 1rem;
  color: var(--text-muted);
  background: var(--surface);
  border-radius: var(--radius);
  margin: 1rem 0;
}
.empty-state .empty-icon {
  font-size: 2rem;
  margin-bottom: .75rem;
  color: var(--text-dim);
  display: block;
}
.empty-state .empty-title { color: var(--text); font-size: var(--text-base); margin-bottom: .25rem; }
.empty-state .empty-desc { font-size: var(--text-sm); line-height: 1.5; max-width: 32rem; margin: 0 auto; }
.empty-state .empty-actions { margin-top: 1rem; display: flex; gap: .5rem; justify-content: center; flex-wrap: wrap; }

/* Collapsible create-form panels — keep list views uncluttered. */
details.create { margin: .75rem 0; }
details.create > summary { cursor: pointer; padding: .5rem .75rem; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); font-weight: 600; color: var(--accent); list-style: none; user-select: none; display: inline-block; }
details.create > summary::-webkit-details-marker { display: none; }
details.create > summary::before { content: '+ '; }
details.create[open] > summary::before { content: '− '; }
details.create[open] > summary { color: var(--heading-2); }
details.create .card-block { margin-top: .25rem; }

/* ---------- 11. Auth pages ---------- */

/* `padding` provides a small gutter so .auth-form/.auth-box never butt
 * against the viewport edge at narrow widths. Combined with the
 * width:min() rule below this fixes the WCAG 1.4.10 Reflow overflow at
 * 320px viewports — the form used to be `width:340px` + `padding:2rem`
 * → ~404px wide, which scrolled horizontally on a 320px viewport. */
body.auth-page {
  display: flex; align-items: center; justify-content: center; padding: .5rem;
  /* Soft accent wash from the top so the page isn't a flat void — the
   * color-mix keeps it theme-correct (mint-on-dark, forest-on-light). */
  background: radial-gradient(120rem 60rem at 50% -20%, color-mix(in srgb, var(--accent) 7%, var(--bg)), var(--bg));
}
/* <main> is a fit-content flex item, and intrinsic sizing resolves the
 * card's calc() width to auto — so a long unwrapped paragraph (reset-link
 * pages) used to inflate main past the card, leaving the card hugging
 * main's left edge. Centering inside main fixes the card regardless. */
body.auth-page main { display: flex; justify-content: center; }
.auth-box { background: var(--surface-2); padding: 2rem; border-radius: var(--radius-lg); max-width: 520px; }
.auth-form { background: var(--surface-2); padding: 2rem; border-radius: var(--radius-lg); display: flex; flex-direction: column; gap: .75rem; width: min(340px, 100% - 1rem); }
/* Card chrome shared by both auth containers: hairline border, an accent
 * top edge (the product's "front door" signature), and popup elevation.
 * Error cards (OIDC/SAML failures) swap the edge to danger. */
.auth-box, .auth-form { border: 1px solid var(--border); border-top: 3px solid var(--accent); box-shadow: var(--shadow-popup); }
.auth-box.err-card { border-top-color: var(--danger); }
.auth-form h1, .auth-box h1 { font-size: var(--text-lg); margin-bottom: .5rem; }
/* Login wordmark — larger than the sentence h1s on reset/step-up pages,
 * with a terminal-cursor underscore in the accent colour. */
h1.auth-brand { font-size: 1.7rem; font-weight: 700; letter-spacing: .01em; color: var(--heading); margin-bottom: 0; }
h1.auth-brand::after { content: '_'; color: var(--accent); }
/* Whitelabel: a tenant's own name/logo replaces the odrac wordmark. The
 * custom modifier drops the terminal-cursor "_" so it reads as a brand name. */
h1.auth-brand--custom::after { content: none; }
.auth-logo { display: block; margin: 0 auto .75rem; max-width: 220px; max-height: 84px; width: auto; height: auto; }
.auth-attribution { margin-top: 1rem; font-size: var(--text-xs); text-align: center; }
/* Whitelabel logo in the authenticated app header — sized to the wordmark. */
.chrome-logo { height: 1.4em; width: auto; max-width: 180px; vertical-align: middle; display: inline-block; }
/* Accent-colour picker row (branding form): swatch + hex field + clear. */
.brand-color-row { display: flex; gap: .5rem; align-items: center; }
.brand-color-row input[type="color"] { width: 2.4rem; height: 2.2rem; padding: 0; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: var(--bg); cursor: pointer; flex: none; }
.brand-color-row input[type="text"] { flex: 1 1 auto; min-width: 0; }
.brand-color-row .btn { flex: none; }
.auth-subtitle { color: var(--text-muted); margin: -.35rem 0 .25rem; }
.auth-box h1.err { color: var(--danger-text); font-size: 1.1rem; margin-bottom: .75rem; }
.auth-form label { font-size: var(--text-sm); color: var(--text-muted); }
.auth-form input { padding: .55rem; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: var(--bg); color: var(--text); font: inherit; }
.auth-form input.totp { letter-spacing: .3em; text-align: center; font-size: 1.4rem; }
.auth-form button, .auth-btn { padding: .55rem; border: 0; background: var(--accent-bg); color: var(--on-accent); border-radius: var(--radius-sm); cursor: pointer; font: inherit; }
.auth-form button:hover, .auth-btn:hover { background: var(--accent-bg-hover); }
.auth-form p, .auth-box p { font-size: var(--text-sm); color: var(--text-muted); line-height: 1.4; margin: .5rem 0; }
.auth-form .oidc-btn, a.oidc-btn { display: block; padding: .55rem; background: var(--oidc-bg); color: var(--text); border-radius: var(--radius-sm); text-decoration: none; text-align: center; }
.auth-form .oidc-btn:hover, a.oidc-btn:hover { background: var(--oidc-bg-hover); }
.auth-form .err, .auth-box .err { color: var(--danger-text); font-size: var(--text-sm); }
.auth-form .local { display: flex; flex-direction: column; gap: .5rem; }
.auth-card .sep, .auth-box .sep { position: relative; text-align: center; margin: .5rem 0; color: var(--text-muted); font-size: var(--text-xs); }
.auth-card .sep::before, .auth-box .sep::before { content: ''; position: absolute; top: 50%; left: 0; right: 0; border-top: 1px solid var(--border-strong); }
.auth-card .sep span, .auth-box .sep span { position: relative; background: var(--surface-2); padding: 0 .5rem; }
.auth-box.err-card h1 { color: var(--danger-text); font-size: 1.1rem; margin-bottom: .75rem; }

/* ---------- 12. Utility classes ---------- */

.inline { display: inline; }
/* Capability checkbox row used in the grant form. Sits inside a parent
 * `<label>` whose default flex-column would stack the checkbox above its
 * text — the specificity bump forces inline layout instead. Works in both
 * card-block (formCard()) and any future container that gives labels the
 * column-flex treatment. */
label.cap-checkbox { display: inline-flex; flex-direction: row; align-items: center; gap: .35rem; margin-right: 1rem; }
/* Tag chips on the /home console list. Compact label-on-rounded-rect that
 * sits inside the Tags column and degrades gracefully at narrow viewports.
 * The text filter substring-matches these so chips are real text content,
 * not pseudo-elements. */
.tag-chip {
  display: inline-block;
  padding: .05rem .4rem;
  margin: .05rem .1rem .05rem 0;
  background: var(--surface-alt, rgba(127,127,127,.12));
  border: 1px solid var(--border, rgba(127,127,127,.25));
  border-radius: 999px;
  font-size: var(--text-xs, .8rem);
  color: var(--text-muted);
  line-height: 1.4;
}
/* Tag multi-select on the iDRAC edit form — same chip-checkbox idiom as
 * .cap-checkbox so the visual rhythm matches the capability row above. */
.tag-multiselect { display: inline-flex; flex-wrap: wrap; gap: .35rem .25rem; }
.sev-security { color: var(--danger-text); }
.sev-warn { color: var(--warn); }
.label-disabled { color: var(--danger-text); }
/* Audit log's filter bar. Self-sufficient: brings its own grid display so
 * it works whether or not the parent provides grid context. .full lets a row
 * span all four columns; .actions is implicitly full-width. */
.audit-filter-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: .5rem;
  margin: .5rem 0;
}
.audit-filter-grid > .full,
.audit-filter-grid > .actions { grid-column: 1 / -1; }
.audit-filter-grid label { display: flex; flex-direction: column; gap: .25rem; font-size: var(--text-sm); color: var(--text-muted); }
.audit-filter-grid input,
.audit-filter-grid select {
  padding: .45rem;
  border: 1px solid var(--border-strong);
  background: var(--bg);
  color: var(--text);
  border-radius: var(--radius-sm);
  font: inherit;
}
.audit-filter-grid .actions { display: flex; gap: .5rem; align-items: center; }
.spaced-left-sm { margin-left: .5rem; }
.spaced-bottom { margin-bottom: 1rem; }
.qr-frame { background: var(--on-accent); display: inline-block; padding: .75rem; border-radius: var(--radius-lg); margin: 1rem 0; }
/* qrcode-generator emits an <svg> with viewBox only — no width/height. Without
 * explicit dimensions modern browsers collapse the inline SVG, so the user
 * sees an empty white pad. Pin it to a comfortable scan size. */
.qr-frame svg { display: block; width: 240px; height: 240px; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
.notes-display { white-space: pre-wrap; background: var(--surface); padding: .75rem; border-radius: var(--radius-sm); }

/* Summary grid: dl-style facts grid used on detail pages (iDRAC, tenant,
 * etc.). Replaces stacked muted paragraphs with a scannable card. Now
 * auto-fit so a wide monitor gets 3 (or 4) facts side-by-side instead
 * of two narrow columns floating in a sea of whitespace. The `minmax`
 * floor of 18rem keeps each fact's value readable; the responsive
 * @media at ≤720px collapses to a single column for mobile.
 *
 * Note: `summary-row-full` and `summary-actions` use `grid-column: 1 /
 * -1` which still works under auto-fit — they span every implicit
 * column regardless of how many auto-fit decided to use. */
.summary-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
  gap: .5rem 1.5rem;
  background: var(--surface);
  border-radius: var(--radius);
  padding: 1rem;
  margin: 1rem 0;
}
.summary-grid .summary-row { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
.summary-grid .summary-row-full { grid-column: 1 / -1; }
.summary-grid dt { font-size: var(--text-xs); color: var(--text-dim); text-transform: uppercase; letter-spacing: .05em; }
.summary-grid dd { color: var(--text); font-size: var(--text-sm); word-break: break-word; }
.summary-grid .summary-actions { grid-column: 1 / -1; border-top: 1px solid var(--border); padding-top: .75rem; margin-top: .25rem; display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
.summary-grid .summary-actions p.muted { margin: 0; }

/* Detail-page hero — opens tenant / iDRAC / /me pages. The surface holds:
 *   [title + subtitle]            [status pills]
 *   ─────────────────────────────────────────────
 *   Label · value · Label · value · …            (inline stats)
 *   ─────────────────────────────────────────────
 *   [primary action] [secondary] [secondary]     (action row, hairline above)
 *
 * `hero-stats` is a `<dl>` of label/value pairs laid out as a flowing
 * inline row; on narrow viewports the pairs wrap onto multiple lines
 * (each pair is `display: inline-flex` so the label and value never
 * separate). Action row has a top border so the buttons cluster reads
 * as a footer to the hero, not floating on the page background. */
.detail-hero {
  background: var(--surface);
  border-radius: var(--radius);
  padding: 1.25rem 1.5rem;
  margin: 0 0 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
.detail-hero .hero-heading {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
.detail-hero .hero-title-block { display: flex; flex-direction: column; gap: .25rem; min-width: 0; }
.detail-hero .page-title { margin: 0; font-size: var(--text-lg); color: var(--heading); }
.detail-hero .hero-subtitle { color: var(--text-muted); font-size: var(--text-sm); }
.detail-hero .hero-pills { display: flex; gap: .4rem; flex-wrap: wrap; align-items: center; }
.detail-hero .hero-stats {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  gap: .35rem 1.25rem;
}
.detail-hero .hero-stat {
  display: inline-flex;
  align-items: baseline;
  gap: .4rem;
  min-width: 0;
}
.detail-hero .hero-stat dt {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--text-dim);
}
.detail-hero .hero-stat dd {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--text);
  word-break: break-word;
}
.detail-hero .hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  align-items: center;
  padding-top: .85rem;
  border-top: 1px solid var(--border);
}
.detail-hero .hero-actions .muted { margin: 0; }

/* Sub-page link cluster: inline `<a> · <a> · <a>`. Lives inside a hero's
 * subtitle slot (tenant detail). Small text-style links rather than
 * button chips so they read as navigation, not actions. */
.quick-links {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .35rem .5rem;
  font-size: var(--text-sm);
}
.quick-links a { color: var(--accent); text-decoration: none; }
.quick-links a:hover { text-decoration: underline; }
.quick-links .quick-links-sep { color: var(--text-dim); }

/* Tile grid for /me — auto-fits 2 columns on desktop, 1 on mobile. */
.tile-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(22rem, 1fr));
  gap: 1rem;
  margin: 0 0 1.25rem;
}

/* Settings tile — compact card with a small uppercase heading and a list
 * of `[label] [value] [arrow]` rows. Each row is the full clickable
 * surface (anchor) when an `href` is provided; static rows omit the
 * arrow. Hover lifts the row background and the arrow colour so the row
 * reads as actionable.
 *
 * The first row has no top border so the heading reads as the row
 * header, not a separate band. */
.settings-tile {
  background: var(--surface);
  border-radius: var(--radius);
  padding: 1rem 1.25rem 1.25rem;
  display: flex;
  flex-direction: column;
  gap: .75rem;
}
.settings-tile-title {
  margin: 0;
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--text-dim);
}
.settings-list { display: flex; flex-direction: column; }
.settings-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  gap: .75rem;
  align-items: center;
  padding: .55rem 0;
  border-top: 1px solid var(--border-subtle);
  text-decoration: none;
  color: inherit;
}
.settings-row:first-child { border-top: 0; }
a.settings-row:hover { color: var(--accent); }
a.settings-row:hover .settings-row-arrow { color: var(--link); }
.settings-row-label { font-size: var(--text-sm); color: var(--text); }
.settings-row-value { font-size: var(--text-sm); color: var(--text-muted); text-align: right; }
.settings-row-arrow { color: var(--text-dim); font-size: var(--text-base); line-height: 1; }
.settings-row-static { color: var(--text); }

/* Hero-embedded staleness/empty hints — used by iDRAC detail's hardware
 * empty state and the hw section's collected-at line. */
.empty-state-inline { margin: .5rem 0; color: var(--text-muted); }

/* ARIA tabs — used on the iDRAC detail Details section to combine
 * Overview + per-component hardware tables under one surface. Wired by
 * /static/tabs.js. Server pre-renders the initial selected state so the
 * UI works (statically) without JS. */
.tabs[role="tablist"] {
  display: flex;
  flex-wrap: wrap;
  gap: .25rem;
  border-bottom: 1px solid var(--border);
  margin: .75rem 0 0;
  padding: 0;
}
.tabs [role="tab"] {
  appearance: none;
  background: transparent;
  border: 0;
  border-bottom: 2px solid transparent;
  padding: .5rem .9rem;
  font: inherit;
  font-size: var(--text-sm);
  color: var(--text-muted);
  cursor: pointer;
  margin-bottom: -1px;
}
.tabs [role="tab"]:hover { color: var(--text); }
.tabs [role="tab"][aria-selected="true"] {
  color: var(--accent);
  border-bottom-color: var(--accent);
  font-weight: 600;
}
.tabs [role="tab"]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
.tab-panel { padding-top: .5rem; }
.tab-panel[hidden] { display: none; }
.tab-rollup {
  margin: .25rem 0 .5rem;
  font-size: var(--text-sm);
}

/* Collapsible section heading — used for secondary content blocks on
 * detail pages (tenant: NetBox / Bulk import / Webhook deliveries /
 * Configuration). Same vertical rhythm as `.section` but the body is
 * hidden until the operator expands. The chevron animates via the
 * [open] attribute.
 *
 * The summary is a full-width accordion bar (surface + border + hover)
 * so a collapsed section reads as clickable, not as inert text. Kept
 * deliberately neutral: accent + "+" = details.create (add something),
 * danger + caret = danger-zone disclosure, neutral bar + caret = this. */
.collapsible-section {
  margin: 2rem 0 1rem;
}
.collapsible-section > summary {
  cursor: pointer;
  list-style: none;
  user-select: none;
  display: flex;
  align-items: center;
  padding: .5rem .75rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  transition: border-color .12s, background .12s;
}
.collapsible-section > summary:hover { border-color: var(--border-strong); background: var(--surface-hover); }
.collapsible-section[open] > summary { margin-bottom: .65rem; }
.collapsible-section > summary::-webkit-details-marker { display: none; }
.collapsible-section > summary > h3 {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  margin: 0;
}
.collapsible-section > summary > h3::before {
  content: "▸";
  color: var(--text-dim);
  font-size: var(--text-sm);
  display: inline-block;
  transition: transform .12s ease-out;
}
.collapsible-section[open] > summary > h3::before {
  transform: rotate(90deg);
}

/* Active-filter chips — pill links above a filtered table (audit log).
 * Each chip removes its facet by linking to the same URL minus that
 * query param. Clear-all is a text link so it doesn't compete visually
 * with the chips themselves. */
.filter-chips {
  display: flex;
  flex-wrap: wrap;
  gap: .35rem;
  align-items: center;
  margin: .25rem 0 .75rem;
}
.filter-chip {
  display: inline-flex;
  align-items: center;
  gap: .25rem;
  padding: .15rem .55rem;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text);
  font-size: var(--text-xs);
  text-decoration: none;
  border: 1px solid var(--border);
}
.filter-chip:hover { border-color: var(--accent); }
.filter-chip-x { color: var(--text-dim); font-weight: 600; }
.filter-chip:hover .filter-chip-x { color: var(--accent); }
.filter-chip-clear {
  font-size: var(--text-xs);
  color: var(--text-muted);
  text-decoration: none;
  margin-left: .25rem;
}
.filter-chip-clear:hover { color: var(--accent); text-decoration: underline; }

/* Tenant notes block — muted, slightly indented prose under the hero.
 * Used in place of the previous full-width Notes row in the summary
 * grid. */
.tenant-notes {
  background: var(--surface);
  border-radius: var(--radius);
  padding: .75rem 1rem;
  color: var(--text-muted);
  font-size: var(--text-sm);
  margin: 0 0 1.25rem;
}

/* Danger zone: visually segregate destructive actions (disable, delete) from
 * routine configuration. The red-tinted border + heading colour signal "this
 * is different" before the user clicks into a collapsed panel. */
.danger-zone {
  border: 1px solid var(--danger-zone-border);
  border-radius: var(--radius);
  padding: 1rem 1rem .25rem;
  /* Use color-mix() to fade --danger-surface to transparent so the same
   * rule looks right in both themes — dark red wash on dark surface, pink
   * wash on light. Falls back gracefully on older browsers (no
   * gradient overlay) per the @supports guard. */
  background: linear-gradient(to bottom, color-mix(in srgb, var(--danger-surface) 50%, transparent), transparent 4rem);
}
.danger-zone > h3 { color: var(--danger-text); margin-top: 0; }

/* Disclosure panels inside a danger zone reuse details.create for the
 * collapse behaviour, but must not look like create affordances: danger
 * colouring and a plain disclosure caret instead of the +/− prefix, so
 * "Delete tenant" never reads as "add something". */
.danger-zone details.create > summary { color: var(--danger-text); border-color: var(--danger-zone-border); }
.danger-zone details.create > summary:hover { background: var(--danger-surface); }
.danger-zone details.create > summary::before { content: '▸ '; }
.danger-zone details.create[open] > summary::before { content: '▾ '; }
.danger-zone details.create[open] > summary { color: var(--danger-text); }

/* ---------- 13. Responsive ---------- */

/* ≤960px — sidebar collapses behind a hamburger overlay. The shell drops
 * its grid and the main column reflows full-width; the sidebar slides
 * in from the left only when the user toggles `.open` via nav.js.
 * Click-outside / link-click close behaviours live in static/nav.js. */
@media (max-width: 960px) {
  .nav-toggle { display: inline-flex; align-items: center; }

  .app-shell {
    grid-template-columns: 1fr;
    padding: var(--space-4);
  }
  .sidebar {
    /* Off-canvas overlay. Slides in when nav.js adds .open. */
    position: fixed;
    top: 0;
    left: 0;
    width: 260px;
    max-width: 80vw;
    height: 100vh;
    background: var(--surface);
    border-right: 1px solid var(--border);
    padding: var(--space-4);
    transform: translateX(-100%);
    transition: transform .18s ease-out;
    z-index: 30;
    box-shadow: var(--shadow-popup);
    overflow-y: auto;
  }
  .sidebar.open { transform: translateX(0); }
  /* Scrim that dims the main content when the drawer is open — pure CSS,
   * pointer events route to the hamburger toggle to close. */
  .sidebar.open::before {
    content: '';
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, .45);
    z-index: -1;
  }
}

@media (max-width: 720px) {
  header { padding: .6rem .9rem; gap: .75rem; }
  header h1 { font-size: 1rem; }
  .user { font-size: var(--text-sm); }

  /* Collapse 2-col form grids to single column. */
  .row-grid,
  .audit-filter-grid,
  .summary-grid { grid-template-columns: 1fr; }

  /* Stat strip: pin to a 2×2 grid. auto-fit/minmax would stack the four
   * cards full-width here, pushing the actual list below the fold. */
  .stat-strip { grid-template-columns: repeat(2, 1fr); gap: .5rem; }
  .stat-card { padding: .55rem .75rem; }

  /* Hide non-essential columns on narrow screens. */
  .col-md-up { display: none; }

  /* Page header: stack title above actions. */
  .page-header { align-items: stretch; }
  .page-header .page-actions { justify-content: flex-start; }

  /* Impersonation banner: stack contents. */
  .impersonation-banner { flex-wrap: wrap; padding: .5rem .9rem; gap: .5rem; }

  /* Data-table → stacked cards.
   *
   * Below the desktop break, a normal HTML table is hostile to thumbs: too
   * many columns, horizontal scroll, microscopic action targets. We reflow
   * each row into a card whose cells render as definition-list pairs —
   * column heading on the left (provided by table.js via `data-column`),
   * cell value on the right. Cells without a label (`.col-actions`,
   * `.col-power`) right-align their content without a heading.
   *
   * Sort + filter still work — `<th>` is hidden but the JS attaches to it
   * before the @media kicks in, and reordered DOM still drives the
   * stacked card order. The toolbar (row count + filter toggle) stays
   * visible above the cards.
   */
  .table-wrap { overflow-x: visible; background: transparent; }
  .data-table { background: transparent; }
  .data-table thead { display: none; }
  /* Column controls are meaningless in the stacked-card layout: no real
   * columns to drag/resize, and the ⚙ pill is wired off by table.js here. */
  .table-toolbar .table-columns-toggle { display: none; }
  .data-table[data-table-resized] { table-layout: auto; }
  .data-table[data-table-resized] td { overflow: visible; text-overflow: clip; }
  th .col-grip, th .col-resize { display: none; }
  .data-table,
  .data-table tbody,
  .data-table tr,
  .data-table td { display: block; width: auto; }
  .data-table tr {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: .65rem .85rem;
    margin-bottom: .5rem;
    box-shadow: none;
  }
  .data-table tr:nth-child(even) td { background: transparent; }
  .data-table tr:hover td { background: transparent; }
  .data-table td {
    border: 0;
    padding: .2rem 0;
    display: flex;
    gap: .75rem;
    align-items: baseline;
    flex-wrap: wrap;
    min-height: 1.6rem;
    /* Reset col-* width constraints from desktop so the card stack uses
     * the full row width. */
    max-width: none !important;
    width: auto !important;
    white-space: normal;
  }
  .data-table td::before {
    content: attr(data-column);
    color: var(--text-dim);
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: .05em;
    flex: 0 0 7.5rem;
    /* Heading column for the dl-pair — narrow on mobile so the value
     * gets the rest of the row. */
  }
  /* Cells with no heading (actions, power-menu) stretch the value across
   * the row and right-align — no awkward empty "before" label. */
  .data-table td:not([data-column])::before,
  .data-table td[data-column=""]::before,
  .data-table td.col-actions::before,
  .data-table td.col-power::before {
    content: none;
  }
  .data-table td.col-actions,
  .data-table td.col-power {
    justify-content: flex-end;
    text-align: right;
  }
  /* Fixed-position power-menu popup needs higher z-index in card mode so
   * it floats above the next row. */
  .data-table .pw-menu-popup { z-index: 40; }

  /* The empty-state row (rendered by table.js when filter has zero
   * matches) shows on its own line in card mode. */
  .data-table tr.empty { text-align: center; }
  .data-table tr.empty td::before { content: none; }
}


/* ---------- Inline help (helpPanel / helpPopover from views.ts) -------------
 * Page-level help is rendered as a soft-bordered <details> card that sits
 * at the top of a form. Per-field help is a small inline (?) disclosure.
 * No JS — native <details> drives both.
 */
.help-panel {
  display: block;
  margin: 1.25rem 0 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2, var(--surface));
  padding: 0;
}
/* Inside the `<details class="create">` "Add provider" disclosure the
 * `<summary>` is `display: inline-block`, so its following sibling needs
 * an explicit top margin (no margin collapse path) — bump it higher than
 * the default to clearly separate the help recipe from the disclosure
 * button. */
details.create > .help-panel { margin-top: 1.25rem; }
details.create > .help-panel + .help-panel { margin-top: .5rem; }
.help-panel > summary {
  list-style: none;
  cursor: pointer;
  padding: .65rem .9rem;
  font-weight: 600;
  color: var(--text);
  border-radius: var(--radius);
  user-select: none;
}
.help-panel > summary::-webkit-details-marker { display: none; }
.help-panel > summary::before {
  content: "ⓘ ";
  color: var(--accent, var(--text-muted));
  margin-right: .25rem;
}
.help-panel[open] > summary {
  border-bottom: 1px solid var(--border);
  border-radius: var(--radius) var(--radius) 0 0;
}
.help-panel .help-body {
  padding: .25rem 1rem .75rem;
  font-size: var(--text-sm);
  line-height: 1.55;
}
.help-panel .help-body h2,
.help-panel .help-body h3,
.help-panel .help-body h4 {
  margin: 1rem 0 .25rem;
  font-size: var(--text-sm);
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--text-muted);
}
.help-panel .help-body code { font-size: .9em; }
.help-panel .help-body pre {
  background: var(--surface);
  padding: .6rem .8rem;
  border-radius: var(--radius);
  overflow-x: auto;
}
.help-panel .help-footer {
  margin: 1rem 0 0;
  font-size: var(--text-xs);
}

.help-pop {
  display: inline-block;
  position: relative;
  margin-left: .35rem;
  vertical-align: baseline;
}
.help-pop > summary {
  list-style: none;
  cursor: help;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.1rem;
  height: 1.1rem;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--surface-2, var(--surface));
  color: var(--text-muted);
  font-size: var(--text-xs);
  font-weight: 700;
  user-select: none;
}
.help-pop > summary::-webkit-details-marker { display: none; }
.help-pop[open] > summary {
  border-color: var(--accent, var(--text-muted));
  color: var(--text);
}
.help-pop .help-pop-body {
  position: absolute;
  top: calc(100% + .35rem);
  left: 0;
  z-index: 20;
  width: min(28rem, 80vw);
  padding: .65rem .9rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 .5rem 1.5rem rgba(0, 0, 0, .15);
  font-size: var(--text-sm);
  line-height: 1.5;
}
