/* Toast Progress Animation - keyframes used by inline styles with dynamic duration */
@keyframes toast-progress {
  from {
    width: 100%;
  }
  to {
    width: 0%;
  }
}

/* ========================================================================
 * Page Header — shared layout for all main pages
 * Used together with components/page_header.html
 * ====================================================================== */
.page-header-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.25rem;
  /* THE HEADER PAINTS OVER WHAT FOLLOWS IT, and this is why.
   *
   * `.v-animate-fade-up` animates a transform and runs `forwards`, so every
   * card wearing it keeps a stacking context for good. The "?" popover's
   * z-50 is then trapped inside the header's own context, and the KPI row
   * below — same class, later in the DOM, z-index auto — painted straight
   * over the open panel. It read as a popover that opens half-clipped.
   *
   * 20, which is a layer of its own between the sticky tab strip (10) and the
   * top bar (30). It took two wrong answers to get here and both are worth
   * knowing: at 20 with the bar still on 10 the title and its buttons painted
   * over the search field; at 1 the tab strip - sticky, opaque, z-10 - ate the
   * first line of the panel. Neither was fixable without giving the top bar a
   * layer of its own, which is what topbar.html now does. */
  position: relative;
  z-index: 20;
}
.page-header-row > .page-header {
  min-width: 0;
  flex: 1 1 auto;
}
.page-header-row .page-header__actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-shrink: 0;
}
@media (max-width: 640px) {
  .page-header-row {
    flex-wrap: wrap;
    align-items: flex-start;
  }
  .page-header-row .page-header__actions {
    flex-wrap: wrap;
  }
}

/* ========================================================================
 * Page top spacing — enforce identical Y-position for every page title.
 * - Pages should NOT use -mt-* / -mx-* negative margins on their first
 *   child. Use this class instead if you need full-width subnavs.
 * - The wrapper inside #content-area carries pt-3 sm:pt-3 (12px);
 *   this rule is the one that wins, so change both together.
 * - 12px at every width. The wide breakpoint used to add 4px on top of a
 *   70px bar, so the first thing on the page started a fifth of the way down
 *   the viewport. The bar is 48px now and the gap does not need to grow with
 *   the screen: what grows with the screen is how many rows fit.
 * ====================================================================== */
.content-area > div { /* the px-4 pt-3 wrapper */
  padding-top: 0.75rem !important;
}
@media (min-width: 640px) {
  .content-area > div {
    padding-top: 0.75rem !important;
  }
}

/* GRC page rhythm (14-sep-2026, Nerea: "en GRC salen los títulos demasiado
   pegados al nav de arriba"). base.html stamps data-module on the wrapper.
   The shared 12px left the title 12-31px under the bar depending on each
   page's action buttons and its own mt-4 patches, and the tab rule 12px above
   the first card. One rhythm for the whole module:
     bar -> header row 20px, header row min 36px (the md button, so the h1
     sits at the same Y with or without actions), row -> tabs 12px,
     tab rule -> content 20px. Per-template mt-* on the header row is ignored. */
.content-area > div[data-module="grc"] {
  padding-top: 1.25rem !important;
}
.content-area > div[data-module="grc"] .page-header-row {
  margin-top: 0 !important;
  min-height: 2.25rem;
}
.content-area > div[data-module="grc"] .page-header-row--above-tabs {
  margin-bottom: 0.75rem;
}
.content-area > div[data-module="grc"] .page-tabs:not(.page-tabs--inline) {
  margin-bottom: 1.25rem;
}

/* Kill any first-child top margin so titles always start at the same Y */
.content-area > div > :first-child,
.content-area > div > [x-data]:first-child > :first-child {
  margin-top: 0 !important;
}
/* Defensive: don't allow first-child negative margins to break out */
.content-area > div > .-mt-4,
.content-area > div > .-mt-6,
.content-area > div > [x-data]:first-child > .-mt-4,
.content-area > div > [x-data]:first-child > .-mt-6 {
  margin-top: 0 !important;
}

/* Standard h1 sizing across all main pages — override Tailwind text-* to
   ensure visual consistency. Affects only top-level page titles, not h1s
   inside specific component contexts (which should use h2/h3 anyway). */
.content-area h1 {
  font-size: 1.25rem !important;   /* 20px — --vs-t-page */
  line-height: 1.3 !important;
  font-weight: 600 !important;     /* --vs-w-title; 700 is not in the scale */
}
/* Default color for h1 unless the page explicitly overrides it (e.g.,
   welcome hero on a dark gradient background uses text-white). */
.content-area h1:not([class*="text-white"]):not([class*="text-purple"]):not([class*="text-violet"]) {
  color: #111827;                   /* gray-900 */
}
/* Allow inline/contenteditable titles (vulnerability detail) to keep
   their hover/focus styles by leaving the box intact. */
.content-area h1[contenteditable] {
  outline: none;
}

/* When .page-tabs is the first element of the page wrapper, kill any
   accidental top margin from sibling animations / custom wrappers so the
   nav y-position is identical across all pages. */
.content-area .page-tabs:first-child,
.content-area > div > .page-tabs:first-child,
.content-area > div > div > .page-tabs:first-child,
.content-area > div > div:first-child > .page-tabs:first-child {
  margin-top: 0 !important;
}

/* ========================================================================
 * Back link — pill style, used at the top of every detail page.
 * Use: <a class="back-link"><i class="fa-solid fa-arrow-left"></i> Back</a>
 * ====================================================================== */
.back-link {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.375rem 0.75rem;
  margin-left: -0.75rem;       /* visually align text with title underneath */
  margin-bottom: 1.375rem;     /* breathing room before the title row */
  font-size: 0.75rem;          /* text-xs */
  font-weight: 500;
  color: #9ca3af;              /* gray-400 */
  border-radius: 0.5rem;
  transition: background-color 0.15s ease, color 0.15s ease, transform 0.15s ease;
  text-decoration: none;
}
.back-link i {
  font-size: 0.625rem;         /* 10px */
  transition: transform 0.15s ease;
}
.back-link:hover {
  background-color: color-mix(in srgb, var(--color-primary) 6%, white);
  color: var(--color-primary);
}
.back-link:hover i {
  transform: translateX(-2px);
}

/* ========================================================================
 * Page Tabs: the sub-nav. Text-first: a hairline rule, quiet labels, and one
 * 2px underline exactly as wide as the active label. Piloted on Audits as
 * `--minimal`, promoted to the base on 4 Sep 2026; the tinted-block and
 * `--flat` variants are gone.
 *
 * Three things that are not decoration:
 *  - Spacing lives in the strip's `gap`, not in each tab's horizontal
 *    padding, so the underline hugs the word instead of a block of air.
 *  - The strip is a scroll container (overflow-x: auto), which CLIPS
 *    anything outside its padding box. The accent is drawn INSIDE the tab
 *    (bottom: 0) and tabs carry no negative margin; the old `bottom: -2px`
 *    underline was invisible on every strip in the app for exactly this.
 *  - The background is the page's own colour, read from the page. A sticky
 *    strip in a colour that is nearly the canvas reads as a band with a
 *    line along its top.
 * ====================================================================== */
