/* NKL Admin Portal — components */

/* card */
.nkl-card{background:var(--nkl-surface); border:1px solid var(--nkl-border); border-radius:var(--nkl-radius); padding:16px; box-shadow:var(--nkl-shadow-sm)}
.nkl-card-title{display:flex; align-items:center; justify-content:between; gap:8px; margin-bottom:12px}

/* contextual help ("what is this?") button - Portal Completion Program P1-B.
   Real button, not decorative; keyboard/touch reachable via native <button>
   semantics + Bootstrap's own modal focus-trap/Escape handling (data-bs-*,
   no custom JS). Never hover-only. */
.nkl-help-btn{background:transparent; border:none; padding:0; margin-left:4px; width:20px; height:20px; border-radius:50%; display:inline-flex; align-items:center; justify-content:center; color:var(--nkl-text-muted); vertical-align:middle; cursor:pointer}
.nkl-help-btn:hover, .nkl-help-btn:focus-visible{color:var(--nkl-navy); background:var(--nkl-surface-hover)}
.nkl-help-btn:focus-visible{outline:2px solid var(--nkl-navy); outline-offset:1px}
.nkl-help-body{font-size:14px; line-height:1.5; color:var(--nkl-text)}
.nkl-help-body dt{font-weight:600; margin-top:10px}
.nkl-help-body dt:first-child{margin-top:0}
.nkl-help-body dd{color:var(--nkl-text-muted); margin-bottom:0}

/* pre-auth shell (login/mfa/reauthenticate) - moved here from an inline
   <style> block in auth/_auth_base.html, itself a CSP style-src
   violation (blocks BOTH inline style="" attributes and inline <style>
   blocks equally - no visual change, same rules, real location). */
/* OWNER CHANGE (2026-09-01): DDP mark retired, NikaLogic is the brand -
   Variant C from the owner's own reviewed demo (centred logo, no dark
   banner). Same page background this file has always used. */
