/* ─────────────────────────────────────────────────────────────────────────
   AssurKit — application styles

   Loaded AFTER tokens.css. Token vars (--color-*, --space-*, --type-*) are
   available. Any color in here must come from a token; no raw hex/rgb.

   Layout:
     - The two-column app shell (sidebar + main content) inherits surface
       colors from tokens automatically; we only override the sidebar's
       background to use the band surface (slightly creamier than body).
     - Bulma's whole color modifier set is already redirected in tokens.css.
       This file holds AssurKit-specific component classes only.
   ───────────────────────────────────────────────────────────────────────── */

/* ─── App shell — sidebar (always dark, regardless of theme) ─────────────── */

/* The sidebar is a deliberate dark accent against the light main content
   (the "admin app shell" pattern: Stripe / Linear / Vercel dashboards).
   Values mirror the dark-theme primitives from tokens.css so dark-mode
   users see a consistent dark UI; light-mode users get the contrast.
   We scope the dark tokens to .app-sidebar specifically — they do NOT
   leak into the rest of the page. Operator request 2026-05-23 (card #80). */
.app-sidebar {
  /* colour-literal: the sidebar is dark in BOTH themes (card #80), so the theme tokens are the wrong palette here — these mirror the dark primitives in tokens.css. */
  background-color: oklch(18% 0.012 var(--brand-hue));     /* dark paper-cream */
  color: oklch(94% 0.005 var(--brand-hue));                /* light graphite */
  border-right: 1px solid oklch(28% 0.012 var(--brand-hue));
}

/* Sidebar menu rules — scoped to .app-sidebar so the menu component on
   other surfaces (admin pages, etc.) still inherits the light token palette. */
.app-sidebar .menu-label {
  /* colour-literal: the sidebar is dark in BOTH themes (card #80), so the theme tokens are the wrong palette here — these mirror the dark primitives in tokens.css. */
  color: oklch(76% 0.008 var(--brand-hue));   /* dimmed light-graphite */
  font-family: var(--font-sans);
  font-size: var(--type-caption-size);
  font-weight: 600;
  /* Bulma sets text-transform: uppercase on .menu-label, so this has to say
     `none` explicitly: simply dropping our declaration hands the caps back to
     Bulma. Letter-spaced caps are the visual idiom of enterprise GRC chrome and
     date the interface immediately (DESIGN.md §4). The labels are already
     sentence case in partials/sidebar.html.twig. */
  text-transform: none;
  letter-spacing: 0.01em;
}

/* Bulma 1.x composes this background from --bulma-menu-item-background-l, which
   defaults to --bulma-scheme-main-l -- Bulma's PAGE background lightness, which
   its own prefers-color-scheme support flips. In the light theme that resolves
   to white, so Bulma painted a white pill inside this deliberately-always-dark
   sidebar, underneath our light text: 1.19:1 across all 20 links on every page.
   Dark mode hid it because Bulma's value happened to match our surface.

   `transparent`, not a pinned colour: the sidebar's own surface shows through,
   so this stays correct if that surface is ever retuned, and it does not depend
   on Bulma's internal HSL component variables keeping their names. The :hover
   and .is-active rules below set their own backgrounds and are unaffected.
   Card #684. */
.app-sidebar .menu-list a {
  /* colour-literal: the sidebar is dark in BOTH themes (card #80), so the theme tokens are the wrong palette here — these mirror the dark primitives in tokens.css. */
  background-color: transparent;
  color: oklch(94% 0.005 var(--brand-hue));   /* light text */
  border-radius: var(--radius-sm);
  padding: var(--space-2xs) var(--space-sm);
  transition: background-color 0.15s var(--ease-out-quart),
              color 0.15s var(--ease-out-quart);
}

.app-sidebar .menu-list a:hover {
  /* colour-literal: the sidebar is dark in BOTH themes (card #80), so the theme tokens are the wrong palette here — these mirror the dark primitives in tokens.css. */
  background-color: oklch(28% 0.06 245);      /* dark sky-action-soft */
  color: oklch(82% 0.16 245);                  /* light sky-action */
}

.app-sidebar .menu-list a.is-active {
  /* colour-literal: the sidebar is dark in BOTH themes (card #80), so the theme tokens are the wrong palette here — these mirror the dark primitives in tokens.css. */
  background-color: oklch(70% 0.16 245);      /* dark sky-action */
  color: oklch(15% 0.012 var(--brand-hue));    /* graphite-on-action */
}

.app-sidebar .menu-list a .fas,
.app-sidebar .menu-list a .fa {
  color: inherit;
  opacity: 0.85;
}

/* ─── Cards (Bulma .box) ─────────────────────────────────────────────────── */

/* Bulma .box defaults to white with a heavy shadow — replace with hairline
   border on the elevated surface for the cpeio "flat-by-default" feel. */
.box {
  background-color: var(--color-surface-elevated);
  border: var(--border-hairline);
  border-radius: var(--radius-md);
  box-shadow: none;
  color: var(--color-text);
}

/* ─── Tables ─────────────────────────────────────────────────────────────── */

.table {
  background-color: transparent;
  color: var(--color-text);
  font-family: var(--font-sans);
}

.table th {
  color: var(--color-text-soft);
  font-size: var(--type-label-size);
  font-weight: 600;
  border-bottom: var(--border-hairline-strong);
}

.table td {
  border-bottom: var(--border-hairline);
}

.table.is-hoverable tbody tr:hover {
  /* Neutral, not the accent. Row hover is not a selection, and tinting every
     hovered row blue spends the one accent colour on a pointer position.
     DESIGN.md One Voice Rule. */
  background-color: var(--color-surface-band);
}

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

/* ─── Hero titles + subtitles ────────────────────────────────────────────── */

.title {
  color: var(--color-text);
  font-family: var(--font-sans);
}

