/* GENERATED FILE. DO NOT EDIT.
 *
 * Source: NB Admin repo, shared/nb-ui/nb-ui.css
 * Regenerate: cd "NB Admin" && npm run build && npm run sync:shared
 *
 * Edits here are overwritten by the next sync, and `npm run sync:shared -- --check` fails the
 * build (this repo's as well as the hub's) while they survive.
 */

/* NB shared components.
 *
 * CANONICAL SOURCE. Edit here, then run `npm run sync:shared` from the NB Admin repo.
 * Requires tokens.css, loaded first.
 *
 * ── What this is ─────────────────────────────────────────────────────────────
 *
 * Fourteen components. This is the CONSOLIDATED INVENTORY of what four surfaces already built
 * independently over six months, not an aspirational library. Every component here replaces a named
 * set of existing classes, listed above it, and those lists are the migration checklist. Nothing was
 * added because a design system "should have" it.
 *
 * The counts are the argument for doing this at all: eleven `.badge-*` variants and eight `.dot-*`
 * variants in the OnOffboarder, plus `.chip` in the hub, `.chip` in Client Truth and `.service-chip`
 * That is twenty-two class definitions for one component with a state.
 *
 * ── Two scoping rules, and why ───────────────────────────────────────────────
 *
 * 1. Components are prefixed `.nb-`. This file OWNS that prefix estate-wide; a host stylesheet that
 *    defines or overrides an `.nb-*` selector fails conformance test C-2. When a host needs
 *    something this file does not do, that is a bug report against this file: fix it here, resync,
 *    delete the local rule. The estate already has two such cases and both were real missing
 *    capabilities, not carelessness: see nb-chip [data-kind] and .nb-extras a in nb-header.css.
 *
 * 2. Bare element selectors (table, th, h2, dl, code) apply ONLY under `.nb-app`. Same reason the
 *    header anchors every rule to `.nb-header`: this file is dropped into pages that already have
 *    their own `table {}` and `button {}` rules, and an unanchored rule would repaint whatever the
 *    host calls a table. `.nb-app` goes on <main>.
 *
 * See ../SCHEMA.md §1.4 and §1.6.
 */

/* ── page shell ────────────────────────────────────────────────────────────── */

.nb-app {
  max-width: var(--nb-content-max);
  margin: var(--nb-space-5) auto var(--nb-space-7);
  padding: 0 var(--nb-space-4);
  color: var(--nb-ink);
  font-family: var(--nb-font);
  font-size: var(--nb-text-base);
  line-height: var(--nb-leading);
}

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

/* Body background is the page's, not a component's, but every surface sets the same two properties
 * and three of them got the font stack subtly different, so it is offered here as an opt-in class on
 * <body> rather than left to be retyped a fifth time. */
.nb-body {
  margin: 0;
  background: var(--nb-canvas);
  color: var(--nb-ink);
  font-family: var(--nb-font);
  font-size: var(--nb-text-base);
  line-height: var(--nb-leading);
}

.nb-app h2 {
  font-size: var(--nb-text-lg);
  font-weight: 700;
  margin: var(--nb-space-5) 0 var(--nb-space-3);
}

.nb-app h3 {
  font-size: var(--nb-text-base);
  font-weight: 700;
  margin: var(--nb-space-4) 0 var(--nb-space-2);
}

.nb-app h2:first-child,
.nb-app h3:first-child {
  margin-top: 0;
}

.nb-app a {
  color: var(--nb-brand-dark);
}

.nb-app code,
.nb-app pre {
  font-family: var(--nb-font-mono);
  font-size: 0.92em;
}

.nb-app pre {
  background: var(--nb-surface-sunken);
  border: 1px solid var(--nb-line);
  border-radius: var(--nb-radius-sm);
  padding: var(--nb-space-3);
  overflow-x: auto;
}

/* A section heading with a trailing rule and optional right-hand controls. Replaces `.section-head`
 * (hub) and `.section-heading` (OnOffboarder), which differed only in name. */
