/* The canonical app shell plus the component base classes every view builds
 * on. Spec: docs-internal/TIER2-WEB-DESIGN-SYSTEM-SPEC.md.
 *
 * Every value resolves through a token from tokens.css. No color literal
 * appears here.
 */

/* --- base ---------------------------------------------------------------- */

/* The UA stylesheet's [hidden] rule is a plain `display: none`, so any author
 * rule that sets a display beats it — .wb-pager sets `display: flex`. Scripts
 * hide elements by toggling `hidden` (see static/paging.js), so the attribute
 * has to carry the stronger claim or a hidden pager keeps its box. */
[hidden] {
	display: none !important;
}

/* --- shell ---------------------------------------------------------------- */

.wb-topbar {
	position: sticky;
	top: 0;
	z-index: 20;
	background: var(--wb-topbar);
	border-bottom: 1px solid var(--wb-topbar-border);
	backdrop-filter: blur(10px);
}

.wb-topbar-inner {
	max-width: 1360px;
	margin: 0 auto;
	padding: 14px 32px;
	display: flex;
	align-items: center;
	gap: 18px;
}

.wb-page {
	max-width: 1360px;
	margin: 0 auto;
	padding: 32px;
}

/* --- brand --------------------------------------------------------------- */

.wb-brand {
	display: inline-flex;
	align-items: center;
	gap: 9px;
	text-decoration: none;
	color: var(--wb-topbar-ink);
	flex: none;
}

/* The brand-mark slot, painting --wb-brand-mark (tokens.css). Sized to the
 * Scout icon's ~1.9:1 aspect so the mark fills it at 22px tall. If the token
 * is undefined it falls back to a flat --wb-primary swatch. */
.wb-logo {
	width: 42px;
	height: 22px;
	background: var(--wb-brand-mark, var(--wb-primary)) center / contain no-repeat;
	flex: none;
}

.wb-wordmark {
	font-size: 15.5px;
	font-weight: 600;
	letter-spacing: -0.2px;
}

/* --- nav ----------------------------------------------------------------- */

.wb-nav {
	display: flex;
	gap: 6px;
	flex-wrap: wrap;
}

.wb-tab {
	padding: 6px 12px;
	border-radius: var(--wb-radius-control);
	font-size: 13px;
	font-weight: 500;
	color: var(--wb-topbar-ink-2);
	text-decoration: none;
	background: transparent;
	white-space: nowrap;
}

.wb-tab:hover {
	color: var(--wb-topbar-ink);
	background: var(--wb-topbar-hover);
}

.wb-tab.is-active {
	color: var(--wb-topbar-ink);
	background: var(--wb-topbar-hover);
}

/* --- topbar right rail --------------------------------------------------- */

.wb-topbar-right {
	margin-left: auto;
	display: flex;
	align-items: center;
	gap: 10px;
}

/* --- topbar disclosure menus --------------------------------------------
 * Two independent surfaces on the right rail: an org menu whose trigger is
 * the org NAME itself (so the current context is legible without opening
 * anything), and a user avatar for theme + log out. Each is a <details> so
 * the popup opens without JavaScript; static/nav-menu.js layers on
 * outside-click / focusout / Escape close.
 */

.wb-nav-menu {
	position: relative;
	flex: none;
}

.wb-avatar {
	width: 30px;
	height: 30px;
	border-radius: var(--wb-radius-pill);
	background: var(--wb-topbar-hover);
	border: 1px solid var(--wb-topbar-border);
	color: var(--wb-topbar-ink);
	font-size: 11px;
	font-weight: 600;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	cursor: pointer;
	list-style: none;
}

/* The org trigger reads as a text button carrying the current org name plus
 * a caret. Same height as the avatar so the two triggers align on the rail;
 * padding gives the name room to breathe without spilling into the avatar. */
.wb-org-trigger {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	height: 30px;
	padding: 0 10px;
	border: 1px solid var(--wb-topbar-border);
	border-radius: var(--wb-radius-control);
	background: var(--wb-topbar-hover);
	color: var(--wb-topbar-ink);
	font-size: 13px;
	font-weight: 600;
	cursor: pointer;
	list-style: none;
	white-space: nowrap;
}