body.nkl-auth{background:#283548; min-height:100vh; display:flex; align-items:center; justify-content:center; padding:24px}
.nkl-auth-card{
  width:100%; max-width:400px; background:var(--nkl-surface); border-radius:12px; padding:32px; box-shadow:var(--nkl-shadow-md);
}
.nkl-auth-brand{display:flex; align-items:center; gap:10px; margin-bottom:24px}

/* Centred NikaLogic wordmark + two-line title (auth/_auth_base.html),
   replacing the former DDP background-banner treatment. `img` sizing
   here (never inline `style=`, `style-src 'self'` CSP) rather than
   relying on the SVG's own intrinsic dimensions, so this stays correct
   even if the shared `nikalogic-full.svg` asset is ever re-exported at a
   different native size. */
.nkl-auth-title{width:100%; padding-top:0; margin-bottom:24px; box-sizing:border-box; display:flex; flex-direction:column; align-items:center; gap:8px}
.nkl-auth-title img{height:40px; width:auto}
.nkl-auth-title-primary{font-size:16px; font-weight:700; color:#283548; text-align:center}
.nkl-auth-title-secondary{font-size:12px; font-weight:400; color:var(--nkl-text-muted); text-align:center}

/* narrow single-column forms (MFA enrollment, password change) */
.nkl-card-narrow-520{max-width:520px; margin-left:auto; margin-right:auto}

.nkl-mfa-qr{background:#fff; border:1px solid var(--nkl-border); border-radius:8px; padding:16px; display:inline-block}
.nkl-mfa-qr svg{width:180px; height:180px; display:block}

/* metric card */
.nkl-metric{padding:14px}
.nkl-metric-label{font-size:var(--nkl-fs-caption); text-transform:uppercase; color:var(--nkl-text-muted); margin-bottom:6px}
.nkl-metric-value{font-size:22px; font-weight:700; display:flex; align-items:center; gap:8px}
.nkl-metric-meta{font-size:var(--nkl-fs-caption); color:var(--nkl-text-muted); margin-top:4px}

/* buttons */
.btn.nkl-btn-primary{background:var(--nkl-accent); border-color:var(--nkl-accent); color:#fff; font-weight:600}
.btn.nkl-btn-primary:hover{background:var(--nkl-accent-dark); border-color:var(--nkl-accent-dark); color:#fff}
.btn.nkl-btn-secondary{background:var(--nkl-secondary-btn); border-color:var(--nkl-secondary-btn); color:#fff; font-weight:600}
.btn.nkl-btn-secondary:hover{background:var(--nkl-secondary-btn-dark); border-color:var(--nkl-secondary-btn-dark); color:#fff}
.btn.nkl-btn-tertiary{background:var(--nkl-surface); border:1px solid var(--nkl-border); color:var(--nkl-text); font-weight:600}
.btn.nkl-btn-danger{background:var(--nkl-error); border-color:var(--nkl-error); color:#fff; font-weight:600}
.btn.nkl-btn-outline-accent{background:var(--nkl-surface); border:1.5px solid var(--nkl-accent); color:var(--nkl-accent); font-weight:600}
.btn.nkl-btn-outline-accent:hover{background:var(--nkl-success-bg)}
.btn.nkl-btn-primary, .btn.nkl-btn-secondary, .btn.nkl-btn-tertiary, .btn.nkl-btn-danger, .btn.nkl-btn-outline-accent{display:inline-flex; align-items:center; justify-content:center; gap:6px; font-size:13px; padding:.45rem .85rem; border-radius:7px}

/* status badges */
.nkl-badge{display:inline-flex; align-items:center; gap:5px; font-size:11px; font-weight:600; padding:3px 9px; border-radius:999px; white-space:nowrap}
.nkl-badge::before{content:''; width:6px; height:6px; border-radius:50%; background:currentColor}
.nkl-badge-success{background:var(--nkl-success-bg); color:var(--nkl-success)}
.nkl-badge-info{background:var(--nkl-info-bg); color:var(--nkl-info)}
.nkl-badge-warning{background:var(--nkl-warning-bg); color:var(--nkl-warning)}
.nkl-badge-error{background:var(--nkl-error-bg); color:var(--nkl-error)}
.nkl-badge-critical{background:var(--nkl-critical-bg); color:var(--nkl-critical)}
.nkl-badge-neutral{background:var(--nkl-neutral-bg); color:var(--nkl-neutral)}

/* permission badge */
.nkl-perm{display:inline-flex; align-items:center; justify-content:center; width:20px; height:20px; border-radius:5px; font-size:11px}
.nkl-perm-yes{background:var(--nkl-success-bg); color:var(--nkl-success)}
.nkl-perm-no{background:var(--nkl-neutral-bg); color:var(--nkl-text-muted)}

/* table */
.nkl-table thead th{font-size:var(--nkl-fs-caption); text-transform:uppercase; color:var(--nkl-text-muted); border-bottom:1px solid var(--nkl-border); font-weight:600; white-space:nowrap}
.nkl-table td, .nkl-table th{padding:10px 10px; font-size:13px; border-color:var(--nkl-border); vertical-align:middle}
.nkl-table tbody tr:hover{background:var(--nkl-bg)}
.nkl-sort-btn{background:none; border:none; padding:0; color:inherit; display:inline-flex; align-items:center; gap:4px; font:inherit; cursor:pointer}
/* Scope E (2026-08-27) - `macros/ui.html`'s `sort_th()` - a real server-side
   sort link, styled to read as part of the header row (not a generic blue
   hyperlink) while still keeping real link semantics/focus outline. */
.nkl-sort-link{color:inherit; text-decoration:none}
.nkl-sort-link:hover{text-decoration:underline}

/* filter bar */
.nkl-filter-bar{display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-bottom:14px}
.nkl-filter-chip{display:inline-flex; align-items:center; gap:6px; background:var(--nkl-bg); border:1px solid var(--nkl-border); border-radius:999px; padding:4px 10px; font-size:12px}
.nkl-filter-chip button{background:none; border:none; padding:0; color:var(--nkl-text-muted); display:flex}

/* pagination */
.nkl-pagination{display:flex; align-items:center; justify-content:space-between; gap:10px; margin-top:14px; flex-wrap:wrap}
.nkl-pagination .nkl-caption{margin:0}

/* states */
.nkl-state{display:flex; flex-direction:column; align-items:center; text-align:center; gap:8px; padding:48px 20px; color:var(--nkl-text-muted)}
.nkl-state .nkl-icon{color:var(--nkl-neutral)}
.nkl-skeleton{background:linear-gradient(90deg, var(--nkl-border) 25%, #eef0f3 37%, var(--nkl-border) 63%); background-size:400% 100%; animation:nkl-shimmer 1.4s infinite; border-radius:6px; height:14px; width:100%}
@keyframes nkl-shimmer{0%{background-position:100% 50%}100%{background-position:0 50%}}
/* macros/ui.html's loading_state(rows) - width steps down 8% per row so
   placeholder lines taper like real trailing text, without a CSP-violating
   inline style="width:...%" per row (never more than 8 rows in practice). */
.nkl-skeleton:nth-child(2){width:92%}
.nkl-skeleton:nth-child(3){width:84%}
.nkl-skeleton:nth-child(4){width:76%}
.nkl-skeleton:nth-child(5){width:68%}
.nkl-skeleton:nth-child(6){width:60%}
.nkl-skeleton:nth-child(7){width:52%}
.nkl-skeleton:nth-child(8){width:44%}

/* tabs */
.nkl-tabs{display:flex; gap:4px; border-bottom:1px solid var(--nkl-border); margin-bottom:16px; flex-wrap:wrap}
.nkl-tab{background:none; border:none; padding:9px 4px; margin-right:16px; font-size:13px; font-weight:600; color:var(--nkl-text-muted); border-bottom:2px solid transparent}
.nkl-tab.active{color:var(--nkl-navy); border-bottom-color:var(--nkl-accent)}

/* definition list / detail rows */
.nkl-dl{display:grid; grid-template-columns:180px 1fr; gap:8px 16px; font-size:13px}
.nkl-dl dt{color:var(--nkl-text-muted)}
.nkl-dl dd{margin:0}

/* timeline */
.nkl-timeline{list-style:none; margin:0; padding:0; position:relative}
.nkl-timeline li{position:relative; padding:0 0 20px 26px; border-left:2px solid var(--nkl-border); margin-left:6px}
.nkl-timeline li:last-child{border-color:transparent}
.nkl-timeline li::before{content:''; position:absolute; left:-7px; top:2px; width:12px; height:12px; border-radius:50%; background:var(--nkl-surface); border:2px solid var(--nkl-info)}
.nkl-timeline li.done::before{background:var(--nkl-success); border-color:var(--nkl-success)}
.nkl-timeline li.failed::before{background:var(--nkl-error); border-color:var(--nkl-error)}

/* secrets */
.nkl-secret-mask{font-family:var(--nkl-font-mono); letter-spacing:2px; color:var(--nkl-text-muted)}
.nkl-fingerprint{font-family:var(--nkl-font-mono); font-size:12px; color:var(--nkl-text-muted); word-break:break-all; user-select:all}
/* one-time-plaintext reveal (activation codes, administrator create/reset) - larger than the default fingerprint display */
.nkl-fingerprint-lg{font-size:1.05rem}
.nkl-code{font-family:var(--nkl-font-mono); font-size:12px; background:var(--nkl-bg); border:1px solid var(--nkl-border); border-radius:6px; padding:8px 10px; display:block; white-space:pre-wrap}
.nkl-copy-btn{background:none; border:1px solid var(--nkl-border); border-radius:6px; padding:3px 6px; color:var(--nkl-text-muted); display:inline-flex; align-items:center}
.nkl-copy-btn:hover{background:var(--nkl-bg)}
/* P11-CONFIG-COPY (2026-08-26) - visible confirmation ONLY while
   portal.js's real clipboard-write Promise has actually resolved (see
   that file's own comment) - never a static/assumed state. */
.nkl-copy-btn[data-copied]{color:var(--nkl-brand-success); border-color:var(--nkl-brand-success)}
/* P11-ACTIVATION-VIEWER (2026-08-26) - bounded scrollable code viewer,
   additive modifier on top of the generic `.nkl-code` class (never a
   change to it - `macros/ui.html`'s `code_block()` macro also uses
   `.nkl-code` alone for short inline snippets that must keep their
   current unbounded/no-scroll behavior). Root cause this fixes: an
   expanded Windows/Linux bootstrap script (`white-space:pre-wrap` on the
   base class, no max-height/overflow anywhere) grew the entire page to
   the script's full line count. `max-height` is viewport-relative
   (clamp, not a fixed px) so it scales sensibly on short and tall
   viewports alike; `overflow:auto` gives vertical scroll inside the
   viewer once content exceeds that height. `white-space:pre` (overriding
   the base class's `pre-wrap`) is required for the horizontal scroll the
   owner also asked for - a long unbroken script line (e.g. a base64
   blob) scrolls sideways inside the box instead of wrapping, so no line
   is ever visually clipped or reflowed away from its real content. */
.nkl-code.nkl-code-viewer{max-height:clamp(180px, 40vh, 480px); overflow:auto; white-space:pre}
.nkl-bootstrap-run-guide{background:var(--nkl-surface); border:1px solid var(--nkl-border); border-radius:var(--nkl-radius-sm); overflow:hidden}
.nkl-bootstrap-run-guide-header{padding:12px 14px; border-bottom:1px solid var(--nkl-border)}
.nkl-bootstrap-run-guide-header .nkl-small{margin-top:3px; max-width:75ch}
.nkl-bootstrap-run-grid{display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr)}
.nkl-bootstrap-run-platform{padding:14px; min-width:0}
.nkl-bootstrap-run-platform + .nkl-bootstrap-run-platform{border-left:1px solid var(--nkl-border)}
.nkl-bootstrap-run-platform-title{font-size:13px; font-weight:700; margin-bottom:10px}
.nkl-bootstrap-run-option + .nkl-bootstrap-run-option{margin-top:12px}
.nkl-bootstrap-run-option-label{font-size:12px; font-weight:600; color:var(--nkl-text-muted); margin-bottom:5px}
.nkl-bootstrap-command{position:relative; min-width:0}
.nkl-bootstrap-command .nkl-code{margin:0; padding:9px 64px 9px 10px; overflow-x:auto; white-space:pre; line-height:1.55; user-select:all}
.nkl-bootstrap-command .nkl-copy-btn{position:absolute; top:6px; right:6px; background:var(--nkl-surface)}
.nkl-bootstrap-command .nkl-copy-btn:hover{background:var(--nkl-surface-hover)}
/* P11-ACTIVATION-R3 (2026-08-26) - REMOVED: the previous pass's
   `.nkl-activation-result-body` (result-level `max-height`/
   `overflow-y:auto` wrapper) and this rule's own `overscroll-behavior:
   contain` (which only existed to keep that now-removed outer scroll
   container from fighting with this one). Real owner correction: a
   second, page-like scrollbar around the whole blue Activation result
   was never wanted - only THIS per-script viewer stays bounded; the
   result block itself now participates in normal document flow again
   (the page grows, the browser's own scrollbar handles it). */
.nkl-sample-tag{display:inline-flex; align-items:center; gap:5px; font-size:10px; font-weight:700; letter-spacing:.04em; text-transform:uppercase; color:var(--nkl-warning); background:rgba(216,145,32,.12); padding:2px 7px; border-radius:5px}

/* health indicator */
.nkl-health{display:flex; align-items:center; gap:6px; font-size:13px}
.nkl-health-dot{width:9px; height:9px; border-radius:50%}
.nkl-health-dot.ok{background:var(--nkl-success)}
.nkl-health-dot.warn{background:var(--nkl-warning)}
.nkl-health-dot.error{background:var(--nkl-error)}
.nkl-health-row{display:flex; align-items:center; justify-content:space-between; padding:7px 0; border-bottom:1px solid var(--nkl-border); font-size:13px; flex-wrap:wrap; row-gap:4px}
.nkl-health-row:last-child{border-bottom:none}
.nkl-health-row > span{min-width:0}
.nkl-health-row .nkl-health{font-size:12px; font-weight:600}
.nkl-health-row .nkl-health.ok{color:var(--nkl-success)}
.nkl-health-row .nkl-health.warn{color:var(--nkl-warning)}
.nkl-health-row .nkl-health.error{color:var(--nkl-error)}

/* P11-DASH-004 / P11-DASH-005 (2026-08-20, retest 1 consolidated root
   cause): a flex item's implicit min-width:auto refuses to shrink below
   an unbreakable child (a machine ID with no wrap points, a status
   badge/dot) - it pushes the whole row, and therefore the page, wider
   than its container at ANY viewport where the row's available width is
   tight, not only mobile. The prior fix (responsive.css) was correct but
   scoped to max-width:767.98px and to `> div` only; it never protected
   the same rows at 1024px tablet width once the Dashboard's Rotation/
   Admin-Actions/Health sections (added between passes) increased card
   content density there too (P11-DASH-005), and it missed rows whose
   direct children are <span> (Key Rotation Status, Recent Administrative
   Actions' timestamp) rather than <div>. Made unconditional - harmless
   at wide viewports since min-width:0/flex-wrap only take visible effect
   when content genuinely does not fit - and widened to cover every real
   card flex-row shape found in this audit. Scoped to `.nkl-card`/
   `.nkl-quick-tile` only - authenticated-portal card content never
   appears on the OWNER-LOCKED Login page. */
.nkl-card .d-flex.justify-content-between{flex-wrap:wrap; row-gap:8px}
.nkl-card .d-flex.justify-content-between > div,
.nkl-card .d-flex.justify-content-between > span{min-width:0}
.nkl-card .flex-grow-1{min-width:0}
.nkl-quick-tile{flex-wrap:wrap}
.nkl-quick-tile > span:last-child{min-width:0}

/* P11-01 (2026-08-24, real-browser 390px QA pass): SAME root cause as
   the two rules above (a flex item's implicit min-width:auto refuses to
   shrink below an unbreakable child) but found in a location that fix
   never reached - Configuration's per-setting Save form
   (`system/configuration.html`), which lives inside a `.nkl-table-stack`
   action cell, not a `.nkl-card` direct child. Nested flex chain: `td`
   (flex, mobile-only via responsive.css) -> `form.d-flex` -> an enum
   `<select>` whose longest option text (e.g. "REQUIRE_ADMIN_APPROVAL")
   or a value `<input>` held the whole chain open past 390px. Written
   unconditionally (not media-query-scoped) because `form.d-flex` is
   always a real flex container via Bootstrap's `.d-flex` regardless of
   viewport - harmless at wide viewports where there is room to spare,
   same discipline as the two rules above. `flex-wrap` on the form is
   the fallback if the select/input and Save button still cannot both
   fit on one line even at their minimum basis. */
.nkl-table-stack td form.d-flex{min-width:0; flex:1 1 auto; flex-wrap:wrap; row-gap:6px}
.nkl-table-stack td form.d-flex > .form-control,
.nkl-table-stack td form.d-flex > .form-select{min-width:0; flex:1 1 140px}

.nkl-metric-icon{width:32px; height:32px; border-radius:8px; display:flex; align-items:center; justify-content:center; flex-shrink:0}
.nkl-metric-icon-success{background:var(--nkl-success-bg); color:var(--nkl-success)}
.nkl-metric-icon-warning{background:var(--nkl-warning-bg); color:var(--nkl-warning)}
.nkl-metric-icon-error{background:var(--nkl-error-bg); color:var(--nkl-error)}
.nkl-metric-icon-info{background:var(--nkl-info-bg); color:var(--nkl-info)}
.nkl-metric-icon-neutral{background:var(--nkl-neutral-bg); color:var(--nkl-neutral)}

.nkl-quick-grid{display:grid; grid-template-columns:1fr 1fr; gap:10px}
.nkl-quick-tile{display:flex; align-items:center; gap:10px; padding:10px; border:1px solid var(--nkl-border); border-radius:var(--nkl-radius-sm); background:var(--nkl-surface); text-align:left}

/* Configuration Center 3-pane layout (owner-approved architecture,
   2026-08-25). Base state below is real, always-rendered HTML - every
   category panel is visible and every info <details> works with zero
   JS; static/js/configuration-panes.js only enhances it (hides
   non-active panels, mirrors an opened <details> into the right pane).
   Left-pane category links deliberately reuse `.nkl-nav-link`/`.active`
   from layout.css (this page's own `.nkl-config-cat-link.active` rule
   below mirrors that exact declaration) rather than inventing new
   active-state styling. */
/* owner 2026-09-09: all three columns share the tallest column's height
   (align-items:stretch), each column's own content stays top-aligned, and
   the two side columns carry a surface background so it runs to the floor.
   The info pane is no longer position:sticky - a sticky box cannot also be
   a full-height panel; the owner asked for the panel. Left category nav is
   toned down to look like the main sidebar links: smaller font, no icons,
   Title-Case labels come from boundary._CONFIG_CATEGORY_DISPLAY. */
.nkl-config-layout{display:grid; grid-template-columns:200px minmax(0,1fr) 300px; gap:16px; align-items:stretch; min-width:0}
.nkl-config-cats{display:flex; flex-direction:column; gap:2px; min-width:0; background:var(--nkl-surface); border:1px solid var(--nkl-border); border-radius:var(--nkl-radius); padding:8px 6px}
.nkl-config-cat-link{background:none; border:none; width:100%; text-align:left; font:inherit; font-size:12px; padding:6px 8px; cursor:pointer}
.nkl-config-cat-link.active{background:var(--nkl-success-bg); color:var(--nkl-brand-success); font-weight:600; border-left:3px solid var(--nkl-brand-success); padding-left:5px}
.nkl-config-center{min-width:0}
.nkl-config-info-pane{min-width:0; align-self:stretch; background:var(--nkl-surface); border:1px solid var(--nkl-border); border-radius:var(--nkl-radius); padding:14px}
.nkl-config-info-details{display:inline-block; margin-left:4px; vertical-align:middle}
.nkl-config-info-details > summary{list-style:none; cursor:pointer}
.nkl-config-info-details > summary::-webkit-details-marker{display:none}
.nkl-config-info-details.nkl-config-info-selected > summary.nkl-help-btn{color:var(--nkl-brand-success); background:var(--nkl-success-bg)}
.nkl-config-info-body{margin-top:8px; max-width:440px}
/* P11-CONFIG-A2 (2026-08-26, corrected same day after a real LIVE-browser
   recheck disproved the first pass) - scoped to the info pane content only
   (never the generic .nkl-dl used by about.html/rotations/detail.html).
   REAL ROOT CAUSE of the first attempt's failure: configuration-panes.js's
   click handler does `clone.classList.remove('nkl-config-info-body')`
   before appending the clone into the right info pane - the class this
   selector depended on is gone from the LIVE DOM by the time it matters,
   so `.nkl-config-info-body .nkl-dl` never matched the actual right-pane
   content at all (it only ever matched the no-JS center `<details>`,
   which nobody was looking at in the browser test that caught this). The
   static/structural test suite that shipped alongside the first pass
   checked the CSS file's text and the JS file's text SEPARATELY - neither
   check simulates cloneNode+classList.remove+DOM-insertion together, so
   neither one could have caught a cross-file runtime DOM state change
   like this; only a real browser/DOM-level check can. Fixed by ALSO
   matching `[data-config-info-content]` - the attribute
   configuration-panes.js sets on the clone and never removes - covering
   both the JS-enhanced right-pane clone (has the attribute, lost the
   class) and the no-JS center `<details>` (still has the class, has no
   attribute). The fixed 180px label column that works in a full-width
   page reads as a squeezed, broken two-column form inside this ~300px
   right pane; vertical label-over-value blocks instead:
   grid-template-columns:1fr turns the existing dt/dd sequence (one dt+dd
   pair per metadata item) into one column, so each dt lands on its own
   row immediately above its dd - no markup change needed, dl/dt/dd order
   is already correct. */
.nkl-config-info-body .nkl-dl, [data-config-info-content] .nkl-dl{grid-template-columns:1fr; gap:0}
/* P11-CONFIG-A3 (2026-08-26) - visual hierarchy pass, same real-browser
   evidence discipline as A2 above (owner: "reads like one continuous
   block of text"). Scoped identically to A2 - only the Configuration
   info-panel content (`.nkl-config-info-body`/`[data-config-info-
   content]`), never the generic `.nkl-dl` or a global heading rule.
   Main title: `.nkl-config-info-body h3` overrides the template's own
   `.nkl-small` class (which SHRINKS font-size - the real reason the
   title read weak instead of like a heading) via selector specificity
   alone (0,1,1 beats 0,1,0), no template edit needed. Section labels
   (dt): uppercase/letter-spaced/muted-but-bold small caps, a subtle
   top border everywhere except the first item - reads as a real section
   divider without extra visual noise (an owner-forbidden loud treatment
   would be a colored/background block instead). Tight dd margin-top
   keeps each label visually bound to ITS OWN value, not the next
   section's, and `overflow-wrap:anywhere` keeps long Security/Notes
   prose or a 64-char SHA256 fingerprint from ever overflowing this
   ~300px pane. */
.nkl-config-info-body h3, [data-config-info-content] h3{font-size:15px; font-weight:700; color:var(--nkl-text); margin-bottom:8px}
.nkl-config-info-body .nkl-dl dt, [data-config-info-content] .nkl-dl dt{
  margin-top:14px; padding-top:10px; border-top:1px solid var(--nkl-border);
  font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.04em; color:var(--nkl-text-muted);
}
.nkl-config-info-body .nkl-dl dt:first-child, [data-config-info-content] .nkl-dl dt:first-child{margin-top:0; padding-top:0; border-top:none}
.nkl-config-info-body .nkl-dl dd, [data-config-info-content] .nkl-dl dd{margin-top:3px; margin-bottom:0; overflow-wrap:anywhere}
.nkl-quick-tile:hover{background:var(--nkl-surface-hover)}
.nkl-quick-tile-icon{width:32px; height:32px; border-radius:8px; background:var(--nkl-info-bg); color:var(--nkl-info); display:flex; align-items:center; justify-content:center; flex-shrink:0}

/* audit row */
.nkl-audit-row{display:grid; grid-template-columns:110px 90px 1fr; gap:10px; padding:8px 0; border-bottom:1px solid var(--nkl-border); font-size:13px}

/* avatars */
.nkl-avatar-sm{width:24px; height:24px; font-size:10px}

/* alerts */
.nkl-alert{border-radius:var(--nkl-radius-sm); padding:10px 14px; font-size:13px; display:flex; gap:10px; align-items:flex-start}
.nkl-alert-info{background:var(--nkl-info-bg); color:var(--nkl-text); border:1px solid var(--nkl-info)}
.nkl-alert-warning{background:var(--nkl-warning-bg); border:1px solid var(--nkl-warning)}
.nkl-alert-error{background:var(--nkl-error-bg); border:1px solid var(--nkl-error)}
.nkl-alert-success{background:var(--nkl-success-bg); border:1px solid var(--nkl-success)}
/* P11-CSP-FIX (2026-08-26) - fixed-position toasts, moved out of
   error-handling.js/portal.js's own `.style.<prop> = ...` JS mutations
   (real CSP `style-src-attr` violations - this project's CSP is
   `style-src 'self'`, no `'unsafe-inline'`) into real stylesheet rules;
   the JS now only ever toggles these class names. */
.nkl-toast-error{position:fixed; bottom:16px; right:16px; z-index:1000; max-width:360px; box-shadow:var(--nkl-shadow-md)}
.nkl-session-timeout-toast{position:fixed; bottom:16px; right:16px; z-index:1000; max-width:320px}
.nkl-error-icon{flex-shrink:0}

/* modal danger */
.nkl-modal-danger .modal-header{border-bottom-color:rgba(208,4,68,.25)}
.nkl-typed-confirm-input{font-family:var(--nkl-font-mono)}

/* form */
.nkl-form-label{font-weight:600; font-size:13px; margin-bottom:4px}
.nkl-form-hint{font-size:12px; color:var(--nkl-text-muted); margin-top:4px}
.nkl-form-error{font-size:12px; color:var(--nkl-error); margin-top:4px}

/* static preview banner */
.nkl-preview-banner{position:sticky; top:0; z-index:calc(var(--nkl-z-topbar) + 1); background:var(--nkl-critical); color:#fff; text-align:center; font-size:12px; font-weight:700; letter-spacing:.03em; padding:5px}

/* P11-CSP-DATA-URI-FIX (2026-08-26) - real root cause of the live CSP
   `img-src data:` violation (1 total, source attributed to
   bootstrap.bundle.min.js's Modal._showElement - the JS call that first
   makes a modal, and therefore its header's real DOM child, visible):
   Bootstrap 5's OWN vendor bootstrap.min.css styles every
   `.btn-close` (the modal dismiss "X", present in `#create-code` and
   every other modal on every page - `data-bs-toggle="modal"` markup is
   pure Bootstrap default, nothing this project authored) with
   `--bs-btn-close-bg:url("data:image/svg+xml,...")` - confirmed by
   direct inspection of the vendor file, no `data:` URI exists anywhere
   in our own CSS/templates/JS. This is NOT a tooltip/popover/carousel/
   sanitizer path (none are used anywhere in this app - confirmed by
   grep, zero `bootstrap.Tooltip`/`bootstrap.Popover` usage) - purely
   the standard close-button background-image every Bootstrap modal
   ships with. Per the owner's remediation contract ("replace with...
   existing icon system", never weaken `img-src`): the vendor's own
   data-uri background is neutralized via its own documented CSS custom
   property (`--bs-btn-close-bg:none` - the officially supported
   Bootstrap 5 way to swap this out, not a hack), replaced with a plain
   text glyph (`content:"\00D7"`, the Unicode multiplication sign) via
   `::before` - zero image/data/network dependency at all, using
   `currentColor`/Bootstrap's own `--bs-btn-close-color` variable so it
   still respects light/dark theming exactly like the SVG did. This is a
   single, additive CSS override in our own stylesheet - the vendor file
   itself is byte-for-byte untouched (still hash/version-pinned), and no
   CSP directive was relaxed. Scoped to `.btn-close` only, which is why
   this is the correct "one minimum consolidated fix" even though the
   button itself appears in templates outside Activation Codes - the
   root cause is the ONE shared component, not per-template markup, so a
   single shared CSS rule is the minimal fix, not 13 separate template
   edits. */
.btn-close{--bs-btn-close-bg:none; display:inline-flex; align-items:center; justify-content:center}
.btn-close::before{content:"\00D7"; font-size:1.4em; line-height:1; color:var(--bs-btn-close-color, #000)}

/* FINAL PORTAL / DATA CLOSEOUT (2026-08-27), G1 - the OTHER remaining
   real `data:image/svg+xml` CSP source (owner: "more remain" after the
   `.btn-close` fix above): `bootstrap.min.css`'s `.form-select` sets
   `--bs-form-select-bg-img:url("data:image/svg+xml,...")` (the dropdown
   chevron) - confirmed by direct inspection of the vendor file. Real,
   currently-triggering usage: `.form-select`/`.form-select-sm` appears in
   18 templates across this Portal (page-size selectors, status/sensitivity
   filters, etc.) - the single highest-frequency remaining violation
   source. `.form-check`/`.form-switch`/`.accordion`/`.navbar` (the other
   Bootstrap components with their own `data:image` variables in the same
   vendor file) are confirmed UNUSED anywhere in this app (grep, zero
   matches) - not touched, since there is nothing live to fix there.
   `--bs-form-select-bg-icon` (the SECOND, validation-state layer) never
   actually renders either - it requires Bootstrap's `.was-validated`/
   `.is-valid`/`.is-invalid` classes, confirmed unused anywhere in this
   app - its own CSS default (`var(--bs-form-select-bg-icon,none)`)
   already stays `none` here, nothing to override.
   Same technique as `.btn-close`: neutralize the vendor's own documented
   CSS custom property, but here `none` alone would leave selects with NO
   visual dropdown affordance at all, so this uses a real, same-origin
   static SVG file (`static/img/chevron-down.svg` — never `data:`, so
   `img-src 'self'` is satisfied) mirroring this app's own icon set
   (`macros/icons.html`'s `chevron-down` path, same stroke-width/viewBox)
   rather than inventing a new visual. Two files (light/dark stroke
   color) swapped via the SAME `[data-bs-theme="dark"]` attribute
   convention `tokens.css` already uses — a theme-aware color baked into
   a `data:` URI was not an option (still blocked by CSP), a same-origin
   file per theme is. Vendor file itself untouched, no CSP directive
   relaxed.

   PORTAL CLOSEOUT — CONSOLIDATED REMEDIATION (2026-08-30), finding A: real
   404 for `/static/vendor/img/chevron-down.svg` on every page, portal-wide.
   Root cause is a documented CSS Custom Properties resolution rule, not a
   typo: a `url()` carried inside a custom-property VALUE is not resolved
   where the property is declared — it is resolved relative to the
   stylesheet in which the `var()` SUBSTITUTION actually happens. The
   `--bs-form-select-bg-img` value is declared here (`static/css/
   components.css`), but the only place it is ever consumed via `var()` is
   `.form-select{background-image:var(--bs-form-select-bg-img),...}` inside
   the VENDOR file `static/vendor/bootstrap/bootstrap.min.css`. So a path
   written relative to THIS file's own location (`../img/...`, which would
   correctly mean `static/img/...` if this property were consumed here)
   instead resolved relative to the vendor file's location
   (`static/vendor/bootstrap/`), landing on the non-existent
   `static/vendor/img/chevron-down.svg`. Fixed by writing the path relative
   to the actual consuming file's location instead
   (`static/vendor/bootstrap/` + `../../img/...` = `static/img/...`) —
   confirmed against `static/img/chevron-down.svg`/`chevron-down-dark.svg`
   both existing on disk. Vendor file itself still untouched. See
   `_portal_verification/tests/test_static_asset_references_exist.py` for
   the permanent regression coverage (including a dedicated test for this
   exact custom-property cross-file resolution class, not just a plain
   same-file `url()` scan). */
.form-select{--bs-form-select-bg-img:url("../../img/chevron-down.svg")}
[data-bs-theme="dark"] .form-select{--bs-form-select-bg-img:url("../../img/chevron-down-dark.svg")}

/* 2026-10-08 (prod console: `img-src 'self'` blocked the checkbox tick
   `data:image/svg+xml`) - `.form-check`/`.form-switch`/`.accordion` ARE
   used now (scenario panels, e.g. captcha-routing-policy). Same technique
   as `.form-select` above: the vendor's own custom properties pointed at
   same-origin copies of the exact vendor SVGs (`static/img/bs-*.svg`), paths
   relative to the CONSUMING vendor stylesheet (`../../img/`). Same
   selectors as bootstrap.min.css, loaded after it. Vendor file untouched,
   CSP not relaxed. */
.form-check-input:checked[type=checkbox]{--bs-form-check-bg-image:url("../../img/bs-check.svg")}
.form-check-input:checked[type=radio]{--bs-form-check-bg-image:url("../../img/bs-radio.svg")}
.form-check-input[type=checkbox]:indeterminate{--bs-form-check-bg-image:url("../../img/bs-indeterminate.svg")}
.form-switch .form-check-input{--bs-form-switch-bg:url("../../img/bs-switch-off.svg")}
.form-switch .form-check-input:focus{--bs-form-switch-bg:url("../../img/bs-switch-focus.svg")}
.form-switch .form-check-input:checked{--bs-form-switch-bg:url("../../img/bs-switch-on.svg")}
[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg:url("../../img/bs-switch-off-dark.svg")}
.accordion{--bs-accordion-btn-icon:url("../../img/bs-accordion.svg");--bs-accordion-btn-active-icon:url("../../img/bs-accordion-active.svg")}
[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon:url("../../img/bs-accordion-dark.svg");--bs-accordion-btn-active-icon:url("../../img/bs-accordion-dark.svg")}