.nb-section-head {
  display: flex;
  align-items: baseline;
  gap: var(--nb-space-3);
  margin: var(--nb-space-5) 0 var(--nb-space-3);
}

.nb-section-head > :first-child {
  margin: 0;
  font-size: var(--nb-text-lg);
  font-weight: 700;
}

.nb-section-head::after {
  content: '';
  flex: 1;
  border-bottom: 1px solid var(--nb-line);
  /* Sits on the text baseline rather than centred: a rule level with the middle of the text reads
     as a strikethrough at small sizes. */
  transform: translateY(-0.25em);
}

/* ── nb-btn ────────────────────────────────────────────────────────────────────
 *
 * Replaces: .btn .ghost-btn .approve-btn .reject-btn .defer-btn .ack-btn .cancel-task-btn
 *           .form-submit-btn .ent-save-btn .clear-all-btn .manual-add-btn .run-delete-btn
 *           .offboard-start-btn                                    (13 definitions, 4 real shapes)
 *
 * Variants, not classes per action. `.approve-btn` and `.reject-btn` are not two components; they
 * are one component with a meaning, and encoding the meaning as `data-variant` is what stops the
 * fourteenth action button from inventing a fourteenth class.
 *
 * The default variant is a bordered neutral button, which is what the great majority of these
 * actually were. `primary` is violet-filled and there SHOULD be at most one per view; violet is
 * reserved (tokens.css) and its job on a page is "the action this page is for".
 */

.nb-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--nb-space-2);
  padding: 6px 14px;
  border-radius: var(--nb-radius-sm);
  border: 1px solid var(--nb-line-strong);
  background: var(--nb-surface);
  color: var(--nb-ink);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  line-height: var(--nb-leading);
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
}

.nb-btn:hover:not(:disabled) {
  background: var(--nb-surface-sunken);
  border-color: var(--nb-ink-muted);
}

.nb-btn[data-variant='primary'] {
  background: var(--nb-brand);
  border-color: var(--nb-brand);
  color: var(--nb-ink-inverse);
}

.nb-btn[data-variant='primary']:hover:not(:disabled) {
  background: var(--nb-brand-dark);
  border-color: var(--nb-brand-dark);
}

/* Destructive. Outlined rather than filled, deliberately: a solid red button is the most visually
 * salient thing on a page, which draws the click it is warning about. The estate's destructive
 * actions (delete a run, purge a folder, push a policy) should look deliberate to reach, not
 * inviting. Filling it also loses the disabled state's meaning. */
.nb-btn[data-variant='danger'] {
  border-color: var(--nb-danger);
  color: var(--nb-danger);
}

.nb-btn[data-variant='danger']:hover:not(:disabled) {
  background: var(--nb-danger-wash);
  border-color: var(--nb-danger);
}

.nb-btn[data-variant='ghost'] {
  border-color: transparent;
  background: transparent;
  color: var(--nb-brand-dark);
}

.nb-btn[data-variant='ghost']:hover:not(:disabled) {
  background: var(--nb-brand-wash);
  border-color: transparent;
}

.nb-btn[data-size='sm'] {
  padding: 3px 9px;
  font-size: var(--nb-text-sm);
}

.nb-btn:disabled,
.nb-btn[aria-disabled='true'] {
  opacity: 0.55;
  cursor: default;
}

.nb-btn[hidden] {
  display: none;
}

.nb-btn-row {
  display: flex;
  gap: var(--nb-space-2);
  flex-wrap: wrap;
  align-items: center;
}