.wb-org-name {
	max-width: 200px;
	overflow: hidden;
	text-overflow: ellipsis;
}

/* Caret is a CSS-drawn chevron so it inherits colour with the trigger; no
 * SVG file to keep in step with the palette. */
.wb-org-caret {
	width: 8px;
	height: 8px;
	border-right: 1.5px solid currentColor;
	border-bottom: 1.5px solid currentColor;
	transform: translateY(-2px) rotate(45deg);
	opacity: 0.7;
	flex: none;
}

/* Suppress the native disclosure triangle in both engines, for either
 * trigger shape. */
.wb-avatar::-webkit-details-marker,
.wb-org-trigger::-webkit-details-marker {
	display: none;
}

.wb-menu {
	position: absolute;
	right: 0;
	top: calc(100% + 8px);
	min-width: 168px;
	padding: 6px;
	background: var(--wb-panel);
	border: 1px solid var(--wb-border);
	border-radius: var(--wb-radius-card);
	box-shadow: var(--wb-shadow);
	display: flex;
	flex-direction: column;
	gap: 2px;
	z-index: 30;
}

/* The org menu is wider than the user avatar's menu — the switch list can
 * carry longer org names, and Settings / Manage read cleaner with a little
 * more space than a two-item theme toggle needs. */
.wb-menu-org {
	min-width: 220px;
}

/* Divider between the top section (Settings, Manage) and the Switch-org
 * list. A one-pixel line inset the same amount as menu items — draws the
 * eye without breaking the menu's rhythm. */
.wb-menu-divider {
	height: 1px;
	margin: 4px 6px;
	background: var(--wb-border);
}

.wb-menu-form {
	margin: 0;
	display: contents;
}

.wb-menu-item {
	display: block;
	width: 100%;
	text-align: left;
	padding: 7px 10px;
	border: 0;
	border-radius: var(--wb-radius-control);
	background: transparent;
	color: var(--wb-ink-2);
	font: inherit;
	font-size: 13px;
	text-decoration: none;
	cursor: pointer;
}

.wb-menu-item:hover {
	background: var(--wb-panel-alt);
	color: var(--wb-ink);
}

/* Reveal whichever theme the current rendering is NOT. This mirrors the
 * token blocks' selector pattern exactly: light by default, the media query
 * flips it when the OS prefers dark and no explicit choice was made, and the
 * data-theme attribute overrides both. */
.wb-offer-dark {
	display: contents;
}

.wb-offer-light {
	display: none;
}

@media (prefers-color-scheme: dark) {
	:root:where(:not([data-theme="light"])) .wb-offer-dark {
		display: none;
	}

	:root:where(:not([data-theme="light"])) .wb-offer-light {
		display: contents;
	}
}

:root[data-theme="dark"] .wb-offer-dark {
	display: none;
}

:root[data-theme="dark"] .wb-offer-light {
	display: contents;
}

/* --- accessibility ------------------------------------------------------- */

.wb-sr-only {
	/* Pinned to the containing block's origin: with `left`/`top` left at auto
	 * the box takes its static position, and with no positioned ancestor that
	 * lands it in the initial containing block — outside whatever scroller it
	 * nominally sits in — where it pushes the document sideways instead of
	 * being clipped. A definite position keeps it out of layout overflow
	 * wherever the utility is used. Either inset alone covers the cases seen
	 * so far; both are declared so the pin does not depend on which axis the
	 * caller's scroller happens to overflow on. */
	position: absolute;
	top: 0;
	left: 0;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
	border: 0;
}

/* --- component base classes ---------------------------------------------
 * Views compose layout from these; they must not restate treatment.
 */

/* Buttons — exactly three tiers: primary, ghost, quiet. No gradient, no
 * text-shadow, no uppercase, no letter-spacing. Deliberately class-based:
 * there is no bare `button` element selector anywhere, so nothing is styled
 * by accident.
 *
 * wb-btn-danger is not a fourth tier — it is the ghost tier carrying the
 * danger role, so a destructive action reads as destructive without becoming
 * the loudest thing on the screen. */

.wb-btn {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	padding: 9px 18px;
	border-radius: var(--wb-radius-control);
	font: inherit;
	font-size: 13px;
	font-weight: 500;
	line-height: 1.2;
	cursor: pointer;
	text-decoration: none;
	border: 1px solid transparent;
}