.page-tabs {
  display: flex;
  align-items: stretch;
  gap: 1.375rem;
  border-bottom: 1px solid #e9e7f0;
  margin-bottom: 0.75rem;
  padding-left: 2px;
  overflow-x: auto;
  scrollbar-width: none;
  /* Sticky to the top of the scroll container (<main>). Opaque on purpose:
     content scrolls UNDER it. z-10, under the page header (20) whose "?"
     panel opens over this strip, and under the top bar (30). */
  position: sticky;
  top: 0;
  z-index: 10;
  background: var(--page-bg, var(--color-background, #fbfaff));
}
.page-tabs::-webkit-scrollbar {
  display: none;
}

/* A strip that lives INSIDE a card, not at the top of a page. `.page-tabs` is
   sticky and paints the page background by construction — both are wrong on a
   card, where the strip would stick to the app's top bar and show the canvas
   colour over white. The tab itself (13px, underline the width of the word,
   var(--color-primary) when active) is untouched: an in-card audience switch
   and a page sub-nav are the same control. */
.page-tabs--inline {
  position: static;
  background: transparent;
  margin-bottom: 0;
}
/* Fade the edge that still has tabs behind it. The scrollbar is hidden, so
   without this a strip wider than the viewport reads as "these are all the
   tabs". The classes are toggled by VioletTabStrip; a strip that fits gets
   neither and stays flat. */
.page-tabs--fade-start {
  mask-image: linear-gradient(to right, transparent 0, #000 2.5rem);
}
.page-tabs--fade-end {
  mask-image: linear-gradient(to left, transparent 0, #000 2.5rem);
}
.page-tabs--fade-start.page-tabs--fade-end {
  mask-image: linear-gradient(to right, transparent 0, #000 2.5rem,
                              #000 calc(100% - 2.5rem), transparent 100%);
}
.page-tabs__tab {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.4375rem;
  padding: 0.4375rem 0 0.5rem;    /* no horizontal padding on purpose */
  font-size: 0.8125rem;           /* 13px */
  font-weight: 500;
  letter-spacing: -0.006em;
  color: #6b7280;
  white-space: nowrap;
  text-decoration: none;
  background: transparent;
  cursor: pointer;
  transition: color 0.16s ease;
}
.page-tabs__tab i {
  font-size: 0.75rem;             /* 12px, one size down and desaturated */
  color: #b7b3c6;
  transition: color 0.16s ease;
}
.page-tabs__tab::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  border-radius: 2px 2px 0 0;
  background: var(--color-primary);
  opacity: 0;
  transform: scaleX(0.55);
  transform-origin: center bottom;
  transition: opacity 0.18s ease,
              transform 0.22s cubic-bezier(0.34, 1.26, 0.64, 1);
}
.page-tabs__tab:hover {
  color: #111827;
}
.page-tabs__tab:hover i {
  color: #6b7280;
}
/* Hover states the intent without claiming the tab. */
.page-tabs__tab:hover::after {
  opacity: 0.22;
  transform: scaleX(1);
}
.page-tabs__tab--active {
  color: var(--color-primary);
  font-weight: 600;
}
.page-tabs__tab--active i {
  color: var(--color-primary);
}
.page-tabs__tab--active::after {
  opacity: 1;
  transform: scaleX(1);
}
.page-tabs__tab:focus-visible {
  outline: 2px solid color-mix(in srgb, var(--color-primary) 45%, transparent);
  outline-offset: -3px;
  border-radius: 6px;
}
.page-tabs__badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 17px;
  height: 17px;
  padding: 0 5px;
  font-size: 10px;
  font-weight: 600;
  border-radius: 9999px;
  background: #f1f0f6;
  color: #6b7280;
}
.page-tabs__tab--active .page-tabs__badge {
  background: color-mix(in srgb, var(--color-primary) 12%, white);
  color: var(--color-primary);
}
@media (prefers-reduced-motion: reduce) {
  .page-tabs__tab,
  .page-tabs__tab::after {
    transition: none;
  }
  .page-tabs__tab::after {
    transform: scaleX(1);
  }
}
/* A header that sits ON a tab strip: the strip's hairline is its rule, so
   the header keeps only enough air for the underline to breathe. */
.page-header-row--above-tabs {
  margin-bottom: 0.375rem;
}

/* ========================================================================
 * PR2 — Visual system unification
 * Standardize typography, cards, and spacing across all main pages.
 * ====================================================================== */

/* ── Typography scale ─────────────────────────────────────────────────── */
/* h2: section / card titles */
.content-area h2 {
  font-size: 0.9375rem;       /* 15px */
  line-height: 1.35;
  font-weight: 600;
}
.content-area h2:not([class*="text-white"]):not([class*="text-purple"]):not([class*="text-violet"]):not([class*="text-red"]):not([class*="text-blue"]):not([class*="text-emerald"]):not([class*="text-amber"]):not([class*="text-indigo"]):not([class*="text-pink"]) {
  color: #111827;              /* gray-900 — only when no explicit color override */
}
/* h3: subsection titles */
.content-area h3 {
  font-size: 0.8125rem;       /* 13px */
  line-height: 1.4;
  font-weight: 600;
}
.content-area h3:not([class*="text-white"]):not([class*="text-purple"]):not([class*="text-violet"]):not([class*="text-red"]):not([class*="text-blue"]):not([class*="text-emerald"]):not([class*="text-amber"]):not([class*="text-indigo"]):not([class*="text-pink"]) {
  color: #111827;
}
/* h4: micro labels (UPPERCASE eyebrow) */
.content-area h4 {
  font-size: 0.6875rem;       /* 11px — UPPERCASE only, see --vs-t-eyebrow */
  line-height: 1.3;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.content-area h4:not([class*="text-white"]):not([class*="text-purple"]):not([class*="text-violet"]):not([class*="text-red"]):not([class*="text-blue"]):not([class*="text-emerald"]):not([class*="text-amber"]):not([class*="text-indigo"]):not([class*="text-pink"]) {
  color: #6b7280;              /* gray-500 */
}

/* ── Standard card ─────────────────────────────────────────────────────
   Use class="card" instead of repeating
   "bg-white rounded-xl border border-gray-200 shadow-sm p-5". */
.card {
  background: #ffffff;
  border: 1px solid #e5e7eb;
  border-radius: var(--vs-radius, 6px);
  padding: 1.25rem;           /* p-5 */
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

/* ── TWO RADII, and this is where they are enforced ────────────────────────
   Measured on /violet/audits/ before this rule: SIX radii on one screen —
   6px used 211 times, 4px 177, 8px 139, plus 12px, the pill and the circle.
   Nothing chose them; each template picked a Tailwind rounding class by feel
   and the product ended up with six ideas of what a corner is.

   Rather than hunt `rounded-*` through 600 templates, the six classes are
   collapsed here onto the token. `rounded-full` is deliberately NOT in the
   list: the pill and the circle are the second radius, and they are what a
   lozenge, a chip, an avatar and a badge are made of.

   A component that genuinely needs its own corner opts out with
   `.no-radius-reset`, the same escape hatch the shadow rule below uses. */
.content-area .rounded,
.content-area .rounded-md,
.content-area .rounded-lg,
.content-area .rounded-xl,
.content-area .rounded-2xl,
.content-area .rounded-3xl {
  border-radius: var(--vs-radius, 6px) !important;
}
.content-area .rounded.no-radius-reset,
.content-area .rounded-md.no-radius-reset,
.content-area .rounded-lg.no-radius-reset,
.content-area .rounded-xl.no-radius-reset,
.content-area .rounded-2xl.no-radius-reset,
.content-area .rounded-3xl.no-radius-reset {
  border-radius: revert !important;
}
.content-area .shadow-md,
.content-area .shadow-lg,
.content-area .shadow-xl {
  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.04) !important; /* shadow-sm */
}
/* Exclude modals, dropdowns, and elements that legitimately need bigger shadows */
.content-area .shadow-md.no-card-reset,
.content-area .shadow-lg.no-card-reset,
.content-area .shadow-xl.no-card-reset {
  box-shadow: revert !important;
}

/* ── Soft background colors normalization ────────────────────────────────
   Reduce the noise of bg-*-50 backgrounds for non-status decoration. We
   keep red/orange/amber/green/yellow which carry semantic meaning, but
   neutralize indigo/blue/purple/pink/violet/teal/cyan that are pure decoration. */
/* (Soft normalization is left manual — auto-stripping risks breaking
   semantic widgets. Use card class instead in new code.) */

/* (Removed a duplicate `.page-header-row { margin-bottom }` rule here — it was
   already fully defined at the top of this file. Single source of truth.) */

/* ── Buttons (preview — will be expanded in PR3) ──────────────────────
   Don't redefine here yet; PR3 will introduce .btn-primary/.btn-secondary/.btn-ghost. */

/* ========================================================================
 * PR3 — Unified button system
 * Replaces the ~7-8 ad-hoc button variants with 3 canonical types
 * (primary, secondary, ghost) + 2 semantic modifiers (danger, success).
 * One size: the sm/lg/icon/group modifiers were never used by a single
 * template and are gone; components/button.html owns sizing.
 *
 * Usage:
 *   <button class="btn btn-primary">Save</button>
 *   <button class="btn btn-secondary"><i class="fa-solid fa-plus"></i> New</button>
 *   <button class="btn btn-ghost">Cancel</button>
 *   <button class="btn btn-primary btn-danger">Delete</button>
 * ====================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.4375rem 0.75rem;
  font-size: 0.8125rem;        /* 13px */
  font-weight: 500;
  line-height: 1;
  border-radius: var(--vs-radius, 6px);
  border: 1px solid transparent;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  transition: background-color 0.15s ease,
              border-color 0.15s ease,
              color 0.15s ease,
              box-shadow 0.15s ease,
              transform 0.05s ease;
  user-select: none;
}
.btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 25%, transparent);
}
.btn:disabled,
.btn.is-disabled {
  opacity: 0.55;
  cursor: not-allowed;
  pointer-events: none;
}
.btn:active:not(:disabled) {
  transform: translateY(0.5px);
}
.btn i {
  font-size: 0.75rem;          /* icon ~12px */
}

/* ── Primary (filled) ──────────────────────────────────────────────────
   Flat. No shadow at rest and none on hover: a brand-hued glow under the one
   filled button on the screen is decoration, and it ignores the tenant colour
   in a white-labelled install. The hover darkens the fill instead. */
.btn-primary {
  background-color: var(--color-primary);
  color: #ffffff;
  border-color: var(--color-primary);
}
.btn-primary:hover:not(:disabled) {
  background-color: color-mix(in srgb, var(--color-primary) 92%, black);
  border-color: color-mix(in srgb, var(--color-primary) 92%, black);
}

/* ── Secondary (outlined, neutral) ───────────────────────────────────── */
.btn-secondary {
  background-color: #ffffff;
  color: #374151;              /* gray-700 */
  border-color: #e5e7eb;        /* gray-200 */
}
.btn-secondary:hover:not(:disabled) {
  background-color: #f9fafb;    /* gray-50 */
  border-color: #d1d5db;        /* gray-300 */
  color: #111827;
}

/* ── Ghost (link-like) ───────────────────────────────────────────────── */
.btn-ghost {
  background-color: transparent;
  color: #6b7280;              /* gray-500 */
  border-color: transparent;
}
.btn-ghost:hover:not(:disabled) {
  background-color: color-mix(in srgb, var(--color-primary) 6%, white);
  color: var(--color-primary);
}

/* ── Semantic modifiers (combine with btn-primary or btn-secondary) ──
   Override the colors for destructive / positive actions. */
.btn-primary.btn-danger {
  background-color: #dc2626;    /* red-600 */
  border-color: #dc2626;
}
.btn-primary.btn-danger:hover:not(:disabled) {
  background-color: #b91c1c;    /* red-700 */
  border-color: #b91c1c;
}
.btn-secondary.btn-danger {
  color: #b91c1c;
  border-color: #fecaca;        /* red-200 */
}
.btn-secondary.btn-danger:hover:not(:disabled) {
  background-color: #fef2f2;    /* red-50 */
  border-color: #f87171;
  color: #991b1b;
}
.btn-ghost.btn-danger {
  color: #b91c1c;
}
.btn-ghost.btn-danger:hover:not(:disabled) {
  background-color: #fef2f2;
  color: #991b1b;
}

.btn-primary.btn-success {
  background-color: #16a34a;    /* green-600 */
  border-color: #16a34a;
}
.btn-primary.btn-success:hover:not(:disabled) {
  background-color: #15803d;
  border-color: #15803d;
}

/* ========================================================================
 * PR3 — Visual normalization for existing inline-class buttons
 * Auto-apply consistent radius, transition, focus state to any <button>
 * or <a> inside .content-area that visually behaves as a button.
 * ====================================================================== */

/* Any element that already uses a primary-coloured background. Radius and
   weight only: the primary button is flat, so this rule may not put a
   brand-tinted glow back under every hand-rolled one. */
.content-area button[class*="bg-[var(--color-primary)]"],
.content-area a[class*="bg-[var(--color-primary)]"]:not(.page-tabs__tab) {
  border-radius: 0.5rem !important;
  font-weight: 600 !important;
  transition: background-color 0.15s ease, transform 0.05s ease !important;
}
.content-area button[class*="bg-[var(--color-primary)]"]:active:not(:disabled),
.content-area a[class*="bg-[var(--color-primary)]"]:not(.page-tabs__tab):active {
  transform: translateY(0.5px);
}

/* Outlined / secondary buttons: white bg + gray border */
.content-area button[class*="bg-white"][class*="border-gray-200"]:not(.btn),
.content-area a[class*="bg-white"][class*="border-gray-200"]:not(.btn):not(.page-tabs__tab) {
  border-radius: 0.5rem !important;
  font-weight: 600 !important;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease !important;
}
.content-area button[class*="bg-white"][class*="border-gray-200"]:not(.btn):hover,
.content-area a[class*="bg-white"][class*="border-gray-200"]:not(.btn):not(.page-tabs__tab):hover {
  border-color: #d1d5db !important;
  background-color: #f9fafb !important;
}

/* Focus-visible accent for any interactive primary/secondary button */
.content-area button:focus-visible,
.content-area a[role="button"]:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 25%, transparent) !important;
}