/* ── nb-chip ──────────────────────────────────────────────────────────────────
 *
 * Replaces: .chip (hub) .chip (Client Truth) .badge .badge-approved .badge-cancelled
 *           .badge-complete .badge-deferred .badge-emergency .badge-failed .badge-in
 *           .badge-partial .badge-pending .badge-rejected .service-chip .pending-tag
 *           .dot-acknowledged .dot-cancelled .dot-deferred .dot-failed .dot-in .dot-pending
 *           .dot-success .lane-never_verified .lane-stale .lane-verified_in_sla
 *                                                                  (24 definitions, 1 component)
 *
 * A chip is a small immutable fact: a status, a role, a source, a staleness. Never a control. If it
 * is clickable it is an nb-btn[data-size=sm] or an nb-entity.
 *
 * `data-kind` carries meaning, and the eleven OnOffboarder badge states map onto six kinds. That
 * collapse is the point; it also fixes a real problem, which is that `.badge-emergency` and
 * `.badge-failed` currently look different from each other for no reason a reader could name.
 *
 * MUST carry the state as TEXT, not only as colour. `.dot-*` was colour-only, which is unreadable
 * for roughly 1 in 12 men and invisible in a greyscale screenshot pasted into Slack, which is how
 * most of these get discussed.
 */

.nb-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--nb-space-1);
  padding: 2px 9px;
  border-radius: var(--nb-radius-pill);
  border: 1px solid var(--nb-line);
  background: var(--nb-surface-sunken);
  color: var(--nb-ink-muted);
  font-size: var(--nb-text-xs);
  font-weight: 600;
  line-height: 1.6;
  white-space: nowrap;
  vertical-align: baseline;
}

.nb-chip[data-kind='ok'] {
  border-color: var(--nb-ok);
  background: var(--nb-ok-wash);
  color: var(--nb-ok);
}

.nb-chip[data-kind='warn'] {
  border-color: var(--nb-warn);
  background: var(--nb-warn-wash);
  color: var(--nb-warn);
}

.nb-chip[data-kind='danger'] {
  border-color: var(--nb-danger);
  background: var(--nb-danger-wash);
  color: var(--nb-danger);
}

.nb-chip[data-kind='info'] {
  border-color: var(--nb-info);
  background: var(--nb-info-wash);
  color: var(--nb-info);
}

.nb-chip[data-kind='pending'] {
  border-color: var(--nb-pending);
  background: var(--nb-pending-wash);
  color: var(--nb-pending);
}

/* Explicitly inert: cancelled, superseded, not-applicable. Distinct from `neutral`, which means "a
 * fact with no valence". Replaces `.rule-inert` and `.suppressed`. */
.nb-chip[data-kind='muted'] {
  border-style: dashed;
  background: transparent;
  color: var(--nb-ink-muted);
}

/* An optional leading bullet, inheriting the chip's own colour.
 *
 * Added during the hub migration: both the hub and Managed Bookmarks build a chip with a status bullet
 * and each had its own `.chip .dot` rule. Styling it from a host stylesheet would mean writing
 * `.nb-chip .dot`, which conformance test C-2 forbids (correctly), since the dot is part of what a
 * chip IS rather than something a page decorates one with.
 *
 * `currentColor`, so it tracks `data-kind` with no per-kind rules. That is the whole reason it works
 * as a shared component: seven kinds, one declaration. */
.nb-chip-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}

.nb-chip[hidden] {
  display: none;
}

/* ── nb-stat ──────────────────────────────────────────────────────────────────
 *
 * Replaces: .tile .tile-source .kpi-row (hub) .stat .stats (Client Truth)
 *
 * A number, its label, and (required) where it came from. The source line is not decoration:
 * `.tile-source` exists in the hub because a metric with no attribution is a number nobody can
 * check, and `.source-error` exists because a metric whose upstream failed must say so rather than
 * render a stale value or a zero.
 *
 * A zero and a failure look identical in a big bold numeral. That is the failure mode this
 * component is shaped around, and it is why `data-state="error"` replaces the value rather than
 * tinting it.
 */

.nb-stat-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--nb-space-3);
  margin-bottom: var(--nb-space-4);
}

.nb-stat {
  background: var(--nb-surface);
  border: 1px solid var(--nb-line);
  border-radius: var(--nb-radius-md);
  padding: var(--nb-space-4);
  display: flex;
  flex-direction: column;
  gap: var(--nb-space-1);
}