.wb-btn-primary {
	background: var(--wb-primary);
	color: var(--wb-primary-ink);
	font-weight: 600;
}

.wb-btn-primary:hover {
	background: var(--wb-primary-hover);
}

.wb-btn-ghost {
	background: var(--wb-panel);
	border-color: var(--wb-border);
	color: var(--wb-ink);
}

.wb-btn-ghost:hover {
	background: var(--wb-panel-alt);
}

.wb-btn-quiet {
	background: transparent;
	color: var(--wb-ink-3);
	padding-left: 6px;
	padding-right: 6px;
}

.wb-btn-quiet:hover {
	color: var(--wb-ink);
	text-decoration: underline;
}

.wb-btn-danger {
	background: transparent;
	border-color: var(--wb-border);
	color: var(--wb-danger);
}

/* Cards and panels. One level of nesting only: a panel inside a panel uses
 * the raised surface and drops its own border. */

.wb-card {
	background: var(--wb-panel);
	border: 1px solid var(--wb-border);
	border-radius: var(--wb-radius-card);
	box-shadow: var(--wb-shadow);
}

.wb-card-pad {
	padding: 18px;
}

.wb-card .wb-card {
	background: var(--wb-panel-alt);
	border: 0;
	box-shadow: none;
}

.wb-card-head {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: 12px;
	padding: 14px 18px;
	border-bottom: 1px solid var(--wb-border-soft);
}

.wb-card-title {
	font-size: 14px;
	font-weight: 600;
	color: var(--wb-ink);
}

.wb-card-note {
	font-size: 11px;
	color: var(--wb-ink-4);
}

/* Tables — quiet headers (no filled bar), hairline rules, row hover,
 * tabular numerics. */

.wb-table {
	width: 100%;
	border-collapse: collapse;
	font-variant-numeric: tabular-nums;
}

/* :where() for the same reason the cell rule below uses it, on alignment
 * rather than colour: a bare `.wb-table th` is (0,1,1) and .wb-num is (0,1,0),
 * so the descendant selector kept `text-align: left` on the header of every
 * right-aligned column and the label floated away from the numbers it names.
 * :where() contributes no specificity, so this drops to (0,1,0) and source
 * order decides — the modifiers below win. */
.wb-table :where(th) {
	text-align: left;
	padding: 8px 18px;
	background: transparent;
	border-bottom: 1px solid var(--wb-border);
	font-size: 10.5px;
	font-weight: 600;
	letter-spacing: 0.6px;
	text-transform: uppercase;
	color: var(--wb-ink-3);
	white-space: nowrap;
}

/* :where() so a cell's own modifier class wins on colour. A bare
 * `.wb-table td` is (0,1,1) and a single class like .wb-status-error is
 * (0,1,0), so the descendant selector silently swallowed every status and
 * error-count colour in every table. :where() contributes no specificity, so
 * this drops to (0,1,0) and source order decides — the modifiers below win. */
.wb-table :where(td) {
	padding: 12px 18px;
	border-bottom: 1px solid var(--wb-border-soft);
	font-size: 12.5px;
	color: var(--wb-ink-2);
}

.wb-table tbody tr:hover {
	background: var(--wb-panel-alt);
}

/* A row that carries a state block rather than data takes no hover: the hover
 * fill is --wb-panel-alt, which is also .wb-banner's fill, so hovering would
 * dissolve a failure banner into the row and leave only its keyline. */
.wb-table tbody tr.wb-row-state:hover {
	background: transparent;
}

.wb-table tbody tr:last-child td {
	border-bottom: 0;
}

/* --- expandable row detail ------------------------------------------------
 * A detail row is a sibling <tr> carrying one call's error text. It is
 * rendered open by the server and collapsed on init by static/row-detail.js,
 * so the text is readable with no JavaScript at all.
 *
 * Collapse is a `data-collapsed` attribute rather than `hidden`, because
 * `hidden` is how the pager takes a row off the current page. Keeping the two
 * on separate attributes keeps them independent — a detail the reader closed
 * stays closed when its page comes back into view — and nothing here sets
 * `display` on an expanded row, so `hidden` still wins wherever it is set.
 */