/* ========================================================================
 * Inline-edit hover hints
 * Make HTMX-bound inputs/selects/textareas inside the content area visually
 * hint that they are editable. Subtle by default, prominent on hover/focus.
 * Affects every element with `hx-post` (the universal marker for inline edit).
 * ====================================================================== */
.content-area input[hx-post],
.content-area select[hx-post],
.content-area textarea[hx-post] {
  transition: border-color 0.15s ease,
              background-color 0.15s ease,
              box-shadow 0.15s ease;
}
.content-area input[hx-post]:hover,
.content-area select[hx-post]:hover,
.content-area textarea[hx-post]:hover {
  border-color: color-mix(in srgb, var(--color-primary) 35%, white) !important;
  background-color: color-mix(in srgb, var(--color-primary) 2%, white);
}
.content-area input[hx-post]:focus,
.content-area select[hx-post]:focus,
.content-area textarea[hx-post]:focus {
  outline: none;
  border-color: var(--color-primary) !important;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 15%, transparent) !important;
  background-color: #ffffff;
}

/* Pencil hint that appears on hover of editable rows */
.content-area [data-inline-edit]:hover::after {
  content: "\f303";
  font-family: "Font Awesome 6 Free";
  font-weight: 900;
  font-size: 9px;
  color: color-mix(in srgb, var(--color-primary) 50%, transparent);
  margin-left: 6px;
  vertical-align: middle;
}

/* Keyboard-focus highlight for vulnerability list rows (used by j/k nav) */
.content-area tr.kbd-focus {
  outline: 2px solid color-mix(in srgb, var(--color-primary) 50%, transparent);
  outline-offset: -2px;
  background-color: color-mix(in srgb, var(--color-primary) 4%, white);
}

/* ═══════════════════════════════════════════════════════════════════════════
   SECTIONED MODAL
   ───────────────────────────────────────────────────────────────────────────
   The layout of the task modal, lifted out so every large form modal can wear
   it: a context strip, a left section rail, a scrolling pane and a sticky save
   bar. Before this, the look lived as a 200-character Tailwind string repeated
   on each field of one template, so no other modal could reuse it.

   Not scoped to .content-area — modals render inside #modal-content, which is
   outside it.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Context strip: where am I, and what state is it in. */
.vs-modal-context {
  padding: 1rem 1.5rem .75rem;
  border-bottom: 1px solid #f3f4f6;
  /* White, not a tint. The modal header above it is already
     var(--vs-surface-2); a second shaded band directly under it reads as a
     second header rather than as the record's own context. */
  background: #ffffff;
}
.vs-modal-context__row {
  display: flex; align-items: center; flex-wrap: wrap;
  column-gap: .5rem; row-gap: .375rem; font-size: 11px;
}
.vs-modal-context__title {
  display: inline-flex; align-items: center; gap: .375rem;
  font-weight: 700; color: #111827;
}
.vs-modal-context__pill {
  padding: .125rem .5rem; border-radius: 9999px;
  font-size: 9px; font-weight: 700; text-transform: uppercase;
  background: #f3f4f6; color: #4b5563;
}
/* The ancestor link in the context strip — "‹ Work plan" above a milestone.
   The sweep deleted this rule as dead, correctly at the time: nothing in the
   product referenced it. It is back because something does now
   (violet/partials/_milestone_modal.html uses it twice), and a class with no
   rule renders an unstyled <button> in the middle of a modal header. If those
   two callers ever go, this goes with them. */
.vs-modal-context__back {
  display: inline-flex; align-items: center; gap: .25rem;
  font-weight: 600; color: #6b7280; transition: color .15s;
}
.vs-modal-context__back:hover { color: var(--color-primary); }