.nb-stat-label {
  font-size: var(--nb-text-sm);
  font-weight: 600;
  color: var(--nb-ink-muted);
}

.nb-stat-value {
  font-size: var(--nb-text-2xl);
  font-weight: 800;
  line-height: 1.15;
  /* Tabular figures so a column of stats does not shimmy as values change width on refresh. */
  font-variant-numeric: tabular-nums;
}

.nb-stat-source {
  font-size: var(--nb-text-xs);
  color: var(--nb-ink-muted);
}

.nb-stat[data-state='error'] {
  border-color: var(--nb-danger);
}

.nb-stat[data-state='error'] .nb-stat-value {
  font-size: var(--nb-text-lg);
  color: var(--nb-danger);
}

.nb-stat[data-state='stale'] {
  border-color: var(--nb-warn);
}

/* Held back while the hub's idle lock is engaged, and while a value is unknown. Not a zero. See
 * the component note above. */
.nb-stat[data-state='hidden'] .nb-stat-value {
  color: var(--nb-ink-muted);
}

/* ── nb-panel ─────────────────────────────────────────────────────────────────
 *
 * Replaces: .panel .policy .policy-head .policy-body .ent-card .run-card .folder-head
 *           .folder-body .figures .rule .rule-head .pending-item .attention-list
 */

.nb-panel {
  background: var(--nb-surface);
  border: 1px solid var(--nb-line);
  border-radius: var(--nb-radius-md);
  margin-bottom: var(--nb-space-4);
  overflow: hidden; /* so a child's corner cannot escape the rounded border */
}

.nb-panel-head {
  display: flex;
  align-items: center;
  gap: var(--nb-space-3);
  padding: var(--nb-space-3) var(--nb-space-4);
  border-bottom: 1px solid var(--nb-line);
  background: var(--nb-surface-sunken);
  font-weight: 700;
}

.nb-panel-head > .nb-spacer {
  margin-left: auto;
}

.nb-panel-body {
  padding: var(--nb-space-4);
}

.nb-panel-body > :last-child {
  margin-bottom: 0;
}

/* A panel with no head, padded directly.
 *
 * For server-rendered pages where wrapping every section's contents in a second element is pure
 * markup tax. SpendTracker's Jinja templates are `<section class="nb-panel" data-pad><h2>…</h2>…`
 * and gain nothing from an inner div. The JS-driven surfaces build panels with heads and bodies and
 * should keep doing so.
 *
 * Deliberately a variant here rather than each template growing its own padded wrapper class, which
 * is how a fifth `.panel` definition gets written. */
.nb-panel[data-pad] {
  padding: var(--nb-space-4);
}

.nb-panel[data-pad] > :last-child {
  margin-bottom: 0;
}

/* A panel whose head is a summary/details disclosure. The bookmarks tree and the OnOffboarder's run
 * list both built this; neither could reuse the other's. */
.nb-panel > details > summary {
  cursor: pointer;
  list-style: none;
}

.nb-panel > details > summary::-webkit-details-marker {
  display: none;
}

.nb-panel[data-kind='danger'] {
  border-color: var(--nb-danger);
}

.nb-panel[data-kind='warn'] {
  border-color: var(--nb-warn);
}

/* ── nb-banner ────────────────────────────────────────────────────────────────
 *
 * Replaces: .banner .notice .error-msg .form-error .source-error .err .warning .impact .lede
 *
 * Something the operator needs to read BEFORE the content below it. Top of `.nb-app`, above the
 * stats. A left border rather than a full tint, so a page with two banners does not become two
 * slabs of colour.
 *
 * MUST NOT be used for a transient success toast. There are none in the estate and adding them
 * would be adding a way to miss a confirmation by looking away.
 */

.nb-banner {
  display: flex;
  align-items: flex-start;
  gap: var(--nb-space-3);
  padding: var(--nb-space-3) var(--nb-space-4);
  margin-bottom: var(--nb-space-4);
  border: 1px solid var(--nb-line);
  border-left: 3px solid var(--nb-ink-muted);
  border-radius: var(--nb-radius-sm);
  background: var(--nb-surface);
}