.wb-row-detail[data-collapsed] {
	display: none;
}

/* The disclosure is the status cell itself: a bare control carrying the
 * status marker, so the row gains an affordance without gaining a column.
 * Not one of the three .wb-btn tiers — it is a disclosure, and it must read
 * as the table cell it replaces rather than as an action. */
.wb-row-toggle {
	/* Positioned so the .wb-sr-only span inside it resolves its containing
	 * block here. Without this the span's containing block is the viewport,
	 * which puts it outside the table's overflow-x scroller and scrolls the
	 * whole page sideways on a narrow screen. */
	position: relative;
	display: inline-flex;
	align-items: center;
	gap: 6px;
	padding: 0;
	border: 0;
	background: transparent;
	font: inherit;
	color: inherit;
	cursor: pointer;
}

/* CSS-drawn chevron, as on the org menu's trigger: it points right when
 * closed and down when open, and takes the danger role so it belongs to the
 * marker beside it. */
.wb-row-toggle-caret {
	width: 6px;
	height: 6px;
	flex: none;
	border-right: 1.5px solid var(--wb-danger);
	border-bottom: 1.5px solid var(--wb-danger);
	transform: translateX(-1px) rotate(-45deg);
}

.wb-row-toggle[aria-expanded="true"] .wb-row-toggle-caret {
	transform: translateY(-2px) rotate(45deg);
}

/* An open detail and its call read as one block: the hairline between them is
 * dropped while the detail is showing. Cosmetic only — an engine that drops
 * this rule keeps the hairline and loses nothing else. */
.wb-table tbody tr:has(+ .wb-row-detail:not([data-collapsed])) > td {
	border-bottom-color: transparent;
}

/* The cell keeps the table's horizontal padding and loses its top padding, so
 * the block below hangs directly off the call above it. */
.wb-table tbody .wb-row-detail > td {
	padding-top: 0;
}

/* Indent, danger keyline and danger tint: three channels saying this belongs
 * to the row above. Same 3px keyline the banners use for a status surface. */
.wb-row-detail-body {
	display: flex;
	align-items: baseline;
	gap: 10px;
	margin-left: 18px;
	padding: 9px 14px;
	background: var(--wb-chip-high-bg);
	border-left: 3px solid var(--wb-danger);
	border-radius: 0 var(--wb-radius-bar) var(--wb-radius-bar) 0;
}

.wb-row-detail-label {
	flex: none;
	font-size: 10.5px;
	font-weight: 600;
	letter-spacing: 0.6px;
	text-transform: uppercase;
	color: var(--wb-danger);
}

/* pre-wrap keeps the line breaks in stack output; overflow-wrap breaks a
 * single long token — an absolute path, a base64 blob — rather than letting
 * it push the table wider than its wrapper. min-width:0 lets the flex item
 * shrink below its content width so both take effect. */
.wb-row-detail-text {
	min-width: 0;
	font-family: var(--wb-font-mono);
	font-size: 12px;
	color: var(--wb-ink-2);
	white-space: pre-wrap;
	overflow-wrap: anywhere;
}

.wb-num {
	font-family: var(--wb-font-mono);
	text-align: right;
	font-variant-numeric: tabular-nums;
}

/* Inline meter for percentage-of-total columns. */

.wb-meter {
	height: 6px;
	border-radius: var(--wb-radius-bar);
	background: var(--wb-track);
	overflow: hidden;
}

.wb-meter-fill {
	height: 100%;
	border-radius: var(--wb-radius-bar);
	background: var(--wb-primary);
}

/* A meter's painted width, in 5% steps, as a class rather than a
 * style="width: …" attribute. The attribute is what forces a CSP to carry
 * style-src-attr 'unsafe-inline'; a class costs twenty-one rules and lets the
 * policy stay at style-src 'self'. The exact share is never lost — it is in
 * each meter's aria-label and in the numeric column beside it. Generated by
 * meterFill() in templatefuncs.go; the step constant lives there. */