.subtitle {
  color: var(--color-text-soft);
}

/* ─── Flash messages ─────────────────────────────────────────────────────── */

.flash-message {
  transition: opacity 0.3s var(--ease-out-quart);
}

.flash-message.is-fading {
  opacity: 0;
}

/* ─── Status badges — domain-specific, used by Test / Control / RCM slices
   when they ship. Class names follow the cpeio convention (.tag.is-status-X)
   and consume token surfaces so they harmonize with the rest of the UI.
   Today (2026-05-23) no template renders these yet; they're prebuilt.
   ───────────────────────────────────────────────────────────────────────── */

/* Control lifecycle */
.tag.is-status-draft {
  background-color: var(--color-surface-band);
  color: var(--color-text-soft);
  border: var(--border-hairline);
}

.tag.is-status-active {
  background-color: var(--color-success-soft);
  color: var(--color-success-deep);
}

.tag.is-status-retired {
  background-color: var(--color-surface-band);
  color: var(--color-text-soft);
  border: var(--border-hairline);
  text-decoration: line-through;
}

/* Test lifecycle */
.tag.is-status-planned {
  background-color: var(--color-sky-action-soft);
  color: var(--color-sky-action-deep);
}

.tag.is-status-in-progress {
  background-color: var(--color-warning-soft);
  color: var(--color-warning-deep);
}

.tag.is-status-submitted {
  background-color: var(--color-sky-action-soft);
  color: var(--color-sky-action-deep);
}

.tag.is-status-in-review {
  background-color: var(--color-warning-soft);
  color: var(--color-warning-deep);
}

.tag.is-status-concluded {
  background-color: var(--color-success-soft);
  color: var(--color-success-deep);
}

/* Issue lifecycle */
.tag.is-status-open {
  background-color: var(--color-warning-soft);
  color: var(--color-warning-deep);
}

.tag.is-status-in-remediation {
  background-color: var(--color-sky-action-soft);
  color: var(--color-sky-action-deep);
}

.tag.is-status-ready-for-retest {
  background-color: var(--color-sky-action-soft);
  color: var(--color-sky-action-deep);
  font-weight: 600;
}

.tag.is-status-closed {
  background-color: var(--color-success-soft);
  color: var(--color-success-deep);
}

/* Issue severity */
.tag.is-severity-low {
  background-color: var(--color-success-soft);
  color: var(--color-success-deep);
}

.tag.is-severity-medium {
  background-color: var(--color-warning-soft);
  color: var(--color-warning-deep);
}

.tag.is-severity-high {
  background-color: var(--color-danger-soft);
  color: var(--color-danger-deep);
}

.tag.is-severity-critical {
  background-color: var(--color-danger-soft);
  color: var(--color-danger-deep);
  border: 1px solid var(--color-danger-deep);
  font-weight: 700;
}

/* Risk levels (formula from card #77) */
.tag.is-risk-low {
  background-color: var(--color-success-soft);
  color: var(--color-success-deep);
}

.tag.is-risk-medium {
  background-color: var(--color-warning-soft);
  color: var(--color-warning-deep);
}

.tag.is-risk-high {
  background-color: var(--color-danger-soft);
  color: var(--color-danger-deep);
}

.tag.is-risk-very-high {
  background-color: var(--color-danger-soft);
  color: var(--color-danger-deep);
  border: 1px solid var(--color-danger-deep);
  font-weight: 600;
}

/* RCM effectiveness */
.tag.is-effective {
  background-color: var(--color-success-soft);
  color: var(--color-success-deep);
}

.tag.is-partially-effective {
  background-color: var(--color-warning-soft);
  color: var(--color-warning-deep);
}

.tag.is-ineffective {
  background-color: var(--color-danger-soft);
  color: var(--color-danger-deep);
}

.tag.is-not-tested {
  background-color: var(--color-surface-band);
  color: var(--color-text-soft);
  border: var(--border-hairline);
}

/* ─── Utility classes — replaces inline style="" attributes ──────────────
   Card #131 (slice 49): every inline style attribute in src/Views/ was
   eliminated to let CSP drop 'unsafe-inline' from style-src. Each utility
   below corresponds to one or more previous inline patterns.
   ─────────────────────────────────────────────────────────────────────── */

/* Inline form layout (used for delete/archive/transition buttons that sit
   inline next to other inline elements). Replaces style="display: inline;"
   on <form> wrappers across show templates. Bulma's .is-inline-block is
   close but different; .is-inline-form matches the original behavior. */
.is-inline-form { display: inline; }

/* Inline-flex variant for transition pickers (tests/show:80) — wraps a
   select + button with horizontal gap. */
.is-inline-flex-form {
  display: inline-flex;
  gap: 0.5rem;
  align-items: center;
}

/* User-selectable secrets (API key plaintext, etc.) — easy triple-click to
   copy. Pairs with the data-copy-from button. */
.is-selectable-all {
  user-select: all;
  word-break: break-all;
}

/* Wrap-anywhere for long hex strings (checksums, hashes). */
.is-wrappable { word-break: break-all; }

/* Pointer cursor (e.g. clickable <summary> elements). */
.is-clickable { cursor: pointer; }

/* Flex item bottom-aligned (audit filter form's submit button row). */
.is-bottom-aligned { align-self: flex-end; }

/* Non-wrapping cells (audit-trail timestamp column). */
.is-nowrap { white-space: nowrap; }

/* Card #297 — label column in a key/value detail table (admin error detail).
 * Sized so the longest label ("Exception class") does not wrap. */
.is-label-column { width: 12rem; }

/* Card #290 — a 0–23 hour input. Bulma stretches number inputs to the control
 * width, which for two digits reads as a text field the operator is meant to
 * write prose into. */
.is-narrow-number { width: 7rem; }