.nb-banner-body {
  flex: 1;
  min-width: 0;
}

.nb-banner-title {
  font-weight: 700;
}

.nb-banner-body > p {
  margin: var(--nb-space-1) 0 0;
}

.nb-banner[data-kind='ok'] {
  border-left-color: var(--nb-ok);
  background: var(--nb-ok-wash);
}

.nb-banner[data-kind='warn'] {
  border-left-color: var(--nb-warn);
  background: var(--nb-warn-wash);
}

.nb-banner[data-kind='danger'] {
  border-left-color: var(--nb-danger);
  background: var(--nb-danger-wash);
}

.nb-banner[data-kind='info'] {
  border-left-color: var(--nb-info);
  background: var(--nb-info-wash);
}

.nb-banner[hidden] {
  display: none;
}

/* ── nb-table ─────────────────────────────────────────────────────────────────
 *
 * Replaces: bare table/th/td rules in all four surfaces, .tablewrap .diff-row .audit-row
 *           .suppressed-row .attn
 *
 * `.nb-table-wrap` is not optional. Every one of these tables overflows on a laptop at some column
 * count, and an overflowing table without a scroll container pushes the whole page sideways,
 * including the header bar.
 */

.nb-table-wrap {
  overflow-x: auto;
  /* Contains the sticky header below: position: sticky resolves against the nearest scrolling
     ancestor, which must be this element and not the viewport. */
  position: relative;
}

.nb-app table,
.nb-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}

.nb-app th,
.nb-table th {
  text-align: left;
  font-weight: 600;
  color: var(--nb-ink-muted);
  background: var(--nb-surface-sunken);
  padding: var(--nb-space-2) var(--nb-space-3);
  border-bottom: 1px solid var(--nb-line-strong);
  position: sticky;
  top: 0;
  /* Under the header bar (z-index 30) and under nb-dialog. A sticky table header that slides over
     the app bar is the single most common version of this bug. */
  z-index: 2;
}

.nb-app td,
.nb-table td {
  padding: var(--nb-space-2) var(--nb-space-3);
  border-bottom: 1px solid var(--nb-line);
  vertical-align: top;
}

.nb-app tbody tr:hover td,
.nb-table tbody tr:hover td {
  background: var(--nb-surface-sunken);
}

/* Numbers right-aligned and tabular. Both Client Truth and SpendTracker discovered this
 * independently; the estate now gets it by default. */
.nb-num,
.nb-app th.nb-num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.nb-figure {
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-weight: 700;
}

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

/* Row states. Wash fills, so the state survives a greyscale screenshot only if the row ALSO carries
 * an nb-chip, which the schema requires. These are reinforcement, not the signal. */
.nb-app tr[data-state='warn'] > td,
.nb-table tr[data-state='warn'] > td {
  background: var(--nb-warn-wash);
}

.nb-app tr[data-state='danger'] > td,
.nb-table tr[data-state='danger'] > td {
  background: var(--nb-danger-wash);
}

.nb-app tr[data-state='ok'] > td,
.nb-table tr[data-state='ok'] > td {
  background: var(--nb-ok-wash);
}

/* Suppressed / superseded: legible but visibly set aside. Replaces `.suppressed-row`. */
.nb-app tr[data-state='muted'] > td,
.nb-table tr[data-state='muted'] > td {
  color: var(--nb-ink-muted);
  text-decoration: line-through;
  text-decoration-color: var(--nb-line-strong);
}

/* ── nb-dialog ────────────────────────────────────────────────────────────────
 *
 * Replaces: .dlg .dlg-bar .dlg-error
 *
 * Built on the native <dialog> element, which the bookmarks implementation already used. Focus trap,
 * Escape handling, inert background and the top layer come from the platform; a hand-rolled modal
 * gets the first three wrong and cannot do the fourth.
 */