.wb-fill-0 { width: 0%; }
.wb-fill-5 { width: 5%; }
.wb-fill-10 { width: 10%; }
.wb-fill-15 { width: 15%; }
.wb-fill-20 { width: 20%; }
.wb-fill-25 { width: 25%; }
.wb-fill-30 { width: 30%; }
.wb-fill-35 { width: 35%; }
.wb-fill-40 { width: 40%; }
.wb-fill-45 { width: 45%; }
.wb-fill-50 { width: 50%; }
.wb-fill-55 { width: 55%; }
.wb-fill-60 { width: 60%; }
.wb-fill-65 { width: 65%; }
.wb-fill-70 { width: 70%; }
.wb-fill-75 { width: 75%; }
.wb-fill-80 { width: 80%; }
.wb-fill-85 { width: 85%; }
.wb-fill-90 { width: 90%; }
.wb-fill-95 { width: 95%; }
.wb-fill-100 { width: 100%; }

/* KPI tiles — left-aligned label above value, mono tabular numerals. The row
 * is the tiles' own layout container: auto-fit collapses empty tracks, so a
 * set of tiles spreads across a wide page and reflows down to one column
 * without a media query. Layout only — the tiles carry the treatment. */

.wb-kpi-row {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
	gap: 16px;
}

.wb-kpi {
	background: var(--wb-panel);
	border: 1px solid var(--wb-border);
	border-radius: var(--wb-radius-card);
	padding: 16px;
}

.wb-kpi-label {
	display: block;
	font-size: 10.5px;
	font-weight: 600;
	letter-spacing: 0.6px;
	text-transform: uppercase;
	color: var(--wb-ink-3);
}

.wb-kpi-value {
	display: block;
	margin-top: 4px;
	font-family: var(--wb-font-mono);
	font-size: 23px;
	font-weight: 500;
	letter-spacing: -0.5px;
	color: var(--wb-ink);
	font-variant-numeric: tabular-nums;
}

.wb-kpi-delta {
	font-size: 11px;
}

.wb-delta-up {
	color: var(--wb-ok);
}

.wb-delta-down {
	color: var(--wb-danger);
}

/* Badges — the reserved status chip pairs only. Nothing else introduces a
 * color, and a series slot is never used here. */

.wb-badge {
	display: inline-block;
	padding: 2px 8px;
	border-radius: var(--wb-radius-pill);
	font-size: 11px;
	font-weight: 600;
	color: var(--wb-chip-low);
	background: var(--wb-chip-low-bg);
}

.wb-badge-high {
	color: var(--wb-chip-high);
	background: var(--wb-chip-high-bg);
}

.wb-badge-med {
	color: var(--wb-chip-med);
	background: var(--wb-chip-med-bg);
}

.wb-badge-low {
	color: var(--wb-chip-low);
	background: var(--wb-chip-low-bg);
}

/* Banners — raised surface with a keyline in the status color carried. */

.wb-banner {
	display: flex;
	align-items: center;
	gap: 8px;
	flex-wrap: wrap;
	background: var(--wb-panel-alt);
	border: 1px solid var(--wb-border);
	border-left: 3px solid var(--wb-accent);
	border-radius: var(--wb-radius-card);
	padding: 13px 18px;
	font-size: 12.5px;
	color: var(--wb-ink-2);
}

.wb-banner-danger {
	border-left-color: var(--wb-danger);
}

.wb-banner-em {
	color: var(--wb-accent);
	font-weight: 600;
}

/* Emphasis inside a danger banner takes the danger role — the accent would
 * read as the anomaly callout on a red-keylined surface. */
.wb-banner-danger .wb-banner-em {
	color: var(--wb-danger);
}

/* Empty states — never a bare header or an empty axis. */

.wb-empty {
	padding: 28px 18px;
	text-align: center;
	color: var(--wb-ink-2);
	font-size: 13px;
}

.wb-empty-actions {
	margin-top: 12px;
	display: flex;
	gap: 10px;
	justify-content: center;
	flex-wrap: wrap;
}

/* Page head — the H1 step plus its lede, above a page's cards. */

.wb-page-head {
	margin-bottom: 20px;
}

.wb-h1 {
	margin: 0;
	font-size: 23px;
	font-weight: 600;
	letter-spacing: -0.4px;
	color: var(--wb-ink);
}

.wb-lede {
	margin: 4px 0 0;
	font-size: 13px;
	color: var(--wb-ink-2);
}

/* Vertical rhythm between a page's cards. Layout only — the cards carry
 * their own treatment. */