/* Card #148 (slice 1 Jon replay) — utility classes lifted from inline
 * styles in projects/* views to satisfy CSP-no-inline-styles enforcement. */
.box.is-danger-bordered { border-color: var(--color-danger-deep); }
.has-min-width-100 { min-width: 100px; }
.has-min-width-110 { min-width: 110px; }
.has-min-width-120 { min-width: 120px; }
.has-min-width-140 { min-width: 140px; }
.has-max-width-80 { max-width: 80px; }

/* Percentage widths, 0 to 100. A proportional bar needs a computed width, and
   CspNoInlineStylesTest (card #131) forbids style="" in templates outright --
   correctly, since inline styles force CSP style-src 'unsafe-inline'. Generated
   classes are the CSP-safe way to express a computed proportion. */
.w-pct-0 { width: 0%; }
.w-pct-1 { width: 1%; }
.w-pct-2 { width: 2%; }
.w-pct-3 { width: 3%; }
.w-pct-4 { width: 4%; }
.w-pct-5 { width: 5%; }
.w-pct-6 { width: 6%; }
.w-pct-7 { width: 7%; }
.w-pct-8 { width: 8%; }
.w-pct-9 { width: 9%; }
.w-pct-10 { width: 10%; }
.w-pct-11 { width: 11%; }
.w-pct-12 { width: 12%; }
.w-pct-13 { width: 13%; }
.w-pct-14 { width: 14%; }
.w-pct-15 { width: 15%; }
.w-pct-16 { width: 16%; }
.w-pct-17 { width: 17%; }
.w-pct-18 { width: 18%; }
.w-pct-19 { width: 19%; }
.w-pct-20 { width: 20%; }
.w-pct-21 { width: 21%; }
.w-pct-22 { width: 22%; }
.w-pct-23 { width: 23%; }
.w-pct-24 { width: 24%; }
.w-pct-25 { width: 25%; }
.w-pct-26 { width: 26%; }
.w-pct-27 { width: 27%; }
.w-pct-28 { width: 28%; }
.w-pct-29 { width: 29%; }
.w-pct-30 { width: 30%; }
.w-pct-31 { width: 31%; }
.w-pct-32 { width: 32%; }
.w-pct-33 { width: 33%; }
.w-pct-34 { width: 34%; }
.w-pct-35 { width: 35%; }
.w-pct-36 { width: 36%; }
.w-pct-37 { width: 37%; }
.w-pct-38 { width: 38%; }
.w-pct-39 { width: 39%; }
.w-pct-40 { width: 40%; }
.w-pct-41 { width: 41%; }
.w-pct-42 { width: 42%; }
.w-pct-43 { width: 43%; }
.w-pct-44 { width: 44%; }
.w-pct-45 { width: 45%; }
.w-pct-46 { width: 46%; }
.w-pct-47 { width: 47%; }
.w-pct-48 { width: 48%; }
.w-pct-49 { width: 49%; }
.w-pct-50 { width: 50%; }
.w-pct-51 { width: 51%; }
.w-pct-52 { width: 52%; }
.w-pct-53 { width: 53%; }
.w-pct-54 { width: 54%; }
.w-pct-55 { width: 55%; }
.w-pct-56 { width: 56%; }
.w-pct-57 { width: 57%; }
.w-pct-58 { width: 58%; }
.w-pct-59 { width: 59%; }
.w-pct-60 { width: 60%; }
.w-pct-61 { width: 61%; }
.w-pct-62 { width: 62%; }
.w-pct-63 { width: 63%; }
.w-pct-64 { width: 64%; }
.w-pct-65 { width: 65%; }
.w-pct-66 { width: 66%; }
.w-pct-67 { width: 67%; }
.w-pct-68 { width: 68%; }
.w-pct-69 { width: 69%; }
.w-pct-70 { width: 70%; }
.w-pct-71 { width: 71%; }
.w-pct-72 { width: 72%; }
.w-pct-73 { width: 73%; }
.w-pct-74 { width: 74%; }
.w-pct-75 { width: 75%; }
.w-pct-76 { width: 76%; }
.w-pct-77 { width: 77%; }
.w-pct-78 { width: 78%; }
.w-pct-79 { width: 79%; }
.w-pct-80 { width: 80%; }
.w-pct-81 { width: 81%; }
.w-pct-82 { width: 82%; }
.w-pct-83 { width: 83%; }
.w-pct-84 { width: 84%; }
.w-pct-85 { width: 85%; }
.w-pct-86 { width: 86%; }
.w-pct-87 { width: 87%; }
.w-pct-88 { width: 88%; }
.w-pct-89 { width: 89%; }
.w-pct-90 { width: 90%; }
.w-pct-91 { width: 91%; }
.w-pct-92 { width: 92%; }
.w-pct-93 { width: 93%; }
.w-pct-94 { width: 94%; }
.w-pct-95 { width: 95%; }
.w-pct-96 { width: 96%; }
.w-pct-97 { width: 97%; }
.w-pct-98 { width: 98%; }
.w-pct-99 { width: 99%; }
.w-pct-100 { width: 100%; }
/* Card #156 (slice 5 Jon replay): filter-row checkbox label alignment */
.is-checkbox-filter { height: 2.5em; gap: 0.4rem; }
.has-preserved-whitespace { white-space: pre-wrap; }
.has-width-40pct { width: 40%; }
.has-list-decimal { list-style: decimal; }