/* Body: rail + pane. Negative margin cancels the modal's own padding so the
   rail reaches the modal edges. */
.vs-modal-body {
  display: flex; flex-direction: column;
  margin-bottom: -1rem;
  /* A FIXED height, not min-height. With min-height the modal took the height
     of whichever section was open, so moving from a long section to a short one
     made the whole dialog jump and resize under the cursor. The frame must hold
     still; the pane scrolls instead. Applies to every sectioned modal, so they
     are all the same size as each other too. */
  height: 62vh;
}
@media (min-width: 640px) {
  .vs-modal-body {
    flex-direction: row; margin-bottom: -1.5rem;
    height: min(70vh, 680px);
  }
}
/* Also cancels the modal's horizontal padding, so the rail runs to the edges
   and its right border reads as a real divider rather than a floating line. */
.vs-modal-body--flush { margin-left: -1rem; margin-right: -1rem; }
@media (min-width: 640px) {
  .vs-modal-body--flush { margin-left: -1.5rem; margin-right: -1.5rem; }
}

/* Left rail. Horizontal scroller on phones, column from sm up. */
.vs-modal-nav {
  flex-shrink: 0; display: flex; gap: .25rem;
  padding: .5rem; overflow-x: auto;
  background: rgba(249, 250, 251, .5);
  border-bottom: 1px solid #e5e7eb;
}
@media (min-width: 640px) {
  .vs-modal-nav {
    width: 12rem; flex-direction: column;
    padding: 1rem .5rem;
    border-bottom: 0; border-right: 1px solid #e5e7eb;
  }
}
.vs-modal-nav__label {
  display: none;
  font-size: 9px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .1em; color: #9ca3af;
  padding: 0 .75rem; margin-bottom: .25rem;
}
@media (min-width: 640px) { .vs-modal-nav__label { display: block; } }
.vs-modal-nav__item {
  display: flex; align-items: center; gap: .625rem;
  padding: .5rem .75rem; border-radius: .5rem;
  font-size: 12px; font-weight: 500; text-align: left;
  white-space: nowrap; color: #4b5563;
  transition: background-color .15s, color .15s;
}
.vs-modal-nav__item i { width: 1rem; text-align: center; font-size: 10px; }
.vs-modal-nav__item:hover { background: #f3f4f6; }
.vs-modal-nav__item--active,
.vs-modal-nav__item--active:hover {
  background: color-mix(in srgb, var(--color-primary) 10%, transparent);
  color: var(--color-primary);
}

/* Right pane: scrolls on its own so the rail and save bar stay put. */
.vs-modal-pane {
  flex: 1 1 0%; min-width: 0;
  overflow-y: auto;
  display: flex; flex-direction: column;
}
.vs-modal-form { flex: 1 1 auto; display: flex; flex-direction: column; }
.vs-modal-content { flex: 1 1 auto; padding: 1.25rem; }
@media (min-width: 640px) { .vs-modal-content { padding: 1.5rem; } }

.vs-modal-section > * + * { margin-top: 1rem; }
.vs-modal-section__title {
  display: flex; align-items: center; gap: .5rem;
  font-size: 13px; font-weight: 600; color: #111827;
  margin-bottom: .25rem;
}
.vs-modal-section__title i { color: #9ca3af; font-size: 12px; }

/* Sticky save bar. */
.vs-modal-footer {
  position: sticky; bottom: 0; z-index: 5;
  display: flex; align-items: center; justify-content: flex-end; gap: .5rem;
  padding: .75rem 1.25rem;
  border-top: 1px solid #f3f4f6;
  /* Opaque, not translucent-and-blurred: the bar is part of the modal, not an
     overlay over it, and the blur cost a compositing layer on every large form. */
  background: #ffffff;
}
@media (min-width: 640px) { .vs-modal-footer { padding: .75rem 1.5rem; } }
/* With a link on the far left (e.g. "Full details"), the actions still sit
   right and the link takes the other end. Without the modifier the bar keeps
   its flex-end default, so adding the slot costs existing footers nothing. */
.vs-modal-footer--split { justify-content: space-between; }

/* ── The footer is pinned by the shell, for every modal ─────────────────────
 *
 * components/modal.html scrolls its body in `.vs-modal-scroll`. Any
 * `.vs-modal-footer` swapped into it (components/_modal_footer.html) sticks to
 * the bottom of the dialog while the body scrolls above it, whatever the modal
 * is: a form, a read-only record, a sectioned rail + pane.
 *
 * Three things make that hold:
 *  - the shell drops its bottom padding when a footer is present, because a
 *    sticky element stops at the scroller's padding edge and the content
 *    would show through the strip below the bar;
 *  - the bar bleeds over the shell's side padding, so nothing scrolls past
 *    it in the gutters;
 *  - `.vs-modal-body` no longer needs its negative bottom margin there.
 *
 * Contract for a template: the footer must be a child of the element that
 * wraps the whole body (the form, or the top-level wrapper), never of a short
 * inner row, or `sticky` has no room to stick. `_check_modal_footer.py` holds
 * it. A footer inside an inner scroller (`.vs-modal-pane`) keeps no bleed. */
.vs-modal-scroll:has(.vs-modal-footer:not([style*="display: none"])) { padding-bottom: 0; }
.vs-modal-scroll:has(.vs-modal-footer:not([style*="display: none"])) .vs-modal-body { margin-bottom: 0; }
.vs-modal-scroll .vs-modal-footer { margin-left: -1rem; margin-right: -1rem; margin-top: 1.25rem; }
@media (min-width: 640px) {
  .vs-modal-scroll .vs-modal-footer { margin-left: -1.5rem; margin-right: -1.5rem; }
}
.vs-modal-scroll .vs-modal-body + .vs-modal-footer,
.vs-modal-scroll .vs-modal-pane .vs-modal-footer { margin-top: 0; }
.vs-modal-scroll .vs-modal-pane .vs-modal-footer { margin-left: 0; margin-right: 0; }

/* Child layer (components/modal.html, #global-modal-child): above the parent
   (z-50) and a little shorter, so the parent's edges read as underneath. */
.vs-modal-child { z-index: 60; }
.vs-modal-child > div { max-height: 86vh; }
.vs-modal-child .vs-modal-body { height: min(62vh, 600px); }

/* ── A tall editor inside the modal shell (.vs-editor-pane) ──────────────
 *
 * NOT `.vs-modal-pane`: that name is already taken by the rail+panel canon
 * above, where it means "the scrolling right-hand pane". Two meanings on one
 * class is how a rule reaches a modal nobody was thinking about.
 *
 * The report export editor pins its two panes to a height, because a list you
 * drag rows around in and a live preview beside it both need a stable box.
 * It used to pin them to `54vh` — a fraction of the WINDOW, which has nothing
 * to do with the space the shell actually leaves: the panel is capped at 90vh
 * and inside it sit the modal header, the body padding, the report-status
 * strip, the template row and the sticky footer. 54vh always overshot, so the
 * body scrolled and the bottom of both panes sat behind the opaque footer —
 * 75px of them at a 640px-tall window, measured.
 *
 * `min()` keeps the 54vh intent where there is room and hands back the shell's
 * chrome where there is not. 20rem is that chrome, measured against the real
 * modal: 65px header + 48px body padding + 72px status strip + 54px template
 * row + 53px footer + the gaps between them.
 *
 * The floor is written into the same expression rather than as `min-height`,
 * so it cannot argue with the `min-h-0` these panes carry for their inner
 * scrolling. Below the floor, scrolling the modal body is the honest answer.
 */
.vs-editor-pane { height: max(16rem, min(54vh, calc(90vh - 20rem))); }

/* Tones for the context strip. One name per meaning so callers pass a state,
   not a colour — "done" stays green everywhere without anyone remembering it. */
.vs-modal-context__pill--done    { background: #d1fae5; color: #047857; }
.vs-modal-context__pill--active  { background: #dbeafe; color: #1d4ed8; }
.vs-modal-context__pill--danger  { background: #fee2e2; color: #b91c1c; }
.vs-modal-context__pill--warning { background: #fef3c7; color: #b45309; }
.vs-modal-context__pill--muted   { background: #f3f4f6; color: #4b5563; }

/* Tom Select dropdowns are rendered on <body> (see user_picker.js) so that a
   card or table cell with overflow-hidden cannot clip them. Once detached they
   no longer inherit the stacking context of whatever opened them, so they need
   a z-index above the global modal (z-50) and the nested overlays (z-70/z-80).
   Without this a picker inside a modal drops its list *behind* the modal.

   `position` and the geometry are set by violetAnchorPanel at open time, in
   viewport coordinates — Tom Select's own absolute placement is computed once
   and is wrong the moment a scrolling pane moves under it. `!important` on
   nothing here: the helper writes inline styles, which already win. */
.ts-dropdown { z-index: 10050; }
/* The bump the shared helper applies while it is anchoring, so the panel never
   flashes at Tom Select's own coordinates before being moved. */
.ts-dropdown.vs-anchored { position: fixed; }

/* ── File field ──────────────────────────────────────────────────────────
   The native control's button and "no file chosen" status are browser chrome
   in the browser's own language, and CSS cannot change either string — only
   ::file-selector-button is reachable, and only for styling. So the native
   chrome is clipped away and file_input.js renders the control instead.

   Clipped rather than display:none: a hidden input is skipped by constraint
   validation in some browsers, which would silently break `required`. */
.vs-file {
  display: flex; align-items: center; gap: .5rem;
  min-width: 0;
}
.vs-file__native {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap; border: 0;
}
.vs-file__button {
  display: inline-flex; align-items: center; gap: .5rem;
  flex-shrink: 0;
  padding: .5rem .875rem;
  border: 1px solid #e5e7eb; border-radius: .5rem;
  background: #fff;
  font-size: 13px; font-weight: 500; color: #374151;
  cursor: pointer;
  transition: border-color .15s, background .15s;
}
.vs-file__button:hover { background: #f9fafb; border-color: #d1d5db; }
.vs-file__button:focus-visible {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 15%, transparent);
}
.vs-file__button i { font-size: 11px; color: var(--color-primary); }
.vs-file__name {
  min-width: 0; flex: 1 1 auto;
  font-size: 13px; color: #374151;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.vs-file__name--empty { color: #9ca3af; }
.vs-file__clear {
  flex-shrink: 0;
  width: 1.5rem; height: 1.5rem;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: .375rem;
  background: transparent; color: #9ca3af;
  font-size: 11px; cursor: pointer;
}
.vs-file__clear:hover { background: #f3f4f6; color: #ef4444; }

/* ── Filter controls ─────────────────────────────────────────────────────
   ONE SHAPE FOR EVERY CONTROL IN A FILTER BAR.

   A bar could hold four different controls and show four different shapes: the
   search box was `border-gray-200 rounded-lg`, the date boxes and the selects
   were `border-gray-300 rounded-md`, and the person picker was whatever Tom
   Select ships — a different height again, with its own border and radius. Five
   fields in a row, none of them lining up.

   Each was defensible on its own; together they are why a filter bar "no tiene
   sentido". So the metrics live here, once, and the three families of control
   all wear them: native <input>, native <select>, and Tom Select's generated
   `.ts-control`.

   Height is set explicitly rather than left to padding + line-height, because
   Tom Select's inner input and a native select compute those differently and
   would still end up a pixel or two apart. */
:root {
  --vs-filter-h: 2rem;             /* 32px — --vs-control-h, one control height */
  --vs-filter-radius: var(--vs-radius, 6px);
  --vs-filter-border: #d1d5db;     /* gray-300 */
  /* The narrowest a filter box may get. Below this "All Audits" is "All Au…"
     and the box says nothing; the grid adds a row instead of taking a pixel. */
  --vs-filter-min: 150px;
}

.vs-filter-control,
.vs-filter .ts-control {
  width: 100%;
  min-height: var(--vs-filter-h);
  height: var(--vs-filter-h);
  padding: 0 0.75rem;
  border: 1px solid var(--vs-filter-border);
  border-radius: var(--vs-filter-radius);
  background-color: #fff;
  font-size: 13px;
  line-height: calc(var(--vs-filter-h) - 2px);
  color: #111827;
  transition: border-color .15s, box-shadow .15s;
}
/* The search box carries its own magnifier, so it needs the room for it. */
.vs-filter-control--search { padding-left: 2rem; }
/* A native select draws its own arrow; a date input its own calendar icon. */
.vs-filter-control--select { padding-right: 2rem; }

.vs-filter-control:focus,
.vs-filter .ts-control.focus,
.vs-filter .ts-wrapper.focus .ts-control {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent);
}
.vs-filter-control:disabled,
.vs-filter-control[readonly] {
  background-color: #f3f4f6;
  color: #6b7280;
  cursor: not-allowed;
}
.vs-filter-control::placeholder { color: #9ca3af; }

/* Tom Select builds a wrapper, an inner input and an item; each needs to be
   told not to reintroduce its own vertical rhythm. */
.vs-filter .ts-wrapper {
  /* `min-height` alone let this grow. Every other control on the row is
     pinned to --vs-filter-h, so the person picker was the one box that could
     be taller than its neighbours — and because the row centres its cells, a
     taller box also pushed its own label up out of line with the rest. It is
     the single most visible break in the filter bar. Pin it like the others;
     `max-height` too, because Tom Select's plugins (remove_button, the multi
     variant) add padding at a higher specificity than this file can reach. */
  min-height: var(--vs-filter-h);
  height: var(--vs-filter-h);
  max-height: var(--vs-filter-h);
}
/* The control fills the wrapper rather than sizing itself. */
.vs-filter .ts-wrapper > .ts-control { height: 100%; }

/* TWO BOXES. Tom Select COPIES the original <select>'s class list onto the
   wrapper it builds. So a select carrying `.vs-filter-control` produced the
   box twice: once on the wrapper, once on the `.ts-control` inside it — a
   bordered field with a second bordered field sitting next to its text, on
   every filter bar in the product.
   The wrapper is a positioning shell, so it is stripped back to nothing and
   the inner control stays the only visible box. */
.vs-filter .ts-wrapper.vs-filter-control,
.vs-filter .ts-wrapper.vs-filter-control--select {
  border: 0;
  padding: 0;
  background: transparent;
  box-shadow: none;
  border-radius: 0;
}
.vs-filter .ts-control > input,
.vs-filter .ts-control > .item {
  font-size: 13px;
  line-height: calc(var(--vs-filter-h) - 2px);
  margin: 0;
  padding: 0;
}
.vs-filter .ts-control::after { display: none; }

/* One label shape above them all. */
.vs-filter-label {
  display: block;
  margin-bottom: .125rem;
  font-size: 11px;
  font-weight: 500;
  color: #6b7280;
}
.vs-filter-label i { color: #9ca3af; margin-right: .25rem; font-size: 10px; }

/* ── Filter bar layout ─────────────────────────────────────────────────────
   The bar used to be a six-column grid at every desktop width with the
   Group / Export / Columns / Saved views cluster sitting BESIDE it. On a
   1500px window the cluster took 600px, the thirteen Findings filters shared
   the rest, and every box read "All Au…".

   Now:
   - the form and the action cluster sit on one flex line that WRAPS: the
     cluster stays beside the form only when the form keeps 36rem, and drops
     under it (right-aligned, one row, same buttons in the same order) when
     it cannot;
   - the grid is auto-fill on --vs-filter-min, so a narrower bar means fewer
     columns and one more row, never a narrower box;
   - the search box spans two columns;
   - from the ninth filter on, cells fold behind "More filters (n)". A cell
     whose control carries a value is `--active` and never folds: a filter
     that is narrowing the list must stay on screen. The grid remembers
     open/closed per page (filters.html, localStorage). */
.vs-filter__row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.75rem;
}
.vs-filter__form {
  flex: 1 1 36rem;
  min-width: 0;
}
.vs-filter__actions {
  flex: 0 0 auto;
  margin-left: auto;
}
.vs-filter__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(var(--vs-filter-min), 100%), 1fr));
  gap: 0.5rem;
  align-items: end;
}
.vs-filter__cell { min-width: 0; }
.vs-filter__cell--search { grid-column: span 2; }
/* Fold. `--search` says the first cell is the search box, which is not a
   filter: the count starts after it. */
.vs-filter__grid:not(.vs-filter__grid--open):not(.vs-filter__grid--search)
  > .vs-filter__cell:nth-child(n+9):not(.vs-filter__cell--active),
.vs-filter__grid--search:not(.vs-filter__grid--open)
  > .vs-filter__cell:nth-child(n+10):not(.vs-filter__cell--active) {
  display: none;
}
/* The disclosure sits in the grid as one more cell, bottom-aligned with the
   controls beside it. */
.vs-filter__more {
  display: flex;
  align-items: flex-end;
  min-width: 0;
}
.vs-filter__more--open i:last-child { transform: rotate(180deg); }
@media (max-width: 639.98px) {
  .vs-filter__grid {
    grid-template-columns: 1fr;
    gap: 0.75rem;
  }
  .vs-filter__cell--search { grid-column: 1 / -1; }
  .vs-filter__actions {
    flex: 1 1 100%;
    flex-wrap: wrap;
    margin-left: 0;
  }
}

/* ==========================================================================
 * CHIP — one shape for every piece of record metadata
 * ==========================================================================
 * The Finding header carried six chips in one row and six different shapes:
 * one with a coloured dot, one with an icon plus a tiny uppercase "IMPACT"
 * prefix, one with a dashed border, one solid, some with a chevron and some
 * without, and three different vertical paddings so they did not even line up.
 * Every one was twelve hand-written utility classes, and no two agreed.
 *
 * That is the tell. A person designing this builds one control and reuses it;
 * only a machine writes the same idea twelve ways. So: one definition here,
 * and the markup names it.
 *
 * What is allowed to vary is the COLOUR, because colour carries meaning here
 * (severity, status). What is not allowed to vary is the shape.
 *
 * Editability is signalled one way and one way only: a chevron. A chip without
 * one is not clickable, and a chip with one is.
 */
.vs-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  height: 26px;                     /* fixed, so a row of chips has one baseline */
  padding: 0 0.625rem;
  border-radius: 6px;               /* not a pill: a pill next to pill-shaped
                                       buttons makes both read as buttons */
  border: 1px solid transparent;
  font-size: 12px;
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
  background: #f3f4f6;
  color: #374151;
  border-color: #e5e7eb;
}
button.vs-chip, a.vs-chip { cursor: pointer; transition: filter .15s ease; }
button.vs-chip:hover, a.vs-chip:hover { filter: brightness(0.96); }
button.vs-chip:focus-visible, a.vs-chip:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 1px;
}
/* The one affordance that says "this opens". Nothing else may say it. */
.vs-chip__caret { font-size: 8px; opacity: .55; transition: transform .15s ease; }
.vs-chip[aria-expanded="true"] .vs-chip__caret { transform: rotate(180deg); }
/* Leading icon, when the chip needs one to be legible at a glance. */
.vs-chip > i:first-child { font-size: 10px; opacity: .75; }
/* A quiet prefix for chips whose value needs naming ("IMPACT: Medium").
   11px/.06em rather than 10px/.04em, so it is the same eyebrow the metric
   labels, the table headers and the filter labels use. It reads better AND
   sits quieter, which is the whole job of a label. */
.vs-chip__label {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .06em;
  opacity: .62;
}

/* Tones. Severity keeps its own scale; everything else is one of three. */
.vs-chip--very-critical { background: #fecaca; color: #7f1d1d; border-color: #f87171; }
.vs-chip--critical { background: #fee2e2; color: #991b1b; border-color: #fecaca; }
.vs-chip--high     { background: #ffedd5; color: #9a3412; border-color: #fed7aa; }
.vs-chip--medium   { background: #fef3c7; color: #92400e; border-color: #fde68a; }
.vs-chip--low      { background: #dcfce7; color: #166534; border-color: #bbf7d0; }
.vs-chip--info     { background: #eef2ff; color: #3730a3; border-color: #e0e7ff; }
.vs-chip--neutral  { background: #f3f4f6; color: #374151; border-color: #e5e7eb; }
.vs-chip--success  { background: #dcfce7; color: #166534; border-color: #bbf7d0; }
.vs-chip--warn     { background: #fef3c7; color: #92400e; border-color: #fde68a; }
.vs-chip--danger   { background: #dc2626; color: #ffffff; border-color: #dc2626; }
.vs-chip--brand    {
  background: color-mix(in srgb, var(--color-primary) 10%, white);
  color: var(--color-primary);
  border-color: color-mix(in srgb, var(--color-primary) 25%, white);
}
/* Monospace variant for identifiers (CVE, codes) — same shape, different face. */
.vs-chip--mono { font-family: "JetBrains Mono", ui-monospace, Menlo, monospace; font-weight: 500; }


/* =============================================================================
 * LOZENGES, INLINE CONTROLS, MODAL ASIDE — the record anatomy
 * ========================================================================== */
/* A status is a small lozenge: 11 px, medium weight, no uppercase, a tint at
   most. The old pills were bold uppercase blocks that shouted over the row. */
.vs-lozenge {
  display: inline-flex; align-items: center; gap: .25rem;
  padding: 2px 8px; border-radius: 4px;
  font-size: 11px; font-weight: 600; line-height: 16px;
  text-transform: none; letter-spacing: 0; white-space: nowrap;
}
.vs-lozenge--neutral  { background: #f3f4f6; color: #4b5563; }
.vs-lozenge--info     { background: #dbeafe; color: #1d4ed8; }
.vs-lozenge--ok       { background: #dcfce7; color: #15803d; }
.vs-lozenge--medium   { background: #fef3c7; color: #a16207; }
.vs-lozenge--critical { background: #fee2e2; color: #b91c1c; }
.vs-lozenge--very-critical { background: #fecaca; color: #7f1d1d; }
/* The status control of a record: a lozenge that happens to be a select. */
.vs-lozenge-select {
  appearance: none; -webkit-appearance: none;
  padding: 3px 24px 3px 10px; border-radius: 4px; border: 1px solid transparent;
  font-size: 12px; font-weight: 600; line-height: 16px; cursor: pointer;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 20 20'><path d='M5 8l5 5 5-5' fill='none' stroke='currentColor' stroke-width='2'/></svg>");
  background-repeat: no-repeat; background-position: right 8px center;
}
.vs-lozenge-select:focus { outline: none; box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-primary) 25%, transparent); }
/* A field that saves itself. It used to be invisible until hovered — a
   border of `transparent` — so a panel of eight editable fields read as eight
   lines of text and nobody could tell what could be changed. A control looks
   like a control at rest; what it does NOT do is shout, so the frame is the
   same hairline every input in the product uses. */
.vs-inline-select {
  width: 100%; font-size: 13px; color: #111827; background: #fff;
  border: 1px solid #e5e7eb; border-radius: 6px; padding: 5px 8px;
  transition: border-color .15s, box-shadow .15s;
}
.vs-inline-select:hover { border-color: #d1d5db; }
.vs-inline-select:focus {
  outline: none; border-color: var(--color-primary);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent);
}
select.vs-inline-select {
  cursor: pointer; appearance: none; -webkit-appearance: none; padding-right: 26px;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 20 20'><path d='M5 8l5 5 5-5' fill='none' stroke='%236b7280' stroke-width='2'/></svg>");
  background-repeat: no-repeat; background-position: right 9px center;
}
/* A date field keeps the browser's own picker button, so it must not be
   pushed under the chevron this rule adds to selects. */
input.vs-inline-select[type="date"] { padding-right: 8px; }
/* The right column of a record modal: the facts, labelled. */
.vs-modal-aside {
  flex-shrink: 0; overflow-y: auto;
  padding: 1.25rem 1.25rem 1.5rem;
  background: #fafafa; border-top: 1px solid #e5e7eb;
}
@media (min-width: 640px) {
  .vs-modal-aside { width: 19rem; border-top: 0; border-left: 1px solid #e5e7eb; }
}
.vs-modal-aside__title {
  font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: #6b7280; margin-bottom: .75rem;
}
.vs-modal-aside__list > div { margin-bottom: .875rem; }
.vs-modal-aside__list dt { font-size: 11px; color: #6b7280; margin-bottom: 2px; }
.vs-modal-aside__list dd { margin: 0; font-size: 13px; color: #111827; }

/* Inside the Details column every control wears the product's field frame,
   the hairline of .vs-inline-select. It used to be quiet at rest, but only the
   native controls obeyed: dates and the owner read as plain text while the
   Tom Select ones kept their box, so one column mixed two looks. One frame. */
.vs-modal-aside select:not(.vs-lozenge-select),
.vs-modal-aside input:not([type="checkbox"]):not([type="file"]):not([type="hidden"]),
.vs-modal-aside .ts-wrapper .ts-control {
  font-size: 13px; color: #111827; background: #fff;
  border: 1px solid #e5e7eb; border-radius: 6px; padding: 5px 8px; min-height: 32px;
  box-shadow: none; transition: border-color .15s, box-shadow .15s;
}
.vs-modal-aside select:not(.vs-lozenge-select):hover,
.vs-modal-aside input:not([type="checkbox"]):not([type="file"]):not([type="hidden"]):hover,
.vs-modal-aside .ts-wrapper .ts-control:hover { border-color: #d1d5db; background: #fff; }
.vs-modal-aside select:focus,
.vs-modal-aside input:focus,
.vs-modal-aside .ts-wrapper.focus .ts-control {
  outline: none; border-color: var(--color-primary); background: #fff;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent);
}
/* A framed control takes its row and lines up with the label above it. */
.vs-modal-aside dd > select,
.vs-modal-aside dd > input:not([type="checkbox"]):not([type="file"]):not([type="hidden"]),
.vs-modal-aside dd > .ts-wrapper { width: 100%; margin-left: 0; }
.vs-modal-aside dd > .ts-wrapper .ts-control { width: 100%; margin-left: 0; }
.vs-modal-aside dd > .flex > input { width: 5.5rem; flex: 0 0 auto; }
/* Tom Select copies the select's classes to its wrapper, so a
   `vs-inline-select` enhanced by data-select drew the frame twice: once on the
   wrapper and once on the control inside it (the box inside a box of the task
   modal's Findings and Dependencies). The control carries the frame. */
.ts-wrapper.vs-inline-select { border: 0 !important; padding: 0 !important; background: transparent !important; box-shadow: none !important; }
/* Same box-in-a-box on a full-size form field: a select framed with utility
   classes and enhanced by data-select. Mark it `vs-ts-field`; the control
   inside keeps the one frame. */
.ts-wrapper.vs-ts-field { border: 0 !important; padding: 0 !important; background: transparent !important; box-shadow: none !important; }
/* The multi-select of a record modal (a task's related findings): a real
   field height and readable text. As a bare Tom Select it collapsed to an
   empty 8px strip with 11px text. */
.ts-wrapper.vs-multi-select .ts-control {
  min-height: 38px !important; padding: 6px 8px !important; font-size: 14px !important;
  border: 1px solid #e5e7eb !important; border-radius: 6px !important; background: #fff !important;
}
.ts-wrapper.vs-multi-select .ts-control input { font-size: 14px !important; min-width: 8rem; }
.ts-wrapper.vs-multi-select .ts-control .item { font-size: 13px; padding: 2px 6px; border-radius: 4px; }
.ts-wrapper.vs-multi-select.focus .ts-control { border-color: var(--color-primary) !important; }
.ts-dropdown .option { font-size: 14px; }
/* The Coverage tab's testing-guide picker: a real field, full width of its
   box, the guide's name in body size and never clipped to "CIS Kubern…". */
.vs-guide-picker .ts-wrapper, .vs-guide-picker select { width: 100% !important; }
.vs-guide-picker .ts-wrapper .ts-control {
  min-height: 36px !important; padding: 6px 30px 6px 10px !important; font-size: 13px !important; font-weight: 600;
  border: 1px solid #e5e7eb !important; border-radius: 6px !important; background: #fff !important; box-shadow: none !important;
}
.vs-guide-picker .ts-wrapper .ts-control .item { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.vs-guide-picker .ts-wrapper:hover .ts-control { border-color: #d1d5db !important; }
.vs-guide-picker .ts-wrapper.focus .ts-control { border-color: var(--color-primary) !important; box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent) !important; }
.vs-guide-picker + .ts-dropdown, .ts-dropdown.vs-inline-select { min-width: 22rem; }
/* The person picker is Tom Select, and base.html frames it with !important
   for every form in the app. Inside the Details column it wears the same
   frame and height as the native controls next to it, so the override is
   !important too. */
.vs-modal-aside .ts-wrapper.single .ts-control,
.vs-modal-aside .ts-wrapper.single .ts-control:hover {
  background-color: #fff !important; border: 1px solid #e5e7eb !important;
  border-radius: 6px !important; padding: 5px 28px 5px 8px !important;
  font-size: 13px !important; line-height: 20px !important; min-height: 32px !important;
  box-shadow: none !important;
}
.vs-modal-aside .ts-wrapper.single .ts-control:hover { border-color: #d1d5db !important; background-color: #fff !important; }
.vs-modal-aside .ts-wrapper.single.focus .ts-control,
.vs-modal-aside .ts-wrapper.single.input-active .ts-control {
  border-color: var(--color-primary) !important; background-color: #fff !important; box-shadow: none !important;
}
.vs-modal-aside .ts-wrapper.single .ts-control::after { right: 10px; }
.vs-modal-aside__list > div { padding-bottom: .5rem; margin-bottom: .5rem; border-bottom: 1px solid #f3f4f6; }
.vs-modal-aside__list > div:last-child { border-bottom: 0; }
.vs-modal-aside__list dt { font-size: 11px; color: #6b7280; margin-bottom: 3px; letter-spacing: .01em; }
/* Two short facts on one row (start and due, priority and effort). */
.vs-modal-aside__pair { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: .625rem; }
.vs-modal-aside .text-\[11px\].text-gray-400 { color: #9ca3af; }
/* The context bar of a record: one line, the type on the left, the state on the right. */
.vs-modal-context { padding: .75rem 1.5rem; border-bottom: 1px solid #e5e7eb; }
.vs-modal-context__row { font-size: 12px; }
.vs-modal-context__eyebrow { font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: #6b7280; }
/* Big editable name in the content column */
.vs-record-title { width: 100%; font-size: 18px; font-weight: 600; color: #111827; letter-spacing: -.01em; background: transparent; border: 1px solid transparent; border-radius: 6px; padding: 4px 8px; margin-left: -8px; }
.vs-record-title:hover { border-color: #e5e7eb; }
.vs-record-title:focus { outline: none; border-color: var(--color-primary); background: #fff; }
.vs-record-text { width: 100%; font-size: 13px; color: #374151; line-height: 1.6; background: transparent; border: 1px solid transparent; border-radius: 6px; padding: 6px 8px; margin-left: -8px; resize: vertical; }
.vs-record-text:hover { border-color: #e5e7eb; }
.vs-record-text:focus { outline: none; border-color: var(--color-primary); background: #fff; }
/* A section inside a record modal (task, work plan). It was an uppercase
   tracked eyebrow, and a modal carries four of them at once: four small grey
   capitals down one pane read as a template, not as a document. A section
   heading names its section in the product's own voice and draws a hairline
   under it, the same way a panel head does on a page. */
.vs-record-section {
  font-size: 12px; font-weight: 600; letter-spacing: -0.006em; color: #111827;
  margin-bottom: .5rem; padding-bottom: .3rem; border-bottom: 1px solid #ece9f5;
}

/* ── Drill affordance ─────────────────────────────────────────────────────
   Any `[data-drill-url]` gets `.vs-drill` from violet-drill.js: a pointer, a
   hairline on hover and a visible focus ring. Cards (`data-drill-card`) show
   the rule on their border; text and numbers get an underline the width of
   the figure. No colour change: the number keeps the colour that means
   something, and the hover only says "this opens something". */
.vs-drill { cursor: pointer; }
.vs-drill:not(.vs-drill--card):hover {
  text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px;
  text-decoration-color: color-mix(in srgb, currentColor 55%, transparent);
}
.vs-drill--card:hover { border-color: color-mix(in srgb, var(--color-primary) 45%, #e5e7eb); }
.vs-drill:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; border-radius: 6px; }
canvas.vs-drill:not(.vs-drill--card):hover { text-decoration: none; }


/* ══════════════════════════════════════════════════════════════════════════
 * MENU — one shape for every popover that drops out of the top bar
 * ══════════════════════════════════════════════════════════════════════════
 * The panel lives in components/_menu_popover.html; these are its rows. Both
 * are needed: the panel so two menus cannot drift apart in shape, the classes
 * so they cannot drift apart in the row.
 *
 * The rule the rows encode, which is the same rule the sidebar follows: a
 * 34px row, a 16px MONOCHROME glyph, 12px of clear space, a 13px label — and
 * the accent only on hover and focus. The profile menu used to paint its
 * glyphs yellow, blue and red by hand, which made a menu of six links look
 * like a menu of six unrelated warnings.
 */
.vs-menu__hd {
  padding: 10px 12px;
  border-bottom: 1px solid #f3f4f6;
}
.vs-menu__label {
  display: block;
  padding: 8px 12px 4px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: #9ca3af;
}
.vs-menu__row {
  display: flex;
  align-items: center;
  width: 100%;
  height: 34px;
  padding: 0 12px;
  font-size: 13px;
  font-weight: 500;
  color: #374151;
  text-align: left;
  text-decoration: none;
  background: transparent;
  border: 0;
  cursor: pointer;
  transition: background-color .15s ease, color .15s ease;
}
/* The leading slot is whatever comes first — a glyph in every row, including
   the language menu, which used to put a flag emoji here and so drew the one
   coloured column in three monochrome menus — pinned to one width so every
   label starts on the same vertical line, and the label always 12px clear. */
.vs-menu__row > :first-child {
  width: 20px;
  font-size: 16px;
  line-height: 1;
  text-align: center;
  flex-shrink: 0;
  color: #9ca3af;                 /* monochrome, always */
  transition: color .15s ease;
}
.vs-menu__row > :first-child + * { margin-left: 12px; }
.vs-menu__row:hover,
.vs-menu__row:focus-visible {
  background: var(--vs-surface-2, #f9fafb);
  color: var(--color-primary);
  outline: none;
}
.vs-menu__row:hover > i:first-child,
.vs-menu__row:focus-visible > i:first-child { color: var(--color-primary); }
.vs-menu__row--on {
  background: color-mix(in srgb, var(--color-primary) 10%, white);
  color: var(--color-primary);
  font-weight: 600;
}
.vs-menu__row--on > :first-child { color: var(--color-primary); }
/* The right-hand slot: a chevron, a shortcut chip, a version, a tick. */
.vs-menu__right {
  margin-left: auto;
  padding-left: 12px;
  font-size: 11px;
  color: #9ca3af;
  flex-shrink: 0;
}
.vs-menu__sep { height: 1px; margin: 4px 0; background: #f3f4f6; }

/* The sidebar scrolls, and the scrollbar stays out of sight. The rail is a
   list you scroll with the wheel; the thumb painted a dark line down its
   right edge on every screen whose nav is taller than the viewport, which is
   most of them once a module is open. Same treatment as .vs-rail__track. */
/* The sidebar nav keeps its scrollbar: a visible bar tells the person there is
   more below, which a hidden one does not. Thin and light, so it reads as part
   of the lilac surface instead of the dark default the browser paints. */
.sidebar-scroll { scrollbar-width: thin; scrollbar-color: #d9d4e6 transparent; }
.sidebar-scroll::-webkit-scrollbar { width: 6px; }
.sidebar-scroll::-webkit-scrollbar-track { background: transparent; }
.sidebar-scroll::-webkit-scrollbar-thumb { background: #d9d4e6; border-radius: 3px; }

/* A quiet input: transparent at rest, hairline on hover, accent on focus.
   The Milestones panel edits twelve dates and twelve comments in place; a
   bordered box on each would draw 24 frames around a table. */
.vs-quiet-input {
  background: transparent; border: 1px solid transparent; border-radius: 6px;
  padding: 2px 6px; line-height: 1.3; outline: none; transition: border-color .12s ease;
}
.vs-quiet-input:hover { border-color: #e5e7eb; background: #fff; }
.vs-quiet-input:focus { border-color: var(--color-primary); background: #fff; box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-primary) 18%, transparent); }

/* Tom Select group headers as the product's eyebrow, and a taller list: the
   timer's task picker groups tasks by work plan, and the group name used to
   look like one more task. */
.ts-dropdown .optgroup-header { font-size: 10.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: #6b7280; background: #f9fafb; padding: 6px 10px 4px; border-top: 1px solid #f3f4f6; }
.ts-dropdown .optgroup:first-child .optgroup-header { border-top: 0; }
.ts-dropdown .optgroup .option { padding-left: 16px; }
.ts-dropdown-content { max-height: 320px; }

/* ── Date field (components/_date_field.html, static/js/date_field.js) ──
   Moved here from the script on 13-sep-2026 so the control is styled by the
   same file as every other control and the JS injects nothing. */
.vs-date{position:relative;width:100%}
.vs-date__field{position:relative}
.vs-date .vs-date__input{padding-right:2rem;font-variant-numeric:tabular-nums}
.vs-date__input--plain{width:100%;height:var(--vs-control-h,32px);padding:0 .75rem;
  border:1px solid var(--vs-filter-border,#d1d5db);border-radius:var(--vs-radius,6px);
  background:var(--vs-surface,#fff);font-size:var(--vs-t-body,13px);
  line-height:calc(var(--vs-control-h,32px) - 2px);color:var(--vs-ink,#111827);
  transition:border-color .15s,box-shadow .15s}
.vs-date__input--plain::placeholder{color:#9ca3af}
.vs-date__input--plain:focus{outline:none;border-color:var(--color-primary);
  box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 18%,transparent)}
.vs-date__input--plain[readonly],.vs-date__input--plain:disabled{background:#f3f4f6;color:#6b7280;cursor:not-allowed}
.vs-date .vs-date__input.is-invalid{border-color:#dc2626}
.vs-date__glyph{position:absolute;right:.75rem;top:50%;transform:translateY(-50%);
  font-size:12px;color:#9ca3af;pointer-events:none}
.vs-date--locked .vs-date__glyph{color:#d1d5db}
.vs-date__panel{position:fixed;z-index:80;width:17rem;padding:10px;
  background:var(--vs-surface,#fff);border:1px solid var(--vs-rule,#e5e7eb);border-radius:8px;
  box-shadow:0 8px 24px rgba(17,24,39,.12);font-size:var(--vs-t-body,13px);color:var(--vs-ink,#111827)}
.vs-date__head{display:flex;align-items:center;justify-content:space-between;gap:.5rem;margin-bottom:6px}
.vs-date__title{flex:1;text-align:center;font-weight:500;font-size:var(--vs-t-body,13px)}
.vs-date__grid{display:grid;grid-template-columns:repeat(7,34px);gap:2px}
.vs-date__wd{height:24px;line-height:24px;text-align:center;font-size:var(--vs-t-eyebrow,11px);
  font-weight:500;color:#6b7280}
.vs-date__day{height:34px;width:34px;margin:0;padding:0;border:0;background:transparent;
  border-radius:6px;font:inherit;font-size:var(--vs-t-body,13px);color:var(--vs-ink,#111827);cursor:pointer}
.vs-date__day:hover:not(:disabled){background:#f3f4f6}
.vs-date__day.is-out{color:#9ca3af}
.vs-date__day.is-today{font-weight:600;box-shadow:inset 0 0 0 1px #d1d5db}
.vs-date__day.is-cursor{box-shadow:inset 0 0 0 2px color-mix(in srgb,var(--color-primary) 45%,transparent)}
.vs-date__day.is-selected,.vs-date__day.is-selected:hover{background:var(--color-primary);color:#fff;box-shadow:none}
.vs-date__day:disabled{color:#d1d5db;cursor:not-allowed}
.vs-date__foot{display:flex;justify-content:space-between;margin-top:6px;padding-top:6px;
  border-top:1px solid var(--vs-rule,#e5e7eb)}

/* A section reached from a record index lands under the fixed top bar and the sticky strip without this. */
[id^="sec-"] { scroll-margin-top: 6.5rem; }

/* ========================================================================
 * Count strip: a row of "dot · number · word" (Findings: Open / Pending
 * retest…, Scope: in scope / with findings…). The dot carries the colour;
 * the number is dark and the word is grey, so the row reads as data rather
 * than as four coloured labels separated by pipes.
 * ====================================================================== */
.vs-counts { display: flex; flex-wrap: wrap; align-items: center; gap: .375rem 1.25rem; font-size: 13px; line-height: 1.25rem; color: #4b5563; }
.vs-counts__item { display: inline-flex; align-items: center; gap: .4rem; white-space: nowrap; }
button.vs-counts__item { border-radius: .375rem; padding: .125rem .375rem; margin: -.125rem -.375rem; transition: background-color .12s; cursor: pointer; }
button.vs-counts__item:hover { background-color: #f3f4f6; }
.vs-counts__dot { width: 7px; height: 7px; border-radius: 9999px; flex-shrink: 0; background: #9ca3af; }
.vs-counts__n { font-weight: 600; color: #111827; font-variant-numeric: tabular-nums; }