.wb-stack {
	display: flex;
	flex-direction: column;
	gap: 16px;
}

/* Sub-navigation within a section. The entries are .wb-tab, so a sub-nav is
 * layout around the nav treatment that already exists rather than a second
 * one. */

.wb-subnav {
	display: flex;
	flex-wrap: wrap;
	gap: 4px;
	margin-bottom: 20px;
	border-bottom: 1px solid var(--wb-border-soft);
	padding-bottom: 10px;
}

/* Forms — the control surface. Inputs and selects are the only elements that
 * sit on --wb-control; buttons are the three .wb-btn tiers. */

.wb-form-row {
	display: flex;
	flex-wrap: wrap;
	align-items: flex-end;
	gap: 12px;
}

.wb-field {
	display: flex;
	flex-direction: column;
	gap: 6px;
	min-width: 0;
}

.wb-field-grow {
	flex: 1 1 220px;
}

.wb-label {
	font-size: 10.5px;
	font-weight: 600;
	letter-spacing: 0.6px;
	text-transform: uppercase;
	color: var(--wb-ink-3);
}

.wb-input,
.wb-select {
	width: 100%;
	padding: 9px 10px;
	background: var(--wb-control);
	border: 1px solid var(--wb-border);
	border-radius: var(--wb-radius-control);
	color: var(--wb-ink);
	font-family: var(--wb-font-sans);
	font-size: 13px;
	line-height: 1.2;
}

.wb-input::placeholder {
	color: var(--wb-ink-4);
}

.wb-hint {
	font-size: 11px;
	color: var(--wb-ink-4);
}

/* Inline actions inside a table cell or card head. */

.wb-actions {
	display: flex;
	flex-wrap: wrap;
	gap: 8px;
	align-items: center;
}

.wb-actions-end {
	justify-content: flex-end;
}

/* Inline mono for identifiers in a table cell. Numerics use .wb-num, which
 * right-aligns; an identifier reads left-aligned. */

.wb-mono {
	font-family: var(--wb-font-mono);
	font-variant-numeric: tabular-nums;
	overflow-wrap: anywhere;
}

/* A rendered token / identifier. Mono, wrapping, on the raised surface. */

.wb-code {
	display: block;
	padding: 10px 12px;
	background: var(--wb-panel-alt);
	border: 1px solid var(--wb-border-soft);
	border-radius: var(--wb-radius-control);
	color: var(--wb-ink);
	font-family: var(--wb-font-mono);
	font-size: 12.5px;
	line-height: 1.5;
	overflow-wrap: anywhere;
}

/* The block form: a captured payload, where the newlines carry the shape.
 * `pre-wrap` keeps them while letting the long lines fold, so the wrapping
 * the class already asks for applies here too and a single unbroken token —
 * an absolute path, a base64 blob, minified JSON — folds instead of running
 * out past the card. A tall payload scrolls inside the box rather than
 * pushing the sections under it off the page. */

pre.wb-code {
	white-space: pre-wrap;
	max-height: 60vh;
	overflow-y: auto;
}

/* A banner carrying the ok status rather than the default accent keyline. */

.wb-banner-ok {
	border-left-color: var(--wb-ok);
}

/* Inline plaintext token in a table cell alongside a Copy affordance. The
 * token wraps if the column is narrow; the button stays snug on the right so
 * the row height does not grow when the value wraps. */

.wb-key-cell {
	display: inline-flex;
	align-items: center;
	gap: 8px;
	flex-wrap: wrap;
	max-width: 100%;
}

.wb-key-value {
	overflow-wrap: anywhere;
	word-break: break-all;
}

/* Post-mutation reveal, sitting above the page without shifting it. Native
 * <dialog> with showModal() gives the backdrop and Escape-to-close for free.
 * The .wb-modal-body form uses method="dialog", so the Done button submits
 * and closes without a page reload. */

.wb-modal {
	padding: 0;
	border: 1px solid var(--wb-border);
	border-radius: var(--wb-radius-card);
	background: var(--wb-panel);
	color: var(--wb-ink);
	box-shadow: var(--wb-shadow);
	max-width: min(560px, calc(100vw - 32px));
	width: 100%;
}

.wb-modal::backdrop {
	background: var(--wb-scrim);
}