/* Card #158 (slice 6 Jon replay) — Dashboard pipeline + activity feed */
.box.is-warning-bordered { border-color: var(--color-warning-deep); }
.dash-severity-bar--critical::-webkit-progress-value { background-color: var(--color-danger-deep); }
.dash-severity-bar--critical::-moz-progress-bar      { background-color: var(--color-danger-deep); }
.dash-severity-bar--high::-webkit-progress-value     { background-color: var(--color-danger-deep); opacity: 0.65; }
.dash-severity-bar--high::-moz-progress-bar          { background-color: var(--color-danger-deep); opacity: 0.65; }
.dash-severity-bar--medium::-webkit-progress-value   { background-color: var(--color-warning-deep); }
.dash-severity-bar--medium::-moz-progress-bar        { background-color: var(--color-warning-deep); }
.dash-severity-bar--low::-webkit-progress-value      { background-color: var(--color-success-deep); }
.dash-severity-bar--low::-moz-progress-bar           { background-color: var(--color-success-deep); }

.activity-feed { display: flex; flex-direction: column; gap: var(--space-2xs); }
.activity-item { display: flex; align-items: baseline; gap: var(--space-xs); padding: var(--space-2xs) 0; border-bottom: var(--border-hairline); font-size: var(--type-caption-size); }
.activity-item:last-child { border-bottom: none; }
.activity-icon { flex-shrink: 0; width: 1.25rem; text-align: center; }
.activity-body { flex: 1; min-width: 0; }
.activity-time { flex-shrink: 0; white-space: nowrap; }

/* ═══ Dashboard, rebuilt to DESIGN.md ═════════════════════════════════════ */
/* What this replaces, and why:
   - six floating cards of unequal height that did not fill their row
   - alert banners nested INSIDE the columns grid, which is what made the
     layout ragged and left the dead space beside them
   - a stepped-tile pipeline that read as a wizard rather than a distribution
   - tables wrapped in card chrome
   - saturated status pills competing with the data they label
   DESIGN.md §6. */

/* ── Page header ──────────────────────────────────────────────────────── */
/* The one-jump rule: a single decisive step from page title to everything
   else, rather than six evenly-spaced ones (DESIGN.md §4). */
.page-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-md);
  padding-bottom: var(--space-sm);
  margin-bottom: var(--space-lg);
  border-bottom: var(--border-hairline);
}
.page-head__title {
  font-size: 2rem;
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.018em;
  color: var(--color-text);
  margin: 0;
}
.page-head__count {
  font-family: var(--font-mono);
  font-feature-settings: "tnum" 1;
  color: var(--color-text-soft);
  font-weight: 400;
}
.page-head__sub {
  font-size: var(--type-caption-size);
  color: var(--color-text-soft);
  margin-top: var(--space-3xs);
}
.page-head__actions { display: flex; gap: var(--space-2xs); flex-shrink: 0; }

/* ── Metric strip ─────────────────────────────────────────────────────── */
/* One ribbon, hairline-separated, cells on a shared baseline. It fills its row
   or it does not exist. Numbers are mono with tabular figures so a value change
   cannot reflow the row. */
.metric-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  border-top: var(--border-hairline);
  border-bottom: var(--border-hairline);
  margin-bottom: var(--space-xl);
}
.metric {
  padding: var(--space-sm) var(--space-md) var(--space-sm) 0;
  border-right: var(--border-hairline);
  min-width: 0;
}
.metric:last-child { border-right: none; }
.metric__label {
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  color: var(--color-text-soft);
  margin-bottom: var(--space-3xs);
}
.metric__value {
  font-family: var(--font-mono);
  font-feature-settings: "tnum" 1;
  font-size: 1.625rem;
  font-weight: 500;
  line-height: 1.1;
  color: var(--color-text);
  letter-spacing: -0.01em;
}
.metric__value a { color: inherit; }
.metric__value a:hover { color: var(--color-sky-action-deep); }
/* Salience is earned: a metric turns red only when it represents work that is
   late, never merely because it is non-zero. */
.metric__value.is-alarm { color: var(--color-danger-deep); }
.metric__note {
  font-size: var(--type-caption-size);
  color: var(--color-text-soft);
  margin-top: var(--space-3xs);
}
.metric__note a { color: var(--color-sky-action-deep); }

/* ── Section heading ──────────────────────────────────────────────────── */
/* Sections are separated by a rule and a heading, not by nesting them in a box. */
.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-sm);
  margin-bottom: var(--space-xs);
}
.section-head__title {
  font-size: var(--type-title-size);
  font-weight: var(--type-title-weight);
  line-height: var(--type-title-line);
  color: var(--color-text);
  margin: 0;
}
.section-head__link { font-size: var(--type-caption-size); }
.dash-section { margin-bottom: var(--space-xl); }

/* ── Data table ───────────────────────────────────────────────────────── */
/* The table is the content. No wrapper whose only job is to draw a rectangle
   around it; no zebra; row separators only. */
.data-table { width: 100%; border-collapse: collapse; }
.data-table th {
  text-align: left;
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  color: var(--color-text-soft);
  padding: var(--space-2xs) var(--space-sm) var(--space-2xs) 0;
  border-bottom: var(--border-hairline);
  white-space: nowrap;
}
.data-table td {
  padding: var(--space-xs) var(--space-sm) var(--space-xs) 0;
  border-bottom: var(--border-hairline);
  vertical-align: top;
  font-size: var(--type-body-size);
}
.data-table tr:last-child td { border-bottom: none; }
.data-table tbody tr:hover { background-color: var(--color-surface-band); }
.data-table__id {
  font-family: var(--font-mono);
  font-feature-settings: "tnum" 1;
  font-size: var(--type-mono-size);
  white-space: nowrap;
}
.data-table__num {
  font-family: var(--font-mono);
  font-feature-settings: "tnum" 1;
  text-align: right;
  white-space: nowrap;
}
.data-table__muted { color: var(--color-text-soft); }

/* ── Status as a dot, not a filled pill ───────────────────────────────── */
/* A filled saturated pill on every row makes the status column the loudest
   thing on screen, which inverts the hierarchy: the data matters more than its
   label. Colour still pairs with text, so this is not colour-only signalling. */
