/* Fortinet GSI Showcase Portal
   System font stack (no external fonts; strict CSP). */

:root {
  --fg-red: #da291c;
  --fg-red-dark: #b11e14;
  --blue: #175cd3;
  --teal: #0a7d76;
  --amber: #b7791f;
  --ink: #16233d;
  --muted: #5b6b85;
  --line: #e5e9f0;
  --bg: #f4f6f9;
  --card: #ffffff;
  --head: #101b33;

  --radius: 12px;
  --shadow: 0 1px 2px rgba(16, 27, 51, .06), 0 1px 3px rgba(16, 27, 51, .05);
}

* { box-sizing: border-box; }

html, body { margin: 0; padding: 0; }

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
    Arial, "Apple Color Emoji", "Segoe UI Emoji", sans-serif;
  background: var(--bg);
  color: var(--ink);
  font-size: 15px;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
}

.wrap { max-width: 1440px; margin: 0 auto; padding: 24px 32px; }

/* ------------------------------------------------------------- masthead */
.masthead {
  background: linear-gradient(118deg, #0a0e1f 0%, #191233 55%, #331636 100%);
  color: #fff;
  border-bottom: 3px solid var(--fg-red);
}
.masthead-inner {
  max-width: 1440px; margin: 0 auto; padding: 30px 32px;
  display: flex; justify-content: space-between; align-items: flex-start; gap: 32px;
}
.brand { display: flex; align-items: flex-start; gap: 18px; }
.brand-logo { height: 34px; width: auto; margin-top: 4px; }
.eyebrow {
  margin: 0 0 2px; font-size: 12px; font-weight: 700; letter-spacing: .12em;
  text-transform: uppercase; color: var(--fg-red);
}
.brand-text h1 { margin: 0; font-size: 30px; font-weight: 800; letter-spacing: -.01em; }
.brand-sub { margin: 8px 0 0; max-width: 620px; color: #b9c1d4; font-size: 14px; }

/* ------------------------------------------------------------- admin bar */
.adminbar { background: #fff; border-bottom: 1px solid var(--line); }
.adminbar-inner { display: flex; flex-wrap: wrap; align-items: center; gap: 14px; padding-top: 12px; padding-bottom: 12px; }
.adminbar-tag {
  background: var(--fg-red); color: #fff; padding: 3px 10px; border-radius: 6px;
  font-size: 11px; font-weight: 800; letter-spacing: .06em;
}
.adminbar-user { color: var(--muted); font-size: 13px; }
.adminbar-spacer { flex: 1; }
.inline-form { display: inline; margin: 0; }

/* ------------------------------------------------------------- dashboard panels */
.panel-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px;
}
.panel {
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 22px 24px;
}
.panel-title { margin: 0 0 8px; font-size: 17px; font-weight: 800; }
.panel-body { margin: 0 0 14px; color: var(--muted); font-size: 14px; }
.panel-soon {
  display: inline-block; background: #eef1f4; color: #6a7688; border-radius: 999px;
  padding: 3px 12px; font-size: 11px; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase;
}

/* ------------------------------------------------------------- buttons */
.btn {
  padding: 8px 15px; border-radius: 8px; font-weight: 600; font-size: 13px; cursor: pointer;
  border: 1px solid var(--line); background: #fff; color: var(--ink); text-decoration: none;
  display: inline-flex; align-items: center; justify-content: center;
}
.btn:not([disabled]):hover { border-color: #c3ccdb; }
/* Operator tier (2026-08-18): a control the signed-in admin may not use renders
   `disabled` with a title= explanation rather than vanishing, so the row does not
   look broken next to a full admin's view. Without this rule it was pixel-identical
   to a live button -- .btn sets `cursor: pointer` unconditionally -- so the only
   signal was a tooltip nobody would hover for, and clicking did nothing visible.
   The hover rules are scoped `:not([disabled])` rather than overridden afterwards,
   because restoring each variant's own background/border on hover would mean three
   more rules that have to stay in step with the variants they mirror. */
.btn[disabled] { opacity: .45; cursor: not-allowed; }
.btn-primary { background: var(--fg-red); color: #fff; border-color: var(--fg-red); }
.btn-primary:not([disabled]):hover { background: var(--fg-red-dark); border-color: var(--fg-red-dark); }
.btn-ghost { background: #fff; }
.btn-block { width: 100%; padding: 11px; }

/* ------------------------------------------------------------- auth pages */
.auth-wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; }
.auth-card {
  background: #fff; border: 1px solid var(--line); border-radius: 16px; box-shadow: var(--shadow);
  padding: 34px 32px; width: 100%; max-width: 400px;
}
.auth-card-center { text-align: center; }
.auth-logo { height: 30px; width: auto; margin-bottom: 18px; }
.auth-title { margin: 0 0 4px; font-size: 21px; font-weight: 800; }
.auth-sub { margin: 0 0 20px; color: var(--muted); font-size: 14px; }
.auth-error {
  background: #fef3f2; border: 1px solid #fdd; color: #b42318; border-radius: 8px;
  padding: 10px 14px; font-size: 13px; margin-bottom: 16px;
}
.auth-error ul { margin: 0; padding-left: 18px; }
.auth-form { display: flex; flex-direction: column; gap: 14px; }
.field { display: flex; flex-direction: column; gap: 6px; }
.field-label { font-size: 13px; font-weight: 600; color: var(--ink); }
.field-input {
  border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; font: inherit;
  color: var(--ink); background: #fff;
}
.field-input:focus { outline: none; border-color: var(--fg-red); box-shadow: 0 0 0 3px rgba(218, 41, 28, .12); }
.field-help { margin: -4px 0 4px; font-size: 12px; color: var(--muted); }
.auth-note { margin: 16px 0 0; font-size: 12px; color: var(--muted); text-align: center; }
.error-code { font-size: 56px; font-weight: 800; margin: 6px 0; color: var(--fg-red); }

.hidden { display: none !important; }

/* --- progressive enhancement ------------------------------------------------
   Every admin action here is already a plain form POST, so nothing breaks when
   admin.js is blocked -- but its confirm() dialog does vanish, and the document
   delete is a hard delete with no in-app undo. So destructive routes require
   `confirm=1` server-side: admin.js ticks the box after its dialog and adds
   `js-on` to <body>, which hides the box; with no script the box is visible and
   must be ticked by hand. Default-visible on purpose -- if the stylesheet AND
   the script both fail, the user still gets a working, guarded control. */
body.js-on .no-js-only { display: none !important; }

/* --- confirmation page (2026-08-07) -----------------------------------------
   Stage 2 of every destructive action. It replaced `.confirm-tick`, a checkbox
   that was hidden under body.js-on and ticked by admin.js after a dialog -- see
   views_admin._unconfirmed for the two ways that left a button silently doing
   nothing. NOTHING here may be `.no-js-only`: this page IS the guard, so it has
   to render identically in every script state. */
.confirm-strip {
  background: #fff5f4; border: 1px solid #f3c3bd; border-left: 4px solid var(--fg-red);
  border-radius: var(--radius); padding: 16px 20px; margin-bottom: 22px;
}
.confirm-strip-title { margin: 0 0 6px; font-size: 16px; color: #b11e14; }
.confirm-strip-body { margin: 0 0 12px; font-size: 13.5px; max-width: 70ch; }
.confirm-strip-form { display: flex; flex-direction: column; gap: 10px; align-items: flex-start;
  margin-top: 14px; }
/* Full-size, not the muted 12px the old inline tick used: each of these agrees
   to lose something specific and unrecoverable. */
.confirm-strip .confirm-box { display: flex; align-items: flex-start; gap: 7px;
  font-size: 13.5px; color: var(--ink); max-width: 70ch; }
.confirm-strip .confirm-box input { margin: 3px 0 0; flex: 0 0 auto; }
.confirm-strip-actions { display: flex; align-items: center; gap: 10px; margin-top: 4px; }

/* ------------------------------------------------------------- bulk edit */
.bulkbar { display: flex; align-items: flex-end; flex-wrap: wrap; gap: 12px; }
.bulkbar-label { font-weight: 700; color: var(--muted); text-transform: uppercase;
  letter-spacing: .04em; font-size: 11px; padding-bottom: 8px; }
.bulkbar-field { display: flex; flex-direction: column; gap: 3px; }
/* Compound selector: a bare rule would fight `.field-input`'s own width at equal
   specificity depending on source order. */
.bulkbar .field-input { width: auto; min-width: 160px; }
.field-opt { font-weight: 400; font-size: 11.5px; color: var(--muted); }
.bulkpick { display: inline-flex; align-items: center; }
.bulkpick input { margin: 0; }
.col-pick { width: 1%; }
/* Live "N selected" count beside Preview (admin.js). Aligned to the button's
   baseline, since the bar is `align-items: flex-end`. */
.bulkbar-count { color: var(--muted); font-size: 13px; padding-bottom: 7px;
  font-variant-numeric: tabular-nums; }
/* The bulk form holds only hidden fields; its controls live in the bar and its
   tick-boxes in the table, wired by the HTML5 `form` attribute. */
.wl-bulkform { display: none; }
/* A secondary line under a preview row's main value. */
.wl-note { display: block; font-size: 11.5px; color: var(--muted); }
.nojs-note {
  background: #fffaeb; border: 1px solid #fedf89; color: #93370d;
  border-radius: var(--radius); padding: 10px 14px; margin: 0 0 16px;
  font-size: 13px; line-height: 1.5;
}

/* ------------------------------------------------------------- admin nav */
/* `flex-wrap` is load-bearing, not cosmetic: `.adminbar-inner` wraps, but this
   inner row did not, so the nav was a single unwrappable line that pushed the
   PAGE 12px wider than the viewport at 768 once a tenth link was added
   (2026-08-05c). Every admin page shares this bar, so one nav item overflowed all
   of them at once -- and it reads as a page-level layout bug rather than a nav
   one, which is why it is worth pinning here. */
.adminnav { display: flex; flex-wrap: wrap; gap: 4px; }
.adminnav-link {
  padding: 6px 12px; border-radius: 8px; font-size: 13px; font-weight: 600;
  color: var(--muted); text-decoration: none;
}
.adminnav-link:hover { background: #f1f3f7; color: var(--ink); }
.adminnav-link.active { background: var(--fg-red); color: #fff; }

/* ------------------------------------------------------------- flash banners */
.flash {
  border-radius: 8px; padding: 10px 14px; font-size: 13.5px; margin-bottom: 16px;
  border: 1px solid var(--line);
}
.flash-ok { background: #ecfdf3; border-color: #abefc6; color: #067647; }
.flash-error { background: #fef3f2; border-color: #fdd; color: #b42318; }

/* Admin "View as group" banner. Deliberately LOUD -- the whole value of the
   preview is knowing you are looking at somebody else's catalog rather than your
   own, so a subtle marker defeats the feature (see CLAUDE.md). It shipped with
   NO rule at all: the class came from the NSE/portfolio siblings, which style it,
   and this repo does not -- so it rendered as plain body text easily mistaken for
   a page subtitle. Amber rather than red: this is a heads-up, not an error. */
.preview-banner {
  margin: 0 0 18px; padding: 12px 16px; border-radius: 8px; font-size: 13.5px;
  background: #fffaeb; border: 1px solid #fedf89; border-left: 4px solid var(--amber);
  color: #7a4d05;
}
.preview-banner strong { color: #62330a; }
.preview-banner a { color: #7a4d05; font-weight: 700; }

/* Read-only summary of an allowlist row's current group scope. */
.scope-current { margin: 6px 0 0; font-size: 11.5px; color: var(--muted); }

/* ------------------------------------------------------------- dashboard stats */
.stat-line { margin: 0 0 16px; color: var(--muted); font-size: 14px; }
.stat-big { font-size: 26px; font-weight: 800; color: var(--ink); }
.panel-wide { margin-top: 20px; }

/* ------------------------------------------------------------- data tables */
.toolbar { display: flex; align-items: center; gap: 16px; margin-bottom: 18px; flex-wrap: wrap; }
.admin-hint { color: var(--muted); font-size: 13px; }
/* One-line "there is more to the right" hint above a horizontally-scrolling
   region. Default-visible and REMOVED by admin.js when the region does not
   actually overflow -- the same default-visible discipline as `.no-js-only`. */
.scroll-hint {
  margin: 0 0 6px; font-size: 12px; color: var(--muted);
  font-weight: 600; letter-spacing: .02em;
}
.table-scroll { overflow-x: auto; }
.dtable {
  width: 100%; border-collapse: collapse; background: var(--card);
  border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden;
}
.dtable th, .dtable td { padding: 10px 14px; text-align: left; vertical-align: top; border-bottom: 1px solid var(--line); }
.dtable th { background: #f7f8fb; font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--muted); }
.dtable tr:last-child td { border-bottom: none; }
.dtable td.num, .dtable th.num { text-align: right; white-space: nowrap; }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12.5px; }
.nowrap { white-space: nowrap; }
.muted-sm { color: var(--muted); font-size: 12.5px; }

/* Right-aligned "see more" link inside a panel heading. */
.panel-title-link { float: right; font-size: 12.5px; font-weight: 500; }

/* Pager -- shared by the view log + audit browser (50 rows/page). */
.pager { display: flex; align-items: center; gap: 12px; margin: 14px 0 4px; }
.pager-pos { color: var(--muted); font-size: 12.5px; }
.col-actions { width: 1%; white-space: nowrap; }
.action-row { display: flex; gap: 6px; flex-wrap: wrap; }

/* ------------------------------------------------------------- badges */
.badge {
  display: inline-block; padding: 2px 9px; border-radius: 999px; font-size: 11px;
  font-weight: 700; letter-spacing: .03em; background: #eef1f4; color: #5a6378;
}
.badge-pub { background: #ecfdf3; color: #067647; }
.badge-draft { background: #fff5e6; color: #b7791f; }

/* ------------------------------------------------------------- admin forms */
.formcard {
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 22px 24px; margin-bottom: 22px; max-width: 720px;
}
.stack-form { display: flex; flex-direction: column; gap: 16px; }
.stack-form textarea.field-input { resize: vertical; font: inherit; }
.opt { color: var(--muted); font-weight: 400; }
.form-actions { display: flex; gap: 10px; }
.inline-add { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.inline-add .grow { flex: 1; min-width: 220px; }
.field-input.compact { width: auto; }
.btn-sm { padding: 5px 11px; font-size: 12px; border-radius: 7px; }
.btn-danger { background: #fff; color: #b42318; border-color: #f3c0bb; }
.btn-danger:not([disabled]):hover { background: #fef3f2; border-color: #e79b93; }
code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12.5px; background: #f1f3f7; padding: 1px 5px; border-radius: 4px; }

/* ------------------------------------------------------------- viewer catalog */
.viewer-id { display: flex; align-items: center; gap: 14px; flex-shrink: 0; }
.viewer-email { color: #b9c1d4; font-size: 13px; }
.doc-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 18px; }
.doc-card {
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 20px 22px; display: flex; flex-direction: column; gap: 8px;
}
.doc-title { margin: 0; font-size: 17px; font-weight: 800; }
.doc-desc { margin: 0; color: var(--muted); font-size: 13.5px; flex: 1; }
.doc-open { margin-top: 6px; align-self: flex-start; }

/* ------------------------------------------------------------- TOTP enrollment */
.enroll-card { max-width: 640px; }
.enroll-grid { display: flex; gap: 24px; align-items: flex-start; flex-wrap: wrap; margin: 18px 0; }
.enroll-qr {
  background: #fff; border: 1px solid var(--line); border-radius: 10px; padding: 10px; line-height: 0;
}
.enroll-qr img { display: block; width: 200px; height: 200px; }
.enroll-key { flex: 1; min-width: 240px; }
.keybox {
  background: #f1f3f7; border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px;
  font-size: 15px; letter-spacing: .08em; word-break: break-all; margin: 4px 0 10px;
}
/* The share link is a readonly INPUT rather than a <p> so it can be selected and
   copied with no script at all. Element-scoped on purpose: `.keybox` is already
   styled, so no new class name enters the template-class sweep, and input.keybox
   (0,1,1) outranks .keybox (0,1,0) rather than depending on source order. */
/* font-FAMILY, never the `font` shorthand: `font: inherit` also resets
   font-size, and at this higher specificity it would silently beat `.keybox`'s
   own 15px one line above -- the equal-specificity cascade trap this repo has
   shipped once already, just with specificity doing the beating instead of
   source order. An input needs only the family, which the UA stylesheet
   otherwise sets for it. */
input.keybox { width: 100%; font-family: inherit; color: inherit; }

/* ------------------------------------------------------------- responsive */
@media (max-width: 900px) {
  .masthead-inner { flex-direction: column; gap: 20px; padding: 24px 18px; }
  .wrap { padding: 20px 18px; }
}

/* Best-knowledge disclaimer footer (see ../CLAUDE.md). */
.disclaimer {
  margin-top: 2rem;
  padding-top: 0.75rem;
  border-top: 1px solid #dfe2e8;
  color: #5a6378;
  font-size: 0.6875rem;
  line-height: 1.5;
  max-width: 70ch;
}
/* base.html puts the footer directly in <body>, which has no padding: inset it so it never
   touches the edge. Same rule as disclaimer.html_style(); :where() keeps it at zero specificity. */
:where(body > .disclaimer) {
  margin-left: 2rem;
  margin-right: 2rem;
  margin-bottom: 1.5rem;
}

/* =====================================================================
   PORTED 2026-07-29 from ../gsi-nse-enablement-portal/app/static/css/portal.css
   (the reference implementation). This portal had no report layer at all, so
   the whole reporting section AND the print stylesheet come across together --
   they are one unit: the charts are SVG precisely because the print block is
   what this report exists for, and splitting them is how the A4-landscape /
   overflow-visible / disclaimer-survives-print rules get lost.
   ===================================================================== */
/* ======================================================================
   Admin reporting tool (/admin/report)
   Built to be PRINTED: the export path is the browser's own
   Print -> Save as PDF, so every visual here has to survive that. Two
   consequences drive the rules below.
     1. Charts are SVG <rect>/<circle>, never a coloured <div>. A CSS
        background is dropped by default print settings; a real element is not.
     2. Anything the reader needs is also stated in text -- colour is a second
        channel, never the only one -- so a greyscale print stays complete.
   ====================================================================== */

/* `.noprint` is a marker class only -- the @media print block at the bottom of
   this file is what acts on it. Nothing to declare for screen. */

.report-builder {
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 20px 22px; margin-bottom: 26px;
}
.builder-grid {
  display: grid; gap: 20px 26px;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
}
.builder-group { border: none; margin: 0; padding: 0; min-width: 0; }
.builder-group legend {
  padding: 0 0 8px; font-size: 11px; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted);
}
.radio-row {
  display: flex; gap: 9px; align-items: flex-start; padding: 5px 0;
  font-size: 13.5px; cursor: pointer;
}
.radio-row input { margin-top: 3px; flex-shrink: 0; }
.radio-row strong { font-weight: 600; display: block; }
.radio-row em { display: block; font-style: normal; font-size: 12px; color: var(--muted); }
.builder-actions {
  display: flex; flex-wrap: wrap; gap: 12px; align-items: center;
  margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--line);
}
.builder-note { font-size: 12px; color: var(--muted); flex: 1; min-width: 240px; }

/* ------------------------------------------------------------ report sheet */
.report {
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 30px 32px 24px;
}
.report-head { display: flex; gap: 20px; align-items: flex-start; padding-bottom: 18px;
  border-bottom: 3px solid var(--fg-red); }
.report-logo { height: 30px; width: auto; flex-shrink: 0; }
.report-eyebrow { margin: 0 0 3px; font-size: 11px; font-weight: 700; letter-spacing: .12em;
  text-transform: uppercase; color: var(--fg-red); }
.report-title { margin: 0; font-size: 25px; font-weight: 800; letter-spacing: -.01em; }
.report-meta { margin: 7px 0 0; font-size: 12.5px; color: var(--muted); }
.report-classification {
  margin: 16px 0 0; padding: 9px 13px; border-left: 3px solid var(--fg-red);
  background: #fef3f2; color: #912018; font-size: 12.5px; font-weight: 600;
}

.report-section { margin-top: 34px; }
.report-h {
  margin: 0 0 6px; padding-bottom: 7px; border-bottom: 1px solid var(--line);
  font-size: 17px; font-weight: 800; letter-spacing: -.01em;
}
.report-h4 { margin: 22px 0 8px; font-size: 13.5px; font-weight: 700; }
.section-intro { margin: 8px 0 14px; font-size: 13px; color: var(--muted); max-width: 92ch; }
.report-foot {
  margin-top: 34px; padding-top: 12px; border-top: 1px solid var(--line);
  font-size: 11.5px; color: var(--muted);
}

/* ------------------------------------------------------------------- KPIs */
.kpi-grid {
  display: grid; gap: 12px; margin: 16px 0 6px;
  grid-template-columns: repeat(auto-fit, minmax(128px, 1fr));
}
.kpi-grid-sm { grid-template-columns: repeat(auto-fit, minmax(112px, 1fr)); }
.kpi {
  border: 1px solid var(--line); border-radius: 10px; padding: 12px 13px;
  border-top: 3px solid #c3ccdb; background: #fcfdfe;
}
.kpi-good { border-top-color: #067647; }
.kpi-bad { border-top-color: var(--fg-red); }
.kpi-warn { border-top-color: var(--amber); }
.kpi-num { margin: 0; font-size: 26px; font-weight: 800; line-height: 1.05; }
.kpi-label { margin: 5px 0 0; font-size: 10.5px; font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted); }

/* ----------------------------------------------------------------- panels */
.panel-row { display: grid; gap: 18px; margin-top: 18px;
  grid-template-columns: repeat(auto-fit, minmax(330px, 1fr)); }
.panel { border: 1px solid var(--line); border-radius: 10px; padding: 15px 16px; min-width: 0; }
.panel-h { margin: 0 0 12px; font-size: 13px; font-weight: 700; }
.panel-note { margin: 10px 0 0; font-size: 11.5px; color: var(--muted); }
.donut-row { display: flex; gap: 20px; align-items: center; flex-wrap: wrap; }
.donut-key, .mini-stats { list-style: none; margin: 0; padding: 0; font-size: 12.5px; }
.donut-key li { display: flex; align-items: center; gap: 8px; padding: 3px 0; }
.mini-stats { display: flex; flex-wrap: wrap; gap: 6px 20px; margin-top: 12px; color: var(--muted); }
.mini-stats strong { color: var(--ink); font-size: 15px; }

/* ----------------------------------------------------------------- charts */
.chart-bar, .chart-stack { display: block; }
.chart-cols { display: block; margin: 4px 0 2px; }
.bar-track { fill: #eef1f6; }
.bar-fill { fill: var(--blue); }
.bar-fill-warn { fill: var(--amber); }
.bar-fill-bad { fill: var(--fg-red); }
.col-fill { fill: var(--blue); }
.col-fill-target { fill: var(--teal); }
.col-fill-audit { fill: #7a50b0; }
.donut-track { stroke: #eef1f6; }
.donut-centre { font-size: 17px; font-weight: 800; fill: var(--ink); }

/* Status ramp -- one hue family per meaning, used by stack, donut and legend. */
.seg-certified { fill: #067647; stroke: #067647; }
.seg-progress  { fill: var(--blue); stroke: var(--blue); }
.seg-expired   { fill: var(--fg-red); stroke: var(--fg-red); }
.seg-none      { fill: #d7dde8; stroke: #d7dde8; }

.chart-legend { list-style: none; display: flex; flex-wrap: wrap; gap: 6px 20px;
  margin: 10px 0 0; padding: 0; font-size: 11.5px; color: var(--muted); }
.chart-legend li { display: flex; align-items: center; gap: 7px; }
.swatch { width: 11px; height: 11px; border-radius: 3px; flex-shrink: 0;
  background: #d7dde8; display: inline-block; }
.swatch.seg-certified { background: #067647; }
.swatch.seg-progress  { background: var(--blue); }
.swatch.seg-expired   { background: var(--fg-red); }
.swatch.seg-none      { background: #d7dde8; }

/* --------------------------------------------------------------- findings */
.finding-list { list-style: none; margin: 14px 0 0; padding: 0; }
.finding { border: 1px solid var(--line); border-left: 4px solid #c3ccdb;
  border-radius: 8px; padding: 12px 15px; margin-bottom: 10px; }
.finding.sev-high { border-left-color: var(--fg-red); background: #fffbfa; }
.finding.sev-medium { border-left-color: var(--amber); background: #fffcf5; }
.finding.sev-low { border-left-color: var(--blue); background: #fbfcff; }
.finding-head { margin: 0; font-size: 14px; font-weight: 700; display: flex;
  align-items: center; gap: 9px; flex-wrap: wrap; }
.finding-detail { margin: 5px 0 0; font-size: 12.5px; color: var(--muted); }
.finding-items { margin: 8px 0 0; padding-left: 18px; font-size: 12px; color: var(--ink); }
.finding-items li { padding: 1px 0; }
.sev-tag { font-size: 10px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
  padding: 2px 7px; border-radius: 4px; color: #fff; }
.sev-tag-high { background: var(--fg-red); }
.sev-tag-medium { background: var(--amber); }
.sev-tag-low { background: var(--blue); }

/* ------------------------------------------------------------------- gaps */
.gap-grid { display: grid; gap: 14px; margin-top: 14px;
  grid-template-columns: repeat(auto-fit, minmax(215px, 1fr)); }
.gap-card { border: 1px solid var(--line); border-radius: 10px; padding: 13px 15px; }
.gap-num { margin: 0; font-size: 24px; font-weight: 800; line-height: 1; }
.gap-label { margin: 4px 0 8px; font-size: 11px; font-weight: 600; letter-spacing: .05em;
  text-transform: uppercase; color: var(--muted); }
.gap-list { margin: 0; padding-left: 16px; font-size: 12px; }
.gap-list li { padding: 1px 0; }

/* ------------------------------------------------------- table extensions */
.dtable-compact th, .dtable-compact td { padding: 6px 9px; font-size: 12.5px;
  vertical-align: middle; }
.dtable-compact th { font-size: 10px; }
.dtable td.num, .dtable th.num { text-align: right; }
.dtable th.w-mix { width: 190px; }
.dtable th.w-bar { width: 146px; }
.muted-sm { color: var(--muted); font-size: 11.5px; }
.strong-good { color: #067647; font-weight: 700; }
.strong-bad { color: var(--fg-red); font-weight: 700; }
.row-muted td { background: #fafbfd; }
.badge-bad { background: #fef3f2; color: #b42318; }
/* Amber, between .badge (neutral) and .badge-bad (already broken): something
   still working that is about to stop. Same --amber the medium severity tags
   and .kpi-warn use, so "act soon" reads consistently across the console. */
.badge-warn { background: #fff5e6; color: var(--amber); }
/* Slate, deliberately NOT .badge-bad. A Secured document is in no catalog and is
   still serveable by share link -- a configuration an admin picks, not a broken
   one -- and it is the DEFAULT for every upload, so badging it red would paint
   the whole list as failing. Background and colour are set TOGETHER: a partial
   override inherits .badge's grey text, which is how .badge-warn spent weeks at
   1.23:1 in the sibling portal. Measured 8.1:1. */
.badge-secure { background: #eaf0f6; color: #33506e; }
.badge-cert { display: inline-block; padding: 1px 6px; margin-right: 3px; border-radius: 4px;
  background: #eef2f9; color: var(--ink); font-size: 10.5px; font-weight: 700;
  white-space: nowrap; }
.badge-expert { background: #efe9f6; color: #56348f; }
.badge-stage { background: #e1f3f4; color: #0a6e69; }
.flag { display: inline-block; padding: 1px 6px; margin-right: 3px; border-radius: 4px;
  font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  white-space: nowrap; }
.flag-bad { background: #fef3f2; color: #b42318; }
.flag-warn { background: #fff5e6; color: #92400e; }
.flag-info { background: #eef2f9; color: #344767; }
.pill { display: inline-block; padding: 1px 7px; margin: 1px 3px 1px 0; border-radius: 999px;
  background: #f1f4f9; font-size: 11px; }

/* ---------------------------------------------------------- board appendix */
/* `.dtable` is width:100%, which makes the nowrap credential column absorb all
   the slack and squeezes the person columns into a sliver at the right edge.
   The matrix wants to be exactly as wide as its content instead. */
.matrix-table { width: auto; min-width: auto; }
.matrix-table th.matrix-col { padding: 4px 2px; text-align: center; vertical-align: bottom; }
.matrix-colname { writing-mode: vertical-rl; transform: rotate(180deg);
  white-space: nowrap; font-size: 10px; letter-spacing: normal; text-transform: none;
  color: var(--ink); font-weight: 600; }
.matrix-table td.matrix-rowhead, .matrix-table th.matrix-rowhead {
  text-align: left; white-space: nowrap; font-size: 11.5px; }
.matrix-cell { text-align: center; font-family: ui-monospace, SFMono-Regular, Menlo,
  Consolas, monospace; font-weight: 700; font-size: 12px; width: 22px; padding: 4px 0; }
.mx-certified { background: #e7f6ee; color: #067647; }
.mx-in_progress { background: #eaf1fd; color: var(--blue); }
.mx-expired { background: #fdecea; color: var(--fg-red); }
.mx-none { color: #b9c1d0; }
.mx-target { outline: 2px solid var(--teal); outline-offset: -2px; }

/* ------------------------------------------- report rewrite (2026-07-29)
   The three-panel report: KPI trend lines, the ranked gap list, and the
   movement event list. Colour for a trend comes from a SENTIMENT class the
   payload chooses, never from the sign of the number -- "+4 expired" and
   "+4 certified" are the same arithmetic and opposite news. */
.kpi-detail { margin: 6px 0 0; font-size: 11.5px; color: var(--muted); }
.kpi-trend { margin: 5px 0 0; font-size: 11.5px; font-weight: 700; }
.trend-good { color: #067647; }
.trend-bad { color: var(--fg-red); }
.trend-neutral { color: var(--muted); font-weight: 600; }
.kpi-note { margin: 7px 0 0; font-size: 11px; line-height: 1.4; color: var(--muted); }

.report-lede { margin: 0 0 4px; font-size: 12.5px; color: var(--muted); }
.report-note { margin: 12px 0 0; font-size: 11.5px; color: var(--muted); }
.report-empty { margin: 12px 0 0; font-size: 13px; color: var(--muted); }
.report-flag { color: var(--fg-red); font-weight: 600; }

/* Ranked gaps. A numbered <ol> with the marker suppressed: the rank is rendered
   as its own element so it can sit inline with the title and the severity tag. */
.gap-rank { list-style: none; margin: 14px 0 0; padding: 0; counter-reset: none; }
.gap-row { border: 1px solid var(--line); border-left: 4px solid #c3ccdb;
  border-radius: 8px; padding: 11px 14px; margin-bottom: 9px; }
.gap-row.sev-high { border-left-color: var(--fg-red); background: #fffbfa; }
.gap-row.sev-medium { border-left-color: var(--amber); background: #fffcf5; }
.gap-row.sev-low { border-left-color: var(--blue); background: #fbfcff; }
.gap-row-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.gap-rank-num { flex-shrink: 0; width: 22px; height: 22px; border-radius: 50%;
  background: var(--ink); color: #fff; font-size: 11.5px; font-weight: 800;
  display: inline-flex; align-items: center; justify-content: center; }
.gap-row-title { margin: 0; font-size: 14px; font-weight: 700; flex: 1; min-width: 0; }
.gap-row-why { margin: 6px 0 0; font-size: 12.5px; color: var(--ink); }
.gap-row-ev { margin: 4px 0 0; font-size: 11.5px; color: var(--muted); }
.gap-ev-label { font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
  font-size: 10px; margin-right: 5px; color: var(--ink); }

.event-list { list-style: none; margin: 8px 0 0; padding: 0; font-size: 12.5px; }
.event-list li { display: flex; gap: 14px; padding: 2px 0; break-inside: avoid;
  border-bottom: 1px solid var(--line); }
.event-list li:last-child { border-bottom: none; }
.event-when { flex: 0 0 84px; color: var(--muted); font-variant-numeric: tabular-nums; }

/* ====================================================================== print
   The report is landscape by default: the widest tables here carry 13-15
   columns, and portrait clips the rightmost ones off the PDF entirely (the
   workspace print corollary -- see ../CLAUDE.md). Scroll regions must go
   `overflow: visible` for the same reason: an overflow container prints only
   its visible slice, silently dropping the columns scrolled out of view.
   ====================================================================== */
@media print {
  /* Filter chrome is not content -- keep it off a printed catalog. */
  .filterbar { display: none !important; }
  @page { size: A4 landscape; margin: 11mm; }

  .noprint, .masthead, .adminbar, .report-builder, .scroll-hint { display: none !important; }

  /* The best-knowledge disclaimer is NOT hidden here on purpose. The PDF is a
     deliverable that leaves the workspace, so it carries the notice like every
     other one (workspace guardrail). It sits after the report footer, small and
     muted -- a footer, never a banner. */
  .disclaimer { margin-top: 10px; padding-top: 6px; font-size: 8px; max-width: none;
    break-inside: avoid; }

  body { background: #fff; font-size: 11px; }
  .wrap { max-width: none; padding: 0; }
  .report { border: none; box-shadow: none; border-radius: 0; padding: 0; }

  /* An overflow container prints its visible slice only. */
  .dtable-scroll, .table-scroll { overflow: visible !important; }

  /* Backgrounds carry meaning on badges, matrix cells and severity strips. */
  .badge, .badge-cert, .flag, .sev-tag, .pill, .swatch, .kpi, .finding,
  .matrix-cell, .report-classification, .dtable th, .row-muted td {
    -webkit-print-color-adjust: exact; print-color-adjust: exact;
  }

  .report-section { break-before: auto; break-inside: auto; margin-top: 18px; }
  /* Tighter gap cards in print: the ranked list is the densest thing a reader
     wants on paper, and the screen padding wastes ~2 entries per page. */
  .gap-row { padding: 8px 11px; margin-bottom: 6px; }
  .gap-row-why { margin-top: 3px; }
  .gap-row-ev { margin-top: 2px; }
  .kpi-note { margin-top: 5px; }
  .report-h { break-after: avoid; }
  .report-h4, .panel-h { break-after: avoid; }
  .kpi, .gap-card, .panel, .finding, .gap-row { break-inside: avoid; }
  /* The appendix is a separate document section -- start it on a fresh page so
     the three panels read as a self-contained report when both are printed. */
  .appendix-start { break-before: page; }
  /* The event list is deliberately single-column (see the rule above) -- do not
     reintroduce CSS columns here: a short list reads as a run-on row. */
  .dtable tr { break-inside: avoid; }
  .dtable thead { display: table-header-group; }   /* repeat headers per page */
  .dtable-compact th, .dtable-compact td { padding: 3px 5px; font-size: 9px; }
  .dtable-compact th { font-size: 8px; }
  .kpi-num { font-size: 19px; }
  .report-title { font-size: 20px; }
  .report-h { font-size: 14px; }
  .matrix-colname { font-size: 8px; }
  .dtable th.w-mix { width: 130px; }
  .dtable th.w-bar { width: 100px; }
  .panel-row { break-inside: auto; }
}

/* ---- catalog filter bar (2026-07-30) -----------------------------------
   Server-side GET controls; same vocabulary as the sibling portals' boards. */
.filterbar { margin: 0 0 18px; display: flex; flex-direction: column; gap: 8px; }
.chiprow { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.chiprow-label {
  font-size: 11px; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--muted);
}
.chip {
  padding: 6px 13px; border-radius: 999px; border: 1px solid var(--line);
  background: #fff; color: var(--ink); font-weight: 600; font-size: 12.5px;
  text-decoration: none; display: inline-block; transition: border-color .12s;
}
.chip:hover { border-color: #c3ccdb; }
.chip-active { background: var(--fg-red); color: #fff; border-color: var(--fg-red); }
.chip-active:hover { border-color: var(--fg-red); }
.searchform { display: flex; align-items: flex-end; gap: 10px 14px; flex-wrap: wrap; }
.field-inline {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; color: var(--muted);
}
.search-input { width: 240px; max-width: 100%; }
.filter-summary { margin: 2px 0 0; font-size: 12.5px; color: var(--muted); }

/* Clamp the Links table's "Sent to / note" cell: an email
   address has no break opportunities, so one long recipient would balloon the
   column and push the widest table in the app wider still (measured: the region
   went 924px -> 1134px before this rule). `overflow-wrap: anywhere` lets the
   address break instead of setting the column width. */
.dtable .share-who { display: block; max-width: 170px; overflow-wrap: anywhere; }

/* ---------------------------------------- allowlist: scope picker + row editors
   (2026-08-02). The scope control used to be `<select multiple size="3">`: at
   1366 it rendered a 76px box holding 200px of options, so an admin saw 3 of 10
   collections, auto-scrolled to a different arbitrary window on every row, with
   the 4th option sliced in half and a second pick needing Ctrl/Cmd-click. It is
   the single highest-stakes field on the page -- ticking nothing grants the whole
   catalog -- so it is now the one you can read without interacting with it. */

/* Visually hidden, still announced. `position: absolute` inside a `.table-scroll`
   is the documented trap here (a visually-hidden checkbox once stretched the NSE
   board's page by 419px by resolving against the initial containing block), so
   the two fieldsets below are `position: relative` to contain it, AND this is
   clipped to 1px so it cannot contribute width even if that ever regresses. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

.scopepick { position: relative; }
.scopepick {
  border: 1px solid var(--line); border-radius: 6px; padding: 8px 10px; margin: 0;
  background: #fff;
}
/* auto-fill, so 10 collections lay out in columns and 30 still fit without a
   scrollbar that hides the rest. No fixed `size` means nothing can clip. */
.scopepick-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: 2px 14px;
}
.scopepick-item {
  display: flex; align-items: center; gap: 6px; font-size: 12.5px;
  color: var(--ink); cursor: pointer; padding: 1px 0;
}
.scopepick-note { margin: 6px 0 0; font-size: 11.5px; color: var(--muted); }
.scopepick-note strong { color: #a01e15; }
/* Watermark picker (_watermark_pick.html). Deliberately NOT .scopepick-grid:
   that lays out in auto-fill columns for a long list of short collection names,
   and these are three options each carrying a line of explanation. Stacked, so
   every option reads as a sentence and nothing depends on the column width. */
.wmpick { display: grid; gap: 7px; }
.wmpick-item {
  display: flex; align-items: flex-start; gap: 8px; cursor: pointer; padding: 1px 0;
}
/* flex: none so the control keeps its size when the help text wraps; the small
   top offset lines the radio up with the label's cap height rather than the
   centre of a two-line block. */
.wmpick-item input { flex: none; margin-top: 3px; }
.wmpick-body { display: flex; flex-direction: column; gap: 1px; }
.wmpick-label { font-size: 12.5px; font-weight: 600; color: var(--ink); }
.wmpick-help { font-size: 11.5px; color: var(--muted); }
/* Inside a row editor the column is narrow, so drop to one column. Deliberately
   NO max-height: item heights vary (a long collection name wraps to two lines --
   measured 56px against 24px for a short one), so any fixed cap slices whichever
   item straddles it. That is the exact defect being fixed, reintroduced one level
   down. The editor opens one row at a time on purpose, so it can afford to show
   the whole list; `min-width` keeps most names on a single line. */
.dtable .scopepick-grid { grid-template-columns: 1fr; }
.dtable .scopepick { padding: 6px 8px; min-width: 186px; }

/* Collapsed row editors. Native <details> -- no script, which matters because
   this deployment can lose its scripts with nothing in the app's logs. Every row
   used to render a live 10-option select inline, so one entry stood ~280px tall
   and seven of them ran to 2000px. Current state stays visible above the
   disclosure; only the controls fold away. */
/* The expiry cell used to borrow `.share-who`, which is the Links table's
   recipient-email rule: `max-width: 170px` + `overflow-wrap: anywhere`. That is
   right for an address with no break opportunities and wrong here -- once the
   table stopped overflowing and the columns tightened, it broke "expired" into
   "expir/ed" and "2026-08-04" across two lines. A status word and an ISO date are
   atomic; they get nowrap and the column sizes to them. */
/* "Added by" is an ADMIN EMAIL, which has no break opportunities, so one entry
   added by a real account (rather than a short CLI actor like "cli") set the
   column width for the whole table and pushed the last column out of the region
   -- measured: 0 -> 159px of overflow the moment a real admin added an entry.
   Same rule `.share-who` uses on the Links table, and for the same reason. */
.addedby-cell {
  display: block; max-width: 98px; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap;
}
.expiry-cell { white-space: nowrap; }
.expiry-cell .rowedit-summary { white-space: normal; }

/* Group names DO wrap (some run to "Accenture Federal Services"), but on word
   boundaries only, and the column gets enough width that they land on two lines
   rather than three. */
.dtable .scope-current { min-width: 118px; }
.dtable .scope-current .badge { overflow-wrap: normal; }

/* An unadorned list inside a row editor -- the GSI member roster. No bullets and
   no left padding, because it sits in a table cell where the marker column would
   push the text out of line with the state badge above it. */
.plainlist { list-style: none; margin: 4px 0 0; padding: 0; font-size: 12px; }
.plainlist li { padding: 2px 0; }

.rowedit { margin: 6px 0 0; }
.rowedit-summary {
  display: inline-block; cursor: pointer; font-size: 11.5px; color: var(--fg-red);
  padding: 1px 0; list-style-position: inside;
}
.rowedit-summary:hover { text-decoration: underline; }
.rowedit[open] > .rowedit-summary { margin-bottom: 6px; }
.rowedit .inline-form { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }

/* ------------------------------------------------- allowlist: numbered add form
   Three steps instead of one flat flex row, because the old layout wrapped an
   unlabelled date input and an unlabelled scope box onto a second line where
   neither said what it was. */
.formstep { display: flex; gap: 10px; padding: 10px 0; border-top: 1px solid var(--line); }
.formstep:first-of-type { border-top: 0; padding-top: 0; }
.formstep-num {
  flex: 0 0 20px; height: 20px; border-radius: 50%; background: var(--fg-red);
  color: #fff; font-size: 11px; font-weight: 700; line-height: 20px; text-align: center;
}
.formstep-body { flex: 1; min-width: 0; }
.formstep-body .field-label { display: block; margin-bottom: 6px; }
.formstep-split { display: flex; gap: 22px; flex-wrap: wrap; }
.formstep-split > div { min-width: 240px; }
.field-optional {
  font-size: 11px; font-weight: 500; color: var(--muted); text-transform: none;
  letter-spacing: 0;
}

/* GSP_DEBUG only: the exception chain on the 500 page. */
.error-detail { text-align: left; white-space: pre-wrap; font-size: 11.5px; max-height: 50vh;
  overflow: auto; background: #f1f3f7; padding: 10px; border-radius: 6px; }