/* The no-scripting path. A dialog reached as the document's fragment target
 * shows itself, so a plain link opens it and a link pointing anywhere else
 * closes it — no `open` attribute, no showModal(). It is `fixed` and centred
 * rather than in flow because the control that opens it sits inside
 * .wb-table-wrap, and that scroller clips anything positioned within it.
 *
 * There is no backdrop and no focus trap on this path — a fragment target is
 * not a modal. With scripting the same element is opened by showModal()
 * instead, which gives both, and the page stamps data-js on <html> so this
 * rule stops applying: the two presentations are then mutually exclusive by
 * construction rather than by the script keeping the fragment tidy. A
 * fragment can arrive after load and outlive a history rewrite, so the rule
 * has to be switched off rather than steered around. */

:root:not([data-js]) .wb-modal:target {
	display: block;
	position: fixed;
	inset: 0;
	margin: auto;
	height: fit-content;
	max-height: calc(100vh - 32px);
	overflow: auto;
	z-index: 100;
}

.wb-modal-body {
	display: flex;
	flex-direction: column;
	gap: 12px;
	padding: 18px 20px 16px;
}

/* --- links ---------------------------------------------------------------
 * The base anchor treatment, so an anchor carrying no component class still
 * resolves through the palette rather than the browser default. Element
 * specificity (0,0,1) — every component class that sets its own colour wins:
 * .wb-btn tiers, .wb-tab, .wb-brand, .wb-menu-item, .wb-link.
 *
 * There is deliberately NO `a:hover` pair. `a:hover` is (0,1,1), which beats
 * .wb-brand / .wb-btn-ghost / .wb-btn-quiet outright and ties-then-wins on
 * source order against .wb-tab:hover, .wb-menu-item:hover and
 * .wb-btn-primary:hover — it would turn the whole topbar into a link on
 * hover. Hover feedback for a real link comes from .wb-link:hover.
 */

a {
	color: var(--wb-link);
	text-decoration: none;
}

/* --- focus --------------------------------------------------------------
 * Never removed. Applied to the shell's own interactive elements and to
 * anything a view builds from the base classes.
 */

a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
summary:focus-visible,
dialog:focus-visible,
[tabindex]:focus-visible {
	outline: 2px solid var(--wb-primary);
	outline-offset: 2px;
}

/* --- session, login and filter-control styles (#222) -------------
 * Kept separate from the management styles above: the two landed on
 * independent branches and share no selector.
 */

.wb-menu-label {
	display: block;
	padding: 6px 10px 2px;
	font-size: 11px;
	color: var(--wb-ink-4);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
	max-width: 240px;
}

.wb-menu-item.is-current {
	color: var(--wb-accent);
	cursor: default;
}

.wb-menu-item.is-current:hover {
	background: transparent;
	color: var(--wb-accent);
}

.wb-field-label {
	font-size: 10.5px;
	font-weight: 600;
	letter-spacing: 0.6px;
	text-transform: uppercase;
	color: var(--wb-ink-3);
}

.wb-auth {
	display: flex;
	justify-content: center;
	padding-top: 6vh;
}

.wb-auth-card {
	width: 100%;
	max-width: 380px;
	display: flex;
	flex-direction: column;
	gap: 14px;
}

.wb-auth-title {
	font-size: 23px;
	font-weight: 600;
	letter-spacing: -0.4px;
	color: var(--wb-ink);
	margin: 0;
}

.wb-auth-lede {
	margin: 0;
	font-size: 13px;
	color: var(--wb-ink-2);
}

.wb-auth-form {
	display: flex;
	flex-direction: column;
	gap: 12px;
}

.wb-auth-submit {
	justify-content: center;
}

.wb-auth-alt {
	display: flex;
	flex-direction: column;
	gap: 8px;
	padding-top: 12px;
	border-top: 1px solid var(--wb-border-soft);
}

.wb-auth-alt .wb-btn {
	justify-content: center;
}

.wb-filters {
	display: flex;
	align-items: flex-end;
	gap: 12px;
	flex-wrap: wrap;
	margin-bottom: 16px;
}

.wb-filters .wb-field {
	min-width: 150px;
}

.wb-filters-actions {
	display: flex;
	align-items: center;
	gap: 6px;
}