.status {
  display: inline-flex;
  align-items: baseline;
  gap: 0.4em;
  white-space: nowrap;
  font-size: var(--type-body-size);
  color: var(--color-text);
}
.status::before {
  content: "";
  width: 0.5em;
  height: 0.5em;
  border-radius: 50%;
  background-color: currentColor;
  flex-shrink: 0;
  transform: translateY(-0.05em);
}
.status--neutral::before { color: var(--color-text-soft); }
.status--active::before  { color: var(--color-sky-action); }
.status--warn::before    { color: var(--color-warning-deep); }
.status--danger::before  { color: var(--color-danger-deep); }
.status--done::before    { color: var(--color-success-deep); }

/* ── Distribution bar ─────────────────────────────────────────────────── */
/* Replaces the stepped tiles: one proportional bar reads as a distribution,
   which is what the data is, and takes a fifth of the vertical space. */
.dist { display: flex; height: 0.5rem; border-radius: var(--radius-pill); overflow: hidden; background-color: var(--color-surface-band); }
.dist__seg { height: 100%; }
.dist__seg--planned  { background-color: var(--color-border-control); }
.dist__seg--progress { background-color: var(--color-sky-action); }
.dist__seg--pending  { background-color: var(--color-warning-deep); }
.dist__seg--review   { background-color: var(--color-sky-action-deep); }
.dist__seg--done     { background-color: var(--color-success-deep); }

.dist-key { display: flex; flex-wrap: wrap; gap: var(--space-md); margin-top: var(--space-xs); }
.dist-key__item { display: flex; align-items: baseline; gap: 0.4em; font-size: var(--type-caption-size); color: var(--color-text-soft); }
.dist-key__swatch { width: 0.5rem; height: 0.5rem; border-radius: 50%; flex-shrink: 0; }
.dist-key__num { font-family: var(--font-mono); font-feature-settings: "tnum" 1; color: var(--color-text); font-weight: 500; }

/* ── Meter (severity, progress) ───────────────────────────────────────── */
.meter { height: 0.375rem; border-radius: var(--radius-pill); background-color: var(--color-surface-band); overflow: hidden; }
.meter__fill { height: 100%; border-radius: var(--radius-pill); background-color: var(--color-sky-action); }
.meter__fill--critical { background-color: var(--color-danger-deep); }
.meter__fill--high     { background-color: var(--color-danger-deep); opacity: 0.6; }
.meter__fill--medium   { background-color: var(--color-warning-deep); }
.meter__fill--low      { background-color: var(--color-success-deep); }
.meter--inline { width: 4.5rem; display: inline-block; vertical-align: middle; }

.severity-row { display: grid; grid-template-columns: 1fr auto; gap: var(--space-2xs) var(--space-sm); align-items: baseline; margin-bottom: var(--space-xs); }
.severity-row__bar { grid-column: 1 / -1; }

/* ── Alerts ───────────────────────────────────────────────────────────── */
/* The Earned Salience Rule: at most one loud thing per surface. The first alert
   is stated plainly; subsequent ones are quiet rows in the same stack, so a
   second warning cannot compete with the first for the same attention. */
.alert-stack { margin-bottom: var(--space-lg); border: var(--border-hairline); border-radius: var(--radius-md); overflow: hidden; }
.alert {
  display: flex;
  gap: var(--space-xs);
  align-items: baseline;
  padding: var(--space-xs) var(--space-sm);
  font-size: var(--type-body-size);
  border-bottom: var(--border-hairline);
  background-color: var(--color-surface-elevated);
  color: var(--color-text);
}
.alert:last-child { border-bottom: none; }
.alert__mark { flex-shrink: 0; font-size: 0.8em; transform: translateY(-0.1em); }
.alert__body { flex: 1; min-width: 0; }
.alert__body a { font-weight: 500; white-space: nowrap; }
.alert--danger  { background-color: var(--color-danger-soft);  color: var(--color-danger-deep); }
.alert--warning { background-color: var(--color-warning-soft); color: var(--color-warning-deep); }
.alert--danger a, .alert--warning a { color: inherit; text-decoration: underline; }

/* ── Quick actions as links, not five stacked buttons ─────────────────── */
.action-list { display: flex; flex-direction: column; }
.action-list__item {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  padding: var(--space-2xs) 0;
  border-bottom: var(--border-hairline);
  font-size: var(--type-body-size);
  color: var(--color-text);
}
.action-list__item:last-child { border-bottom: none; }
.action-list__item:hover { color: var(--color-sky-action-deep); }
.action-list__item .icon { color: var(--color-text-soft); width: 1.1rem; }
.action-list__item:hover .icon { color: inherit; }

/* ── Activity feed ────────────────────────────────────────────────────── */
.activity-item { align-items: baseline; }
.activity-time { flex-shrink: 0; font-family: var(--font-mono); font-feature-settings: "tnum" 1; }

/* ── Panel: used only where a surface is genuinely separable ──────────── */
.panel-block-quiet {
  border: var(--border-hairline);
  border-radius: var(--radius-md);
  padding: var(--space-sm) var(--space-md);
  background-color: var(--color-surface-elevated);
}

@media (max-width: 768px) {
  .page-head { flex-direction: column; }
  .metric { border-right: none; border-bottom: var(--border-hairline); }
  .metric:last-child { border-bottom: none; }
}

/* ── Flash message ────────────────────────────────────────────────────── */
/* A confirmation is not an alarm. It was rendering as a full saturated block
   above the page title, competing with the heading for first attention.
   One compact line, stated and dismissible. DESIGN.md Earned Salience Rule. */
.flash-message {
  display: flex;
  align-items: baseline;
  gap: var(--space-xs);
  padding: var(--space-2xs) var(--space-sm);
  margin-bottom: var(--space-md);
  font-size: var(--type-body-size);
  border-radius: var(--radius-md);
}
.flash-message .delete { margin-left: auto; flex-shrink: 0; }