.nb-dialog {
  border: 1px solid var(--nb-line);
  border-radius: var(--nb-radius-md);
  background: var(--nb-surface);
  color: var(--nb-ink);
  box-shadow: var(--nb-shadow-pop);
  padding: 0;
  max-width: min(560px, calc(100vw - var(--nb-space-6)));
  max-height: calc(100vh - var(--nb-space-7));
  font-family: var(--nb-font);
  font-size: var(--nb-text-base);
  line-height: var(--nb-leading);
}

.nb-dialog::backdrop {
  background: var(--nb-scrim);
}

.nb-dialog-head {
  display: flex;
  align-items: center;
  gap: var(--nb-space-3);
  padding: var(--nb-space-3) var(--nb-space-4);
  border-bottom: 1px solid var(--nb-line);
  font-weight: 700;
}

.nb-dialog-body {
  padding: var(--nb-space-4);
  overflow-y: auto;
}

/* Actions bar. Sticky at the bottom so a long confirmation (a bookmarks diff, an offboarding
 * checklist) cannot scroll its Confirm button out of reach. */
.nb-dialog-bar {
  display: flex;
  gap: var(--nb-space-2);
  justify-content: flex-end;
  align-items: center;
  padding: var(--nb-space-3) var(--nb-space-4);
  border-top: 1px solid var(--nb-line);
  background: var(--nb-surface);
  position: sticky;
  bottom: 0;
}

.nb-dialog-bar > .nb-spacer {
  margin-right: auto;
}

/* ── nb-field ─────────────────────────────────────────────────────────────────
 *
 * Replaces: .field .field-inline .form-row .form-actions
 */

.nb-field {
  display: flex;
  flex-direction: column;
  gap: var(--nb-space-1);
  margin-bottom: var(--nb-space-3);
}

.nb-field > label {
  font-size: var(--nb-text-sm);
  font-weight: 600;
  color: var(--nb-ink-muted);
}

.nb-field input,
.nb-field select,
.nb-field textarea {
  font: inherit;
  padding: 6px 10px;
  border: 1px solid var(--nb-line-strong);
  border-radius: var(--nb-radius-sm);
  background: var(--nb-surface);
  color: var(--nb-ink);
  width: 100%;
}

.nb-field-inline {
  flex-direction: row;
  align-items: center;
  gap: var(--nb-space-2);
}

.nb-field-inline > label {
  margin: 0;
}

.nb-field-hint {
  font-size: var(--nb-text-xs);
  color: var(--nb-ink-muted);
}

.nb-field[data-state='error'] input,
.nb-field[data-state='error'] select,
.nb-field[data-state='error'] textarea {
  border-color: var(--nb-danger);
}

.nb-field-error {
  font-size: var(--nb-text-sm);
  color: var(--nb-danger);
  font-weight: 600;
}

.nb-field-error[hidden] {
  display: none;
}

/* One-time codes and other fixed-length secrets. Monospace and tracked out so a transposed digit is
 * visible before submitting. Used by the OnOffboarder's step-up dialog. */
.nb-field input[data-format='code'] {
  font-family: var(--nb-font-mono);
  letter-spacing: 0.35em;
  text-align: center;
  font-size: var(--nb-text-xl);
}

/* ── nb-grid ──────────────────────────────────────────────────────────────────
 *
 * Replaces: .tool-grid .service-grid .rule-grid .run-list
 *
 * `data-min` sets the column floor; auto-fit does the rest. Four hand-written grid declarations with
 * four different breakpoints became one, and the breakpoints were never intentional, just
 * whatever looked right on the author's monitor.
 */

.nb-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--nb-space-3);
}

.nb-grid[data-min='sm'] {
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
}

.nb-grid[data-min='lg'] {
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
}

/* ── nb-defs ──────────────────────────────────────────────────────────────────
 *
 * Replaces: .disposition-detail, dl.detail
 *
 * Label/value pairs. Collapses to stacked rows on narrow screens rather than compressing the value
 * column to unreadable width.
 */