/* A filter bar that narrows one card's table rather than the whole page sits
   inside that card, under its head, and borrows the card's own padding so it
   lines up with the table beneath it. */
.wb-filters-inset {
	padding: 14px 18px 0;
	margin-bottom: 14px;
}

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

/* The scroll floor is opt-in. It buys a many-columned table room to keep its
   columns apart on a narrow screen, at the cost of a horizontal scroll inside
   .wb-table-wrap. A table whose card is narrower than the floor pushes its
   last column out of sight, so a table with few enough columns to fit its
   card carries .wb-table alone and sizes to the card. */
.wb-table-scroll {
	min-width: 720px;
}

.wb-pager {
	display: flex;
	align-items: center;
	justify-content: flex-end;
	gap: 10px;
	padding: 12px 18px;
	border-top: 1px solid var(--wb-border-soft);
	font-size: 12px;
	color: var(--wb-ink-3);
}

.wb-empty-title {
	font-weight: 600;
	color: var(--wb-ink);
}

.wb-empty-hint {
	margin-top: 6px;
	font-size: 12px;
	color: var(--wb-ink-4);
}

.wb-cell-mono {
	font-family: var(--wb-font-mono);
	font-variant-numeric: tabular-nums;
}

.wb-cell-muted {
	color: var(--wb-ink-4);
}

.wb-link {
	color: var(--wb-link);
	text-decoration: none;
}

.wb-link:hover {
	color: var(--wb-link-hover);
	text-decoration: underline;
}

.wb-errors-hot {
	color: var(--wb-danger);
}

.wb-errors-zero {
	color: var(--wb-ink-4);
}

.wb-cell-sub {
	display: block;
	font-size: 11px;
	color: var(--wb-ink-4);
}

.wb-status-ok {
	color: var(--wb-ok);
}

/* Failure differs on two channels, not just hue: at the 12.5px table step a
 * single error row among a hundred is a needle, so it also carries weight. */
.wb-status-error {
	color: var(--wb-danger);
	font-weight: 600;
}

/* A denied call never ran -- a permission rule, a hook or a human said no.
 * It is neither a success nor a failure, so it takes the warn hue rather
 * than the danger one, and stays at normal weight so it cannot be mistaken
 * for an error while scanning a column. */
.wb-status-rejected {
	color: var(--wb-warn);
}

.wb-kv {
	display: flex;
	flex-direction: column;
}

.wb-kv-row {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: 16px;
	padding: 10px 0;
	border-bottom: 1px solid var(--wb-border-soft);
}

.wb-kv-row:last-child {
	border-bottom: 0;
}

.wb-kv-label {
	font-size: 13px;
	color: var(--wb-ink-3);
}

.wb-kv-value {
	font-family: var(--wb-font-mono);
	font-variant-numeric: tabular-nums;
	font-weight: 500;
	color: var(--wb-ink);
}

.wb-kv-total .wb-kv-value {
	color: var(--wb-accent);
}

/* Both cards in a row share that row's height, so the pair reads as one
 * band rather than as two columns with ragged bottoms. Only the surface
 * grows: no content is padded out to match, and a chart keeps its own
 * aspect ratio rather than stretching to fill.
 *
 * The padded body takes the slack and centres what sits in it — a chart,
 * a stat list, an empty state. A read-error banner is pinned back to the
 * top: an alert belongs where the eye lands, not floating mid-card. A
 * table is left alone in its bare .wb-table-wrap, because growing that
 * would carry its horizontal scrollbar down to the card's bottom edge,
 * away from the rows it scrolls.
 *
 * Once the grid wraps to one column per row the stretch is a no-op and
 * each card is back to its own content height. */
.wb-split {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
	gap: 16px;
	margin-bottom: 16px;
}

.wb-split > .wb-card {
	display: flex;
	flex-direction: column;
}

.wb-split > .wb-card > .wb-card-pad {
	flex: 1;
	display: flex;
	flex-direction: column;
	justify-content: center;
}

.wb-split > .wb-card > .wb-card-pad > .wb-banner {
	margin-bottom: auto;
}

.wb-split > .wb-card > .wb-empty {
	margin-top: auto;
	margin-bottom: auto;
}