/* ── Inline progress cell ─────────────────────────────────────────────── */
/* Bar and fraction on one baseline; they were stacking and making every row
   in Project health twice as tall as it needed to be. */
.progress-cell { display: flex; align-items: center; gap: var(--space-2xs); white-space: nowrap; }
.progress-cell progress { flex: 0 0 4.5rem; margin: 0; }

/* ── Page headers, applied globally ───────────────────────────────────── */
/* The dashboard was rebuilt with an explicit .page-head. Every other page still
   uses the Bulma `.level` + `.title.is-3` shape, and editing 58 templates would
   collide with concurrent work for no additional benefit. Styling the existing
   shape gives every page the same hierarchy for free.
   DESIGN.md §4, the one-jump rule. */

h1.title.is-3 {
  font-size: 2rem;
  font-weight: 600;
  letter-spacing: -0.018em;
  line-height: 1.15;
  margin-bottom: var(--space-3xs);
}

/* A page title's icon is decoration. Keep it, since removing it means touching
   every template, but stop it competing with the words. */
h1.title.is-3 > .fas,
h1.title.is-3 > .fa,
h1.title.is-3 > .fa-solid {
  font-size: 0.62em;
  color: var(--color-text-soft);
  margin-right: 0.35rem;
  vertical-align: baseline;
}

h1.title.is-3 + .subtitle {
  font-size: var(--type-caption-size);
  color: var(--color-text-soft);
  margin-bottom: 0;
}

/* The rule under a page header, without a template change. :has() is supported
   by every browser this ships to; the header simply reads unseparated where it
   is not, which degrades cleanly. */
.level:has(h1.title.is-3) {
  padding-bottom: var(--space-sm);
  margin-bottom: var(--space-lg);
  border-bottom: var(--border-hairline);
}

/* Section headings inside panels: one step down, not another page title. */
.box > .level:first-child .title.is-5,
.box .title.is-5,
.box .title.is-6 {
  font-size: var(--type-title-size);
  font-weight: var(--type-title-weight);
  letter-spacing: var(--type-title-track);
}

/* ── Panel chrome, lightened ──────────────────────────────────────────── */
/* DESIGN.md §6 says a data-dense list is not wrapped in a card. 58 templates do
   exactly that, and converting them is a separate slice. Reducing the chrome
   gets most of the benefit now: the border stays as a boundary, the heavy
   interior padding that made every table feel boxed does not. */
.box {
  padding: var(--space-md);
}
.box:has(> .table-container),
.box:has(> table.table) {
  padding: var(--space-sm) var(--space-md);
}

/* ── Status and severity as dots ──────────────────────────────────────── */
/* DESIGN.md §6: a dot plus text, not a filled pill. A saturated chip on every
   row of every list makes the status column the loudest thing on screen, which
   inverts the hierarchy -- the data matters more than its label. The colour
   still pairs with the word, so this remains compliant with the no-colour-only
   -signalling rule, and it reads at a glance the same way.
   Applied to the existing .tag.is-status-* / .is-severity-* classes so every
   list page inherits it without a template change.
   The filled tonal chip is kept for genuine exceptions: see .tag.is-exception
   and the OVERDUE marker, where the row itself is the alarm. */

.tag.is-status-draft,
.tag.is-status-active,
.tag.is-status-retired,
.tag.is-status-planned,
.tag.is-status-in-progress,
.tag.is-status-submitted,
.tag.is-status-in-review,
.tag.is-status-concluded,
.tag.is-status-open,
.tag.is-status-in-remediation,
.tag.is-status-ready-for-retest,
.tag.is-status-closed,
.tag.is-severity-low,
.tag.is-severity-medium,
.tag.is-severity-high,
.tag.is-severity-critical {
  background-color: transparent;
  border: none;
  padding-left: 0;
  padding-right: var(--space-2xs);
  color: var(--color-text);
  font-weight: 400;
  display: inline-flex;
  align-items: baseline;
  gap: 0.4em;
}

.tag.is-status-draft::before,
.tag.is-status-active::before,
.tag.is-status-retired::before,
.tag.is-status-planned::before,
.tag.is-status-in-progress::before,
.tag.is-status-submitted::before,
.tag.is-status-in-review::before,
.tag.is-status-concluded::before,
.tag.is-status-open::before,
.tag.is-status-in-remediation::before,
.tag.is-status-ready-for-retest::before,
.tag.is-status-closed::before,
.tag.is-severity-low::before,
.tag.is-severity-medium::before,
.tag.is-severity-high::before,
.tag.is-severity-critical::before {
  content: "";
  width: 0.5em;
  height: 0.5em;
  border-radius: 50%;
  background-color: currentColor;
  flex-shrink: 0;
  transform: translateY(-0.05em);
}

/* The dot carries the colour; the label stays in reading ink. */
.tag.is-status-draft::before,
.tag.is-status-planned::before        { color: var(--color-text-soft); }
.tag.is-status-retired::before,
.tag.is-status-closed::before,
.tag.is-status-concluded::before,
.tag.is-status-active::before,
.tag.is-severity-low::before          { color: var(--color-success-deep); }
.tag.is-status-in-progress::before,
.tag.is-status-in-review::before,
.tag.is-status-ready-for-retest::before { color: var(--color-sky-action); }
.tag.is-status-submitted::before,
.tag.is-status-in-remediation::before,
.tag.is-severity-medium::before       { color: var(--color-warning-deep); }
.tag.is-status-open::before,
.tag.is-severity-high::before,
.tag.is-severity-critical::before     { color: var(--color-danger-deep); }

/* Critical is the one severity that keeps weight, because it is the one the
   eye should find first in a long list. */
.tag.is-severity-critical { font-weight: 500; color: var(--color-danger-deep); }