.nb-defs {
  display: grid;
  grid-template-columns: minmax(9rem, 14rem) 1fr;
  gap: var(--nb-space-1) var(--nb-space-4);
  margin: 0;
}

.nb-defs dt {
  color: var(--nb-ink-muted);
  font-size: var(--nb-text-sm);
  font-weight: 600;
}

.nb-defs dd {
  margin: 0;
  min-width: 0;
  overflow-wrap: break-word;
}

@media (max-width: 560px) {
  .nb-defs {
    grid-template-columns: 1fr;
    gap: 0;
  }

  .nb-defs dd {
    margin-bottom: var(--nb-space-2);
  }
}

/* ── nb-empty ─────────────────────────────────────────────────────────────────
 *
 * Replaces: .empty .pending-empty .tree-empty
 *
 * MUST say why it is empty, not just that it is. "No pending tasks" and "Could not load tasks" are
 * different facts and all three replaced implementations rendered them identically, which meant a
 * failed fetch looked like a clean queue. That is the whole reason this is a component.
 */

.nb-empty {
  padding: var(--nb-space-6) var(--nb-space-4);
  text-align: center;
  color: var(--nb-ink-muted);
  font-size: var(--nb-text-sm);
}

.nb-empty[data-state='error'] {
  color: var(--nb-danger);
}

/* ── nb-spinner / nb-progress ─────────────────────────────────────────────────
 *
 * Replaces: .spinner .spin .loading-strip .attention-loading
 *
 * Both honour prefers-reduced-motion through the animation tokens, and both still mark their place
 * when stopped. See tokens.css.
 */

.nb-spinner {
  display: inline-block;
  width: 14px;
  height: 14px;
  border: 2px solid var(--nb-line-strong);
  border-top-color: var(--nb-brand);
  border-radius: 50%;
  animation: var(--nb-anim-spin);
  vertical-align: -2px;
}

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

/* Indeterminate strip, for a region that is refreshing in place. Sits at the top edge of the panel
 * it belongs to rather than the top of the page; a page-level bar cannot say WHICH of four tiles is
 * reloading. */
.nb-progress {
  height: 2px;
  background: var(--nb-line);
  overflow: hidden;
}

.nb-progress::after {
  content: '';
  display: block;
  height: 100%;
  width: 40%;
  background: var(--nb-brand);
  animation: var(--nb-anim-sweep);
}

@keyframes nb-sweep {
  0% {
    transform: translateX(-100%);
  }
  100% {
    transform: translateX(250%);
  }
}

.nb-progress[hidden] {
  display: none;
}

/* ── nb-coverage ──────────────────────────────────────────────────────────────
 *
 * Promoted from SpendTracker, where it is enforced at the renderer: an aggregate cannot be produced
 * without one.
 *
 * A number on an internal admin page is a number someone will act on. "£41,200 across 38 vendors"
 * and "£41,200 across 38 vendors, 4 of which last reconciled 19 days ago" support different
 * decisions, and the second is the true one. Client Truth reached the same conclusion independently
 * and built a staleness banner; the hub built `.tile-source` for the same reason.
 *
 * Schema C-8: any rendered aggregate carries one of these. A tool that cannot state its coverage
 * does not display the aggregate.
 *
 * Left border is --nb-ok because full coverage is the normal case and should not look like a
 * warning. The `.nb-coverage-gap` block inside it is what carries the alarm.
 */

.nb-coverage {
  border: 1px solid var(--nb-line);
  border-left: 3px solid var(--nb-ok);
  border-radius: var(--nb-radius-sm);
  background: var(--nb-surface);
  padding: var(--nb-space-3) var(--nb-space-4);
  margin-bottom: var(--nb-space-4);
  font-size: var(--nb-text-sm);
}

.nb-coverage-title {
  font-weight: 700;
  margin-bottom: var(--nb-space-2);
}

.nb-coverage ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