/* ═══ App shell: sticky header, collapsible sidebar ═══════════════════════ */
/* Cards #693 and #692. Both change the same shell, so they share a section. */

/* ── Sticky header ────────────────────────────────────────────────────── */
/* A header is orientation, not decoration: it says where you are and who you
   are signed in as. On /admin/audit or a full /tests list it used to scroll
   away entirely, taking the account menu and log-out with it.
   Needs an opaque background and a stacking context of its own, or page
   content shows through it. */
.navbar {
  position: sticky;
  top: 0;
  z-index: 30;
  background-color: var(--color-surface-band);
  border-bottom: var(--border-hairline);
  /* The token drives the height rather than describing it. The sidebar pins at
     exactly this offset, so a token that merely guessed the rendered height
     would tuck the rail under the header by the difference — which is what the
     inherited 52px literal was doing. */
  min-height: var(--nav-height);
}

/* A pinned header above a sidebar that scrolls away is a half-measure, so the
   rail pins too, directly beneath it, and scrolls internally when the nav is
   taller than the viewport. */
.app-shell-columns { min-height: calc(100vh - var(--nav-height)); }

.app-sidebar {
  position: sticky;
  top: var(--nav-height);
  align-self: flex-start;
  height: calc(100vh - var(--nav-height));
  overflow-y: auto;
  overscroll-behavior: contain;
  z-index: 20;
}

/* A sticky header covers the top of the viewport, so anything scrolled to the
   top — an in-page anchor, or an element receiving keyboard focus — can land
   underneath it. Reserve the header's height plus a little breathing room. */
:target,
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
  scroll-margin-top: calc(var(--nav-height) + var(--space-sm));
}

/* ── Collapsible sidebar ──────────────────────────────────────────────── */
/* State lives on the root element, stamped before first paint in
   base.html.twig, so a collapsed rail does not flash open on navigation. */

.sidebar-toggle {
  /* colour-literal: the sidebar is dark in BOTH themes (card #80), so the theme tokens are the wrong palette here — these mirror the dark primitives in tokens.css. */
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  width: 100%;
  margin-bottom: var(--space-sm);
  padding: var(--space-2xs) var(--space-xs);
  background: transparent;
  border: none;
  border-radius: var(--radius-sm);
  color: oklch(76% 0.008 var(--brand-hue));
  font-family: var(--font-sans);
  font-size: var(--type-caption-size);
  cursor: pointer;
  transition: background-color 0.15s var(--ease-out-quart),
              color 0.15s var(--ease-out-quart);
}
.sidebar-toggle:hover { /* colour-literal: the sidebar is dark in BOTH themes (card #80), so the theme tokens are the wrong palette here — these mirror the dark primitives in tokens.css. */ background-color: oklch(28% 0.06 245); color: oklch(82% 0.16 245); }
.sidebar-toggle:focus-visible { outline: none; box-shadow: var(--focus-ring); }

.app-sidebar { transition: flex-basis 0.18s var(--ease-out-quart), width 0.18s var(--ease-out-quart); }

/* Rail mode. Bulma's `column is-2` sets the width, so both flex-basis and
   width have to be overridden for the column to actually narrow. */
:root[data-sidebar="rail"] .app-sidebar {
  flex: none;
  width: var(--rail-width);
  min-width: var(--rail-width);
  max-width: var(--rail-width);
  padding: 0;
}
:root[data-sidebar="rail"] .app-sidebar .menu { padding: var(--space-2xs) !important; }

/* The label is hidden VISUALLY, not removed: a screen reader still reads it, so
   every link keeps its accessible name in rail mode. Same technique as .sr-only. */
:root[data-sidebar="rail"] .nav-label,
:root[data-sidebar="rail"] .app-sidebar .menu-label {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

:root[data-sidebar="rail"] .app-sidebar .menu-list a,
:root[data-sidebar="rail"] .sidebar-toggle {
  justify-content: center;
  padding-left: 0;
  padding-right: 0;
}
:root[data-sidebar="rail"] .app-sidebar .menu-list a .fas { margin-right: 0 !important; }
:root[data-sidebar="rail"] .sidebar-toggle .fas { transform: rotate(180deg); }

/* Groups lose their headings in rail mode, so the separation has to come from
   somewhere: a hairline between lists keeps the grouping legible. */
:root[data-sidebar="rail"] .app-sidebar .menu-list + .menu-label + .menu-list,
:root[data-sidebar="rail"] .app-sidebar .menu-list ~ .menu-list {
  /* colour-literal: the sidebar is dark in BOTH themes (card #80), so the theme tokens are the wrong palette here — these mirror the dark primitives in tokens.css. */
  border-top: 1px solid oklch(28% 0.012 var(--brand-hue));
  padding-top: var(--space-2xs);
  margin-top: var(--space-2xs);
}

/* Rail-mode tooltips are the NATIVE title attribute, not a styled ::after.
   The sidebar needs `overflow-y: auto` so a nav taller than the viewport can
   still be reached, and that creates a clipping context an absolutely
   positioned tooltip cannot escape — it renders, and is invisible. A styled
   flyout would need JS to position a fixed element per item, which is a lot of
   machinery for a hover hint. The browser draws `title` above everything and
   never clips it. Card #692. */

@media (prefers-reduced-motion: reduce) {
  .app-sidebar,
  .sidebar-toggle { transition: none; }
}

/* ── Lifecycle strip ──────────────────────────────────────────────────── */
/* Card #703. The project Lifecycle used `.dash-pipeline`, and the dashboard
   rebuild deleted that block without noticing this second consumer, so the
   strip rendered as unstyled divs with bare arrow glyphs. Migrated to the
   current system rather than restored under the old name, so one deleted
   component cannot come back twice.

   Why this is a stepper when /dashboard's pipeline became a proportional bar:
   the dashboard showed a DISTRIBUTION (how many tests sit in each status), and
   a stepper misread that as a wizard the user was progressing through. This is
   the opposite — a genuine sequence with one current position — so steps are
   the honest affordance.

   The accent marks the current step only. DESIGN.md §3 permits the accent for
   current-state indication, and a step strip where several things are coloured
   stops saying which one you are on. */

.lifecycle {
  display: flex;
  list-style: none;
  margin: 0;
  padding: 0;
}
.lifecycle__step {
  flex: 1 1 0;
  min-width: 0;
  position: relative;
  padding-top: 1.35rem;
  text-align: center;
}

/* The connector is drawn by the steps themselves: a half-track to the left and
   another to the right, so the line always meets the marker and never overruns
   the ends of the strip. */
.lifecycle__step::before,
.lifecycle__step::after {
  content: "";
  position: absolute;
  top: 0.3rem;
  height: 2px;
  background-color: var(--color-border);
  width: 50%;
}
.lifecycle__step::before { left: 0; }
.lifecycle__step::after  { right: 0; }
.lifecycle__step:first-child::before { display: none; }
.lifecycle__step:last-child::after   { display: none; }

.lifecycle__mark {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 0.7rem;
  height: 0.7rem;
  border-radius: 50%;
  background-color: var(--color-surface);
  border: 2px solid var(--color-border-control);
  z-index: 1;
}
.lifecycle__label {
  display: block;
  font-size: var(--type-caption-size);
  color: var(--color-text-soft);
  overflow-wrap: anywhere;
}

/* Completed: the track behind it is filled, the marker is solid but quiet. */
.lifecycle__step.is-done::before,
.lifecycle__step.is-done::after,
.lifecycle__step.is-current::before {
  background-color: var(--color-text-soft);
}
.lifecycle__step.is-done .lifecycle__mark {
  background-color: var(--color-text-soft);
  border-color: var(--color-text-soft);
}

/* Current: the one accented thing in the strip, with a ring so it reads as a
   position rather than another completed step. */
.lifecycle__step.is-current .lifecycle__mark {
  background-color: var(--color-sky-action);
  border-color: var(--color-sky-action);
  box-shadow: 0 0 0 3px var(--color-sky-action-soft);
}
.lifecycle__step.is-current .lifecycle__label {
  color: var(--color-text);
  font-weight: 500;
}

/* ── Detail strip ─────────────────────────────────────────────────────── */
/* A row of labelled facts, same idiom as .metric-strip but for text rather
   than numerals. Replaces `.box` + Bulma `.columns` on /profile: a definition
   list is not card-worthy content (DESIGN.md §6), and the two-row columns
   layout went ragged because row one was three thirds and row two was a third
   plus two thirds, so nothing lined up between them. */
.detail-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  border-top: var(--border-hairline);
  border-bottom: var(--border-hairline);
  margin: 0 0 var(--space-md);
}
.detail {
  padding: var(--space-sm) var(--space-md) var(--space-sm) 0;
  border-right: var(--border-hairline);
  min-width: 0;
}
.detail:last-child { border-right: none; }
.detail__label {
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  color: var(--color-text-soft);
  margin-bottom: var(--space-3xs);
}
.detail__value { color: var(--color-text); overflow-wrap: anywhere; }
.detail__value--mono {
  font-family: var(--font-mono);
  font-feature-settings: "tnum" 1;
  font-size: var(--type-mono-size);
}

/* A fact that carries guidance gets its own line rather than a narrow column,
   so the guidance has room to be a sentence. */
.detail-note {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2xs) var(--space-md);
  padding-bottom: var(--space-sm);
  margin-bottom: var(--space-md);
  border-bottom: var(--border-hairline);
}
.detail-note__label {
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  color: var(--color-text-soft);
  flex: 0 0 11rem;
}
.detail-note__body { flex: 1 1 22rem; min-width: 0; }
.detail-note__hint {
  font-size: var(--type-caption-size);
  color: var(--color-text-soft);
  margin-top: var(--space-3xs);
}

@media (max-width: 768px) {
  .detail { border-right: none; border-bottom: var(--border-hairline); }
  .detail:last-child { border-bottom: none; }
  .detail-note__label { flex-basis: 100%; }
}

/* ── List pages: the table is the page ────────────────────────────────── */
/* Card #712. DESIGN.md §6. Seventeen list pages wrap their filter controls in
   card chrome and eleven stripe their rows; both are fixed here rather than
   across seventeen templates the backend sessions are also editing.

   The selector is semantic, not cosmetic. A GET form on a list page IS a
   filter; a POST form is an editing form that legitimately wants a panel. The
   repo splits exactly on that line — 17 GET forms carry .box, all filter bars,
   against 20 POST forms carrying .box, all create/edit — so this reaches every
   filter bar and cannot touch a form someone types into. */

form[method="get"].box {
  background-color: transparent;
  border: none;
  border-bottom: var(--border-hairline);
  border-radius: 0;
  box-shadow: none;
  padding: 0 0 var(--space-sm);
  margin-bottom: var(--space-md);
}

/* The controls sit closer together once the panel around them is gone; without
   this they keep the airy spacing that only made sense inside a card. */
form[method="get"].box .field.is-grouped.is-grouped-multiline { margin-bottom: 0; }
form[method="get"].box > .field:last-child,
form[method="get"].box > .columns:last-child { margin-bottom: 0; }

/* Zebra striping. DESIGN.md §6: hairline row separators only. Bulma's rule is
   `.table.is-striped tbody tr:not(.is-selected):nth-child(even)`, so this has
   to match that specificity to win, and must leave the hover state alone —
   hover is a pointer affordance, striping is decoration. */
.table.is-striped tbody tr:not(.is-selected):nth-child(even) {
  background-color: transparent;
}
.table.is-striped tbody tr:not(.is-selected):nth-child(even):hover {
  background-color: var(--color-surface-band);
}