.nb-coverage li {
  display: flex;
  justify-content: space-between;
  gap: var(--nb-space-5);
  padding: 2px 0;
}

.nb-coverage li > :last-child {
  font-variant-numeric: tabular-nums;
  color: var(--nb-ink-muted);
}

/* What could NOT be verified. Distinct fill because it is the part that changes a decision. */
.nb-coverage-gap {
  margin-top: var(--nb-space-2);
  padding: var(--nb-space-2) var(--nb-space-3);
  border-radius: var(--nb-radius-sm);
  background: var(--nb-warn-wash);
  color: var(--nb-warn);
  font-weight: 600;
}

.nb-coverage[data-state='incomplete'] {
  border-left-color: var(--nb-warn);
}

/* ── nb-entity ────────────────────────────────────────────────────────────────
 *
 * New. The wiki link. See ../SCHEMA.md §3 and ../nb-link/.
 *
 * Renders IDENTICALLY regardless of the viewer's access, and that is a security property, not a
 * shortcut. A link that appears for admins and vanishes for viewers tells a viewer both that the
 * entity exists and that they are not permitted to see it, often the more sensitive of the two
 * facts. The target applies its own gates and returns its own page, its own 403, or its own
 * sign-in.
 *
 * So there is no `data-access` state here and there will not be one.
 *
 * The type marker is a text prefix rather than an icon: six entity types would need six icons, an
 * icon set is a font or a sprite (both of which the CSP baseline makes awkward), and the type name
 * is two to six characters anyway.
 */

.nb-entity {
  display: inline-flex;
  align-items: baseline;
  gap: var(--nb-space-1);
  color: var(--nb-brand-dark);
  text-decoration: none;
  border-bottom: 1px solid var(--nb-brand-line);
  /* Wraps as a unit. A client name broken across two lines with the type marker orphaned on the
     first is unreadable in a dense table. */
  white-space: nowrap;
}

.nb-entity:hover {
  border-bottom-color: var(--nb-brand);
  background: var(--nb-brand-wash);
}

.nb-entity-type {
  font-size: var(--nb-text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--nb-ink-muted);
}

/* A URI whose type is not registered. Rendered as inert text rather than dropped: a link that
 * silently disappears is a link nobody fixes. Conformance test C-9 fails the build on one of these,
 * so this style should only ever be seen in local development. */
.nb-entity[data-unresolved='true'] {
  color: var(--nb-ink-muted);
  border-bottom-style: dotted;
  cursor: help;
}

/* ── utilities ─────────────────────────────────────────────────────────────── */

.nb-spacer {
  margin-left: auto;
}

.nb-muted {
  color: var(--nb-ink-muted);
}

.nb-row {
  display: flex;
  gap: var(--nb-space-3);
  align-items: center;
  flex-wrap: wrap;
}

.nb-footnote {
  font-size: var(--nb-text-xs);
  color: var(--nb-ink-muted);
  margin-top: var(--nb-space-2);
}

/* Visible to a screen reader, not on screen. Used for live-region announcements and for the text
 * half of any status that also has a colour. */
.nb-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── focus ─────────────────────────────────────────────────────────────────────
 *
 * One rule, last, applying to everything interactive. Conformance test C-10 asserts each component
 * has a visible focus style; a single anchored rule is easier to keep true than fourteen.
 *
 * :focus-visible, not :focus. A mouse click on a button should not leave a ring behind. The
 * separate .nb-entity rule exists because its own border-bottom would otherwise sit inside the
 * outline and read as a double line.
 */

.nb-btn:focus-visible,
.nb-chip[tabindex]:focus-visible,
.nb-entity:focus-visible,
.nb-field input:focus-visible,
.nb-field select:focus-visible,
.nb-field textarea:focus-visible,
.nb-panel summary:focus-visible {
  outline: 2px solid var(--nb-focus);
  outline-offset: 2px;
}

.nb-entity:focus-visible {
  border-bottom-color: transparent;
  border-radius: 2px;
}
