/* =============================================================================
 * VIOLET DESIGN SYSTEM
 * =============================================================================
 * Loaded after components.css. Everything here is a decision taken ONCE, so
 * that fixing a screen stops being an opinion and becomes applying a rule.
 *
 * Why this file exists, in numbers measured in the repository:
 *
 *   6 057  loose pixel sizes in templates (2 618 of them text-[10px]),
 *          400+ of them below 8px. text-[5px] exists in the product.
 *   1 861  hand-written purple/violet/indigo classes.
 *      40  distinct shapes of <select>, across 291 of them in 119 files.
 *       6  different ways to paint "Critical" in red, across 24 templates.
 *
 * None of that is a bug. It is what happens when the COLOUR and the SIZE are
 * decided by the template instead of by the component: every new screen invents
 * them again, and the product ends up looking like it was assembled rather than
 * designed. That is the actual mechanism behind "this looks like AI made it".
 *
 * -----------------------------------------------------------------------------
 * THE FOUR RULES
 * -----------------------------------------------------------------------------
 * 1. The chrome is neutral. Colour is DATA, never decoration. When everything
 *    is coloured, the one red thing that matters does not read as red.
 *
 * 2. A fact is not a control. `.vs-prop` for something you can only read,
 *    `.vs-chip` for something you can change — and a chip ALWAYS carries a
 *    chevron. The Finding header used to chip everything, so the chip stopped
 *    meaning anything and six of them sat in a row in six different shapes.
 *
 * 3. One control, one shape. State changes the colour; it never changes the
 *    shape, the height or the padding.
 *
 * 4. Nothing below 11px. Text at 8px is not small, it is unread.
 * ========================================================================== */

:root {
  /* ── Neutrals. Cool-biased, so they sit under any tenant accent ────────── */
  --vs-ink:        #111827;
  --vs-ink-2:      #4b5563;
  --vs-ink-3:      #6b7280;
  --vs-ink-4:      #9ca3af;
  --vs-surface:    #ffffff;
  --vs-surface-2:  #f9fafb;
  --vs-surface-3:  #f3f4f6;
  --vs-rule:       #e5e7eb;
  --vs-rule-2:     #d1d5db;

  /* ── Semantic. Four meanings and no more. The accent is the tenant's ──── */
  --vs-very-critical: #b91c1c;  --vs-very-critical-bg: #fee2e2;  --vs-very-critical-line: #fecaca;  /* = Critical: same red (RSI, 30-sep-2026) */
  --vs-critical:      #b91c1c;  --vs-critical-bg: #fee2e2;  --vs-critical-line: #fecaca;
  --vs-high:          #c2410c;  --vs-high-bg:     #ffedd5;  --vs-high-line:     #fed7aa;
  --vs-medium:        #a16207;  --vs-medium-bg:   #fef3c7;  --vs-medium-line:   #fde68a;
  --vs-low:           #15803d;  --vs-low-bg:      #dcfce7;  --vs-low-line:      #bbf7d0;
  --vs-info:          #1d4ed8;  --vs-info-bg:     #dbeafe;  --vs-info-line:     #bfdbfe;
  /* Not severities. OK = done / compliant / remediated / success: the same
     green as Low, but a different meaning. PROGRESS = a workflow stage under
     way (in progress, planned-for). */
  --vs-ok:            #15803d;  --vs-ok-bg:       #dcfce7;  --vs-ok-line:       #bbf7d0;
  --vs-progress:      #1d4ed8;  --vs-progress-bg: #dbeafe;  --vs-progress-line: #bfdbfe;
  /* ── States, for the dashboards (7-oct-2026). One traffic light for every
     state a chart or a legend paints: a control's proof, a supplier's
     assessment, the move a supplier calls for. OK green (the same green as
     Low and OK), WARN amber, BAD red, NEUTRAL blue (a count that is neither
     good nor bad: "assess", "in review"), MUTED grey for what does not exist
     yet ("no evidence", "never assessed") and MUTED-2 a darker grey for the
     second absence on the same bar ("no owner"). Severities keep their own
     tokens above; a state is never painted with a severity and a severity
     never with a state. violet-charts.js resolves the chart tokens "ok",
     "warn", "bad", "neutral", "muted", "muted2" through these variables, and
     services/palette.py hands the same variables to HTML legends, so there is
     one place to change a state's colour. */
  --vs-warn:          #d97706;  --vs-warn-bg:     #fef3c7;  --vs-warn-line:     #fde68a;
  --vs-bad:           #dc2626;  --vs-bad-bg:      #fee2e2;  --vs-bad-line:      #fecaca;
  --vs-neutral:       #2563eb;  --vs-neutral-bg:  #dbeafe;  --vs-neutral-line:  #bfdbfe;
  --vs-muted:         #9ca3af;  --vs-muted-bg:    #f3f4f6;  --vs-muted-line:    #e5e7eb;
  --vs-muted-2:       #6b7280;  --vs-muted-2-bg:  #f3f4f6;  --vs-muted-2-line:  #d1d5db;

  /* ── Type scale. FOUR sizes plus the metric ─────────────────────────────
     12 / 13 / 15 / 20, and 28 for the one number a screen exists to show.
     --vs-t-eyebrow is the single exception and it is not a fifth size: it is
     11px ONLY because it is always uppercase and letterspaced, which reads a
     size larger than it measures. Nothing in sentence case may use it. */
  --vs-t-eyebrow: 11px;   /* UPPERCASE labels only — never sentence case   */
  --vs-t-meta:    12px;   /* secondary text, timestamps, counts           */
  --vs-t-body:    13px;   /* running text and table cells                 */
  --vs-t-title:   15px;   /* card and section titles                      */
  --vs-t-page:    20px;   /* the one page title                           */
  --vs-t-metric:  28px;   /* the number a screen exists to show           */

  /* ── Weight. Three, and they mean something ─────────────────────────────
     400 running text · 500 a label or a row · 600 a title or an active row.
     700/800 are not in the system: bold everywhere is bold nowhere. */
  --vs-w-body:   400;
  --vs-w-label:  500;
  --vs-w-title:  600;

  /* ── Geometry. One height for controls, TWO radii ───────────────────────
     6px for every surface and every control — a card, a panel, a modal, an
     input, a button — and the pill for lozenges and chips, which are the only
     things whose shape is allowed to say "this is a small piece of state".
     --vs-radius-lg was 10px and is kept as an alias so the ~40 rules that
     name it converge instead of being hunted down one by one. */
  --vs-control-h:   32px;
  --vs-radius:      6px;      /* surfaces and controls alike               */
  --vs-radius-lg:   var(--vs-radius);
  --vs-radius-pill: 9999px;   /* lozenges and chips ONLY                   */
}

/* =============================================================================
 * TYPE — three classes replace twelve variants of the same idea
 * ========================================================================== */
/* The uppercase label. Twelve hand-written variants existed (10/11px x
   bold/semibold x wide/wider/widest); the most common one appeared 207 times. */
.vs-eyebrow {
  font-size: var(--vs-t-eyebrow);
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--vs-ink-3);
  line-height: 1.4;
}
.vs-meta {
  font-size: var(--vs-t-meta);
  color: var(--vs-ink-3);
  line-height: 1.5;
}
/* Running text and table cells. Named so markup never has to spell 13px —
   the first pass at the table wrote `text-[13px]` by hand and the arbitrary-
   size ratchet caught it, correctly: a token you have to retype is a token
   that will drift. */
.vs-body { font-size: var(--vs-t-body); }
/* Section titles were font-semibold 71 times and font-bold 53 times on the same
   element, in four different greys. One answer. */
.vs-section-title {
  font-size: var(--vs-t-title);
  font-weight: 600;
  color: var(--vs-ink);
  letter-spacing: -.006em;
  line-height: 1.35;
}
/* The number a screen exists to show: large, black, and NOT inside a coloured
   box with an icon tile beside it. */
.vs-metric {
  font-size: var(--vs-t-metric);
  font-weight: 700;
  color: var(--vs-ink);
  line-height: 1.05;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.02em;
}
.vs-metric-label {
  font-size: var(--vs-t-eyebrow);
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--vs-ink-3);
}
/* A metric wears a colour ONLY when the colour is the reading. "1 critical" is
   red because red is the answer; "12 findings" is not anything, so it is ink. */
.vs-metric--very-critical { color: var(--vs-very-critical); }
.vs-metric--critical { color: var(--vs-critical); }
.vs-metric--high     { color: var(--vs-high); }
.vs-metric--medium   { color: var(--vs-medium); }
.vs-metric--low      { color: var(--vs-low); }
.vs-metric--info     { color: var(--vs-info); }
.vs-metric--ok       { color: var(--vs-ok); }
.vs-metric--progress { color: var(--vs-progress); }

/* The KPI tile. Grey by default, and that is the point: a row of four cards
   used to show four brand-coloured squares, so the eye went to the decoration
   instead of to the one number that needed attention. It takes a tone only
   when the card is reporting a severity. */
.vs-kpi-icon {
  width: 32px;
  height: 32px;
  border-radius: var(--vs-radius);
  border: 1px solid var(--vs-rule);
  background: var(--vs-surface-3);
  color: var(--vs-ink-3);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  font-size: 13px;
}
.vs-kpi-icon--very-critical { background: var(--vs-very-critical-bg); border-color: var(--vs-very-critical-line); color: var(--vs-very-critical); }
.vs-kpi-icon--critical { background: var(--vs-critical-bg); border-color: var(--vs-critical-line); color: var(--vs-critical); }
.vs-kpi-icon--high     { background: var(--vs-high-bg);     border-color: var(--vs-high-line);     color: var(--vs-high); }
.vs-kpi-icon--medium   { background: var(--vs-medium-bg);   border-color: var(--vs-medium-line);   color: var(--vs-medium); }
.vs-kpi-icon--low      { background: var(--vs-low-bg);      border-color: var(--vs-low-line);      color: var(--vs-low); }
.vs-kpi-icon--info     { background: var(--vs-info-bg);     border-color: var(--vs-info-line);     color: var(--vs-info); }
.vs-kpi-icon--ok       { background: var(--vs-ok-bg);       border-color: var(--vs-ok-line);       color: var(--vs-ok); }

/* A KPI strip of nine tiles (Score, the five severities, Open, Remediated,
   Accepted). Tailwind is compiled and ships no xl:grid-cols-9, so the xl step
   lives here; below xl the strip keeps its md:grid-cols-4 wrap. */
@media (min-width: 1280px) {
  .vs-kpi-grid--9 { grid-template-columns: repeat(9, minmax(0, 1fr)); }
}

/* =============================================================================
 * PROP — a fact. Label above, value below. No border, no background.
 * ========================================================================== */
/* This is rule 2, and it is the one that changes the product. Anything the
   reader cannot change is a prop; only what they CAN change earns a chip. */
.vs-prop { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.vs-prop__label {
  font-size: var(--vs-t-eyebrow);
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--vs-ink-4);
  line-height: 1.3;
}
.vs-prop__value {
  font-size: var(--vs-t-body);
  color: var(--vs-ink);
  line-height: 1.4;
  min-width: 0;
}
.vs-prop__value a { color: var(--color-primary); text-decoration: none; }
.vs-prop__value a:hover { text-decoration: underline; }
/* A row of them. Sits under a title, reads as one block. */
.vs-prop-row {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 2rem;
  align-items: flex-start;
}
/* The right-hand properties rail: label left, value right, one row each. */
.vs-prop-list { display: flex; flex-direction: column; }
.vs-prop-list > div {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 7px 0;
  border-bottom: 1px solid var(--vs-rule);
  font-size: var(--vs-t-body);
}
.vs-prop-list > div:last-child { border-bottom: none; }
.vs-prop-list dt, .vs-prop-list .vs-prop-list__k {
  color: var(--vs-ink-3);
  font-size: var(--vs-t-meta);
  white-space: nowrap;
}
.vs-prop-list dd, .vs-prop-list .vs-prop-list__v {
  margin: 0;
  color: var(--vs-ink);
  text-align: right;
  min-width: 0;
}

/* =============================================================================
 * SEVERITY — one token, used by all 24 templates that used to invent it
 * ========================================================================== */
/* The WORD, everywhere. Not the letter: this product exports to PDF and PPTX
   for a client committee, and a bare "C" in a report needs a legend the word
   does not. `--compact` exists for the two or three genuinely dense tables (one
   already reached for it by hand in audit_detail.html) and is an exception that
   must stay an exception. */
.vs-sev {
  display: inline-flex;
  align-items: center;
  gap: .375rem;
  /* 26px, the same as .vs-chip. A finding header puts facts and controls in
     one row — SEVERITY, IMPACT, the effective-band arrow, STATUS, KEV, PEOPLE
     — and at 22px against 26px they sat on different baselines. What separates
     a fact from a control is the chevron, not four pixels.
     `.vs-sev--compact` still exists for dense tables. */
  height: 26px;
  padding: 0 .5rem;
  border-radius: 4px;
  border: 1px solid transparent;
  font-size: var(--vs-t-eyebrow);
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
}
.vs-sev--very-critical,
.vs-sev--very_critical { background: var(--vs-very-critical-bg); color: var(--vs-very-critical); border-color: var(--vs-very-critical-line); }
.vs-sev--critical { background: var(--vs-critical-bg); color: var(--vs-critical); border-color: var(--vs-critical-line); }
.vs-sev--high     { background: var(--vs-high-bg);     color: var(--vs-high);     border-color: var(--vs-high-line); }
.vs-sev--medium   { background: var(--vs-medium-bg);   color: var(--vs-medium);   border-color: var(--vs-medium-line); }
.vs-sev--low      { background: var(--vs-low-bg);      color: var(--vs-low);      border-color: var(--vs-low-line); }
.vs-sev--info     { background: var(--vs-info-bg);     color: var(--vs-info);     border-color: var(--vs-info-line); }
.vs-sev--none     { background: var(--vs-surface-3);   color: var(--vs-ink-3);    border-color: var(--vs-rule); }
/* Same shape for a GOOD outcome (compliant, clean, implemented). Not a
   severity: it shares Low's green, but Low is never borrowed to mean "fine". */
.vs-sev--ok       { background: var(--vs-ok-bg);       color: var(--vs-ok);       border-color: var(--vs-ok-line); }
/* The exception, declared. Square, initial only, for tables of 20+ rows. */
.vs-sev--compact {
  width: 18px; height: 18px; padding: 0;
  justify-content: center;
  font-size: 10px; font-weight: 700;
}

/* =============================================================================
 * CARD — a surface, and nothing else
 * ========================================================================== */
/* No gradient band, no coloured icon tile, no oversized padding, and since the
   canvas is tinted (--page-bg), no outline either: the ground separates the
   card, and drawing a hairline as well is the same boundary twice. The border
   is kept and made transparent rather than removed, so the 1px still occupies
   its space and no layout shifts. Rules INSIDE a card (.vs-card__head's
   bottom edge, table rules, dividers) are a different job and stay. */
.vs-card {
  background: var(--vs-surface);
  border: 1px solid transparent;
  border-radius: var(--vs-radius-lg);
  overflow: hidden;
}

/* The same card, hand-written. 145 templates draw their surfaces as
   `bg-white rounded-lg border border-gray-200` instead of .vs-card, and they
   are not going to be swept in one sitting — so the rule reaches them where
   they are. Four classes outrank Tailwind's single `border-gray-200`, and the
   combination is specific enough to mean "a card": a bordered white rounded
   box. Anything that needs its edge (inputs, the bordered variants of a
   lozenge) does not match all four. */
main .bg-white.rounded-lg.border.border-gray-200,
main .bg-white.rounded-xl.border.border-gray-200 {
  border-color: transparent;
}
/* ...except a dropdown panel: positioned absolutely over the page, it is a
   popover, not a card, and without its edge it read as a loose white box
   (Saved views, Columns). Its lift is restored in components.css. */
main .absolute.bg-white.rounded-lg.border.border-gray-200,
main .absolute.bg-white.rounded-xl.border.border-gray-200 {
  border-color: #e5e7eb;
}
.vs-card__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .75rem;
  padding: .75rem 1rem;
  border-bottom: 1px solid var(--vs-rule);
}
.vs-card__head h2, .vs-card__head h3 {
  font-size: var(--vs-t-title);
  font-weight: 600;
  color: var(--vs-ink);
  margin: 0;
}
.vs-card__body { padding: 1rem; }

/* =============================================================================
 * BUTTON — hierarchy comes from position, not from colour
 * ========================================================================== */
/* One height, one radius, one type size. `--primary` is the tenant accent and
   there is at most ONE on a screen; everything else is quiet. 156 buttons had
   their own px-* padding, which is why no two action rows lined up. */
.vs-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .4rem;
  height: var(--vs-control-h);
  padding: 0 .75rem;
  border-radius: var(--vs-radius);
  border: 1px solid var(--vs-rule-2);
  background: var(--vs-surface);
  color: var(--vs-ink-2);
  font-size: var(--vs-t-meta);
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  text-decoration: none;
  transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}
.vs-btn > i { font-size: 11px; opacity: .8; }
.vs-btn:hover { background: var(--vs-surface-2); border-color: var(--vs-ink-4); color: var(--vs-ink); }
.vs-btn:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 1px; }
.vs-btn[disabled], .vs-btn.is-disabled { opacity: .5; cursor: not-allowed; }
.vs-btn--primary {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
}
.vs-btn--primary:hover { filter: brightness(.94); background: var(--color-primary); color: #fff; }
.vs-btn--danger { background: var(--vs-critical); border-color: var(--vs-critical); color: #fff; }
.vs-btn--danger:hover { filter: brightness(.94); background: var(--vs-critical); color: #fff; }
/* Icon-only, square, same height as everything else in the row. */
.vs-btn--icon { width: var(--vs-control-h); padding: 0; }

/* =============================================================================
 * TABLE — one header treatment
 * ========================================================================== */
.vs-table { width: 100%; border-collapse: collapse; font-size: var(--vs-t-body); }
.vs-table th {
  text-align: left;
  padding: .5rem .75rem;
  font-size: var(--vs-t-eyebrow);
  font-weight: 600;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--vs-ink-3);
  border-bottom: 1px solid var(--vs-rule-2);
  white-space: nowrap;
  background: var(--vs-surface-2);
}
.vs-table td {
  padding: .625rem .75rem;
  border-bottom: 1px solid var(--vs-rule);
  color: var(--vs-ink);
  vertical-align: middle;
}
.vs-table tbody tr:last-child td { border-bottom: none; }
.vs-table tbody tr:hover td { background: var(--vs-surface-2); }
.vs-table .is-num { text-align: right; font-variant-numeric: tabular-nums; }

/* =============================================================================
 * SUB-NAV — the second strip, inside a section
 * ========================================================================== */
/* No second strip any more. GRC used to carry a top strip (Posture /
 * Compliance / Risk / Policies / Reports) repeating the sidebar entry the
 * reader had just clicked, with these below it — so a quieter `--sub`
 * variant existed to say "subordinate". The top strip is gone: the sidebar
 * owns that level, and each section keeps only its own strip, which is
 * therefore the module's PRIMARY nav and wears `.page-tabs` like every
 * other module. A second size with no second level to mark was just a
 * second size. */

/* =============================================================================
   Lifecycle rail (Audits). One row, six steps, a hairline between them.
   Node + label + count read left to right like a sentence; the active step is
   the only filled node on the row, hover borrows the accent for one node.
   ========================================================================== */
.vs-rail {
  display: flex;
  align-items: center;
  gap: 1rem;
  background: #fff;
  border: 1px solid var(--vs-rule);
  border-radius: .75rem;
  padding: .625rem 1.125rem;
  margin-bottom: 1.25rem;
}
.vs-rail__eyebrow {
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--vs-ink-4);
  white-space: nowrap;
  padding-right: 1rem;
  border-right: 1px solid var(--vs-rule);
}
.vs-rail__track {
  display: flex;
  align-items: center;
  flex: 1 1 auto;
  min-width: 0;
  gap: .375rem;
  overflow-x: auto;
  scrollbar-width: none;
}
.vs-rail__track::-webkit-scrollbar { display: none; }
.vs-rail__step {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  padding: .25rem .5rem .25rem .25rem;
  border-radius: 9999px;
  text-decoration: none;
  white-space: nowrap;
  transition: background-color .15s ease;
}
.vs-rail__step:hover { background: var(--vs-surface-3); }
.vs-rail__node {
  width: 26px;
  height: 26px;
  border-radius: 9999px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--vs-surface-3);
  border: 1px solid var(--vs-rule);
  color: var(--vs-ink-3);
  font-size: 10.5px;
  transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}
.vs-rail__label {
  font-size: 13px;
  font-weight: 600;
  color: #111827;
  letter-spacing: -.005em;
}
.vs-rail__count {
  font-size: 11.5px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--vs-ink-4);
  min-width: 1.25rem;
  height: 18px;
  line-height: 18px;
  padding: 0 .375rem;
  text-align: center;
  border-radius: 9999px;
  background: var(--vs-surface-3);
}
/* Stuck = urgency, but five red pills in a row is a siren, not a signal: the
   colour goes in the number and a tint, and the loudest thing on the row is
   still the one step you are filtering by. */
.vs-rail__count--stuck { color: var(--vs-critical); background: color-mix(in srgb, var(--vs-critical) 10%, white); }
.vs-rail__link { flex: 1 1 auto; min-width: .75rem; height: 1px; background: var(--vs-rule); }
.vs-rail__step:hover .vs-rail__node { border-color: var(--color-primary); color: var(--color-primary); }
.vs-rail__step--active .vs-rail__node { background: var(--color-primary); border-color: var(--color-primary); color: #fff; }
.vs-rail__step--active .vs-rail__label { color: var(--color-primary); }
.vs-rail__step--active .vs-rail__count { background: color-mix(in srgb, var(--color-primary) 12%, white); color: var(--color-primary); }
.vs-rail__clear {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  font-size: 12px;
  color: var(--vs-ink-4);
  white-space: nowrap;
  text-decoration: none;
}
.vs-rail__clear:hover { color: #111827; }
@media (max-width: 1100px) { .vs-rail__eyebrow { display: none; } }



/* ══════════════════════════════════════════════════════════════════════════
   THE KPI TILE — .vs-kpi, rendered by components/_exec_kpi.html
   ══════════════════════════════════════════════════════════════════════════
   This is the product's ONE card for "a number that means something", and as
   of 7-sep-2026 it is the canonical one: components/kpi_card.html delegates
   to it, so the 107 tiles that used the older card wear this shape too.

   It used to live inside violet/partials/_vs_design_system.html, an OPTIONAL
   per-page include - which is precisely why it could not be canonical: a page
   that adopted the component and forgot the partial rendered an unstyled div,
   so 25 templates kept using the older card instead. A shared component whose
   styles are opt-in is not shared.

   Every var() carries the literal it resolves to inside `.vs-dash` as its
   fallback, so the tile is identical on a dashboard (tokens present) and on a
   plain page (tokens absent), and nothing has to be defined on :root where it
   could collide.
   ══════════════════════════════════════════════════════════════════════════ */
.vs-kpi{
  position:relative; background:#fff;
  border:1px solid var(--border-1, #e5e7eb); border-radius:var(--vs-radius, 6px);
  padding:12px 14px; cursor:pointer; overflow:hidden;
  display:flex; flex-direction:column; gap:8px;
  transition:border-color .2s var(--e, cubic-bezier(.4,0,.2,1));
}
.vs-kpi:hover{ border-color:var(--border-2, #d1d5db); }
/* A tile that is also a filter says so HERE, not in the page that uses it.
   Three screens had grown their own selected state as an inline
   `border-[var(--color-primary)] ring-1 ring-.../20` on a hand-built box,
   which is the whole reason they could not adopt the component. Pass
   kpi_class="vs-kpi--on". */
.vs-kpi--on{
  border-color:var(--color-primary);
  box-shadow:0 0 0 1px color-mix(in srgb, var(--color-primary) 25%, transparent);
}
/* The audit Summary cockpit: eight tiles on one slim row. Same tile, half the
   height, so the page's own summary card is whole on screen when it opens. */
.vs-cockpit .vs-kpi{ padding:8px 12px; gap:2px; }
.vs-cockpit .vs-kpi__label{ font-size:10.5px; letter-spacing:.04em; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; display:block; }
.vs-cockpit .vs-kpi__label i{ font-size:11px; margin-right:4px; }
.vs-cockpit .vs-kpi__num{ font-size:20px; margin-top:0; }
.vs-cockpit .vs-kpi__num--word{ font-family:inherit; font-size:13px; font-weight:600; letter-spacing:0; line-height:1.4; }
.vs-cockpit .vs-kpi__sub{ font-size:11px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.vs-cockpit .vs-kpi > div{ min-width:0; }
/* A row of many small tiles (one per status): same tile, tighter. */
.vs-kpi--compact{ padding:10px 12px; gap:4px; }
.vs-kpi--compact .vs-kpi__label{ font-size:10.5px; letter-spacing:.04em; line-height:1.25; min-height:2.5em; align-items:flex-start; }
.vs-kpi--compact .vs-kpi__label i{ font-size:12px; }
.vs-kpi--compact .vs-kpi__num{ font-size:22px; }
.vs-kpi__label{
  font-size:11px; letter-spacing:.06em; text-transform:uppercase; font-weight:600;
  color:var(--fg-3, #8a8aa0); display:flex; align-items:center; gap:6px;
}
.vs-kpi__label i{ font-size:14px; color:var(--ink-300, #b8b8c8); }
/* The tinted 28px icon chip is gone from the component. The class survives as
   the rule that UNDOES it, so the handful of tiles still hand-written with it
   land in the same place without those pages being edited. */
.vs-kpi__chip{
  width:auto !important; height:auto !important; padding:0 !important;
  background:transparent !important; border:0 !important; border-radius:0 !important;
  display:inline-flex; align-items:center; font-size:14px; flex-shrink:0;
}
.vs-kpi__num{
  font-family:var(--font-display, 'Space Grotesk', system-ui, sans-serif);
  font-weight:600; font-size:28px; line-height:1.02; letter-spacing:-.02em;
  color:var(--fg-1, #0f0f1c); margin-top:2px;
}
/* A value that is a word, not a figure. See components/_exec_kpi.html: the
   28px display size is what makes a number legible across a row of tiles, and
   what makes "No" read as shouting. */
.vs-kpi__num--word{ font-size:18px; letter-spacing:-.01em; }
/* A tile whose value is a name (a finding, a milestone) keeps one line and
   ends in an ellipsis, so a row of tiles keeps one height. kpi_class="vs-kpi--clip". */
.vs-kpi--clip{ min-width:0; }
.vs-kpi--clip > div{ min-width:0; }
.vs-kpi--clip .vs-kpi__num,
.vs-kpi--clip .vs-kpi__sub{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* A name is read, not scanned: body size, not the 18px word size, so a row
   of names (stage, finding, milestone, report) does not shout. */
.vs-kpi--clip .vs-kpi__num--word{
  font-family:inherit; font-size:14px; font-weight:600; letter-spacing:0; line-height:1.35; margin-top:0;
}
.vs-kpi__sub{ font-size:12px; color:var(--fg-3, #8a8aa0); margin-top:1px; }
/* State or flow: "Now" / "Last 30 days", top-right, quieter than the label. */
.vs-kpi__scope{
  font-size:11px; letter-spacing:.06em; text-transform:uppercase; font-weight:500;
  color:#9ca3af; white-space:nowrap;
}
.vs-kpi__spark{ position:relative; width:100%; height:52px; margin-top:auto; }
.vs-kpi__spark canvas{ position:absolute; inset:0; width:100% !important; height:100% !important; }
/* The small line BESIDE the number (`trend` on the tile): fixed box, so a row
   of tiles keeps one height whether or not a tile carries it. */
.vs-kpi__trend{ position:relative; width:96px; max-width:45%; height:34px; flex-shrink:0; margin-bottom:2px; }
.vs-kpi__spark > div{ position:absolute; inset:0; }

/* ══════════════════════════════════════════════════════════════════════════
   THE SEGMENTED CONTROL: .vs-seg, the product's one view switch
   ══════════════════════════════════════════════════════════════════════════
   One pill group, one look: Dashboard | List on every built-in dashboard
   (components/_dash_toolbar.html), the period on the TPRM dashboard, and
   List | Matrix | By process on the Risk register.

   It lived inside violet/partials/_vs_design_system.html, the optional
   per-page include, which is why a list screen could not wear it: off a
   `.vs-dash` page every token was undefined and the control lost its tray.
   Same rule as the KPI tile above: each var() carries the literal it
   resolves to inside `.vs-dash`, so it reads the same on both kinds of page.
   ══════════════════════════════════════════════════════════════════════════ */
.vs-seg{
  display:inline-flex; align-items:center; gap:0;
  background:var(--ink-25, #fafaff); border:1px solid var(--border-1, #e5e7eb);
  border-radius:var(--vs-radius, 6px); padding:2px;
}
.vs-seg__icon{ display:inline-flex; align-items:center; padding:0 4px 0 6px; color:var(--fg-3, #8a8aa0); }
.vs-seg__btn{
  padding:4px 9px; border-radius:4px; font-size:12px; font-weight:500;
  white-space:nowrap; color:var(--fg-3, #8a8aa0); text-decoration:none;
  border:0; background:transparent; cursor:pointer;
  transition:background .2s var(--e, cubic-bezier(.2,.7,.2,1)), color .2s var(--e, cubic-bezier(.2,.7,.2,1)), box-shadow .2s var(--e, cubic-bezier(.2,.7,.2,1));
}
.vs-seg__btn:hover{ color:var(--fg-1, #0f0f1c); }
.vs-seg__btn--on{ background:#fff; color:var(--color-primary); box-shadow:var(--shadow-sm, none); }
/* A short code beside a segment's label: the EBA template a register view
   fills (B_05.01). Smaller and muted, so the label still reads first. */
.vs-seg__hint{ margin-left:6px; font-family:var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size:10.5px; font-weight:500; color:var(--fg-3, #8a8aa0); }
.vs-seg__btn--on .vs-seg__hint{ color:inherit; opacity:.7; }
/* The view switch a grouped tab draws under the strip
   (components/_sub_nav_strip.html). It scrolls sideways on a phone rather
   than wrapping into a second tray. */
.vs-view-switch{ max-width:100%; overflow-x:auto; scrollbar-width:none; }
.vs-view-switch::-webkit-scrollbar{ display:none; }

/* =============================================================================
   Mentions: an @colleague inside a comment (services/mentions.py, |mentions)
   ============================================================================= */
.vs-mention { color: var(--color-primary); font-weight: 600; background: color-mix(in srgb, var(--color-primary) 10%, white); border-radius: 4px; padding: 0 3px; }

/* ── Floating labels never wrap over the value (9-oct-2026) ──────────────
   A floating label sits absolutely over the top of its input. A long label
   ("Cost of a compromise through a supplier · Default *") wrapped to a second
   line and printed over the number. Every floating label keeps to one line
   inside the box, clear of the spinner or chevron; the canon's components
   shorten only the text with an ellipsis, so the required star and the help
   icon stay visible. The attribute rule catches hand-written labels too. */
label[class*="absolute left-4 top-2"],
label[class*="absolute left-3 top-2"] {
  right: 2.25rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.vs-float-label {
  display: flex;
  align-items: center;
  gap: 2px;
  overflow: visible;
}
.vs-float-label > * { flex-shrink: 0; }
/* A floating label is read whole, never cut with an ellipsis (Nerea,
   12-nov-2026, on New room: "Cuota de almacenamie…", "Recordatorio (días
   ante…"). One that does not fit wraps; the field it names needs the width,
   or the label above it (vs-prop__label) as the dates have. `label.` lifts
   it over the attribute rule above, which still shortens hand-written ones. */
label.vs-float-label { white-space: normal; overflow: visible; text-overflow: clip; line-height: 1.15; }
.vs-float-label > .vs-float-label__text {
  flex-shrink: 1;
  min-width: 0;
  overflow-wrap: anywhere;
  white-space: normal;
}

/* ══════════════════════════════════════════════════════════════════════════
 * 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 var(--vs-surface-3);
}
.vs-menu__label {
  display: block;
  padding: 8px 12px 4px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--vs-ink-4);
}
.vs-menu__row {
  display: flex;
  align-items: center;
  width: 100%;
  height: 34px;
  padding: 0 12px;
  font-size: 13px;
  font-weight: 500;
  color: var(--vs-ink-2);
  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: var(--vs-ink-4);                 /* 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: var(--vs-ink-4);
  flex-shrink: 0;
}
.vs-menu__sep { height: 1px; margin: 4px 0; background: var(--vs-surface-3); }

/* ══════════════════════════════════════════════════════════════════════════
 * THE ROOMS SHELL — top bar, user menu and sidebar (Violet Rooms, 12-nov-2026)
 * ══════════════════════════════════════════════════════════════════════════
 * Here and not in a template because both top bars use it: the consultancy's
 * (rooms/_topbar.html) and the client room's (portal/base.html), which loads
 * only this file.
 *
 * A top-bar control that drops a menu carries the chevron, the same one on
 * every such control, small and grey: if it opens, it says so (the rule the
 * chips follow).
 */
.vs-menu-chevron {
  font-size: 10px;
  line-height: 1;
  color: var(--vs-ink-4);
  flex-shrink: 0;
}
.vs-topbar-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 32px;
  padding: 0 8px;
  border-radius: var(--vs-radius);
  color: var(--vs-ink-3);
  background: transparent;
  border: 0;
  cursor: pointer;
}
.vs-topbar-btn:hover { background: var(--vs-surface-3); color: var(--vs-ink-2); }
.vs-topbar-btn > .fa-globe,
.vs-topbar-btn > .fa-gear { font-size: 15px; color: var(--vs-ink-4); }

/* The user menu's trigger: avatar, two lines, chevron (components/_user_menu.html). */
.vs-user-menu {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  height: 40px;
  padding: 0 8px 0 4px;
  border-radius: var(--vs-radius);
  background: transparent;
  border: 0;
  cursor: pointer;
  text-align: left;
}
.vs-user-menu:hover { background: var(--vs-surface-3); }
.vs-user-menu__avatar {
  width: 30px;
  height: 30px;
  border-radius: 9999px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  font-size: 11px;
  font-weight: 600;
  color: #fff;
  background: var(--color-primary);
}
.vs-user-menu__text { display: flex; flex-direction: column; min-width: 0; line-height: 1.2; }
.vs-user-menu__who {
  font-size: 13px;
  font-weight: 500;
  color: var(--vs-ink);
  max-width: 16rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.vs-user-menu__role { font-size: 11px; color: var(--vs-ink-3); }

/* The consultancy's sidebar. On the page's own ground (the same token as the
   content behind the cards), set off by a 1px rule: never a dark slab with
   the workspace's name painted in the brand colour on top of it. */
/* The shell's layout lives here, not in Tailwind classes: tailwind.css is
   precompiled and has no md:pl-60 / md:translate-x-0, so the sidebar sat on
   top of the page instead of beside it (Nerea, 12-nov-2026). */
.vs-side {
  width: 15rem;
  background: var(--page-bg, var(--vs-surface-2));
  border-right: 1px solid var(--vs-rule);
  color: var(--vs-ink-2);
  transform: translateX(-100%);
  transition: transform 0.2s ease;
}
.vs-side.vs-side--open { transform: translateX(0); }
@media (min-width: 768px) {
  .vs-side { transform: translateX(0); }
  .vs-shell-main { padding-left: 15rem; }
}
/* The product's mark in the top bar (rooms/_topbar.html), drawn the way
   Violet Security draws its own: the product lilac, never the consultancy's
   colour (a white-labelled red would paint our name). Same width as the
   sidebar so the logo sits over the rail's left edge. */
:root {
  --vr-product: #7C3AED;
  --vr-product-ink: color-mix(in srgb, var(--vr-product) 60%, #1f1147);
}
.vs-product-mark {
  display: flex;
  align-items: center;
  gap: 10px;
  height: 100%;
  flex-shrink: 0;
  padding: 0 1.25rem;
  text-decoration: none;
}
@media (min-width: 768px) { .vs-product-mark { width: 15rem; } }
.vs-product-mark__word {
  font-family: var(--font-display, 'Space Grotesk', system-ui, sans-serif);
  font-size: 15px;
  line-height: 1;
  letter-spacing: -0.015em;
  white-space: nowrap;
}
.vs-product-mark__violet { font-weight: 700; color: var(--vr-product-ink); }
.vs-product-mark__rooms { font-weight: 500; color: var(--vr-product); }

/* The top bar's search (components/_global_search.html): Violet Security's
   search box, left of centre and growing up to 32rem, with its results in a
   panel under it. Hidden on a phone, as Violet's was. */
.vs-topsearch {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  max-width: 32rem;
  display: none;
}
@media (min-width: 640px) { .vs-topsearch { display: block; } }
.vs-topsearch input[type="search"] { height: 2rem; }
.vs-search-field--kbd { padding-right: 3.75rem !important; }
/* Typing: the hint gives its place to the field's own clear button. */
.vs-search-field--kbd:not(:placeholder-shown) + .vs-search-kbd { display: none; }
.vs-search-kbd {
  position: absolute; right: .5rem; top: 50%; transform: translateY(-50%);
  font-size: 11px; line-height: 1; padding: 3px 6px; border-radius: 4px;
  background: var(--vs-surface-3); color: var(--vs-ink-3);
  pointer-events: none;
}
.vs-topsearch__panel {
  position: absolute; left: 0; right: 0; top: calc(100% + 4px); z-index: 60;
  max-height: min(70vh, 32rem); overflow-y: auto;
  padding: .375rem;
  background: var(--vs-surface);
  border: 1px solid var(--vs-rule);
  border-radius: var(--vs-radius);
  box-shadow: 0 8px 24px rgba(17, 24, 39, .12);
}
.vs-topsearch__group + .vs-topsearch__group { margin-top: .375rem; padding-top: .375rem; border-top: 1px solid var(--vs-rule); }
.vs-topsearch__label { padding: .25rem .5rem; }
.vs-topsearch__note { padding: .625rem .5rem; font-size: 13px; color: var(--vs-ink-3); }
.vs-topsearch__hit {
  display: flex; align-items: center; gap: .625rem;
  padding: .4375rem .5rem; border-radius: 6px;
  color: var(--vs-ink); text-decoration: none;
}
.vs-topsearch__hit:hover,
.vs-topsearch__hit--active { background: color-mix(in srgb, var(--color-primary) 6%, white); }
.vs-topsearch__icon { width: 1rem; text-align: center; font-size: 12px; color: var(--vs-ink-4); flex-shrink: 0; }
.vs-topsearch__text { display: flex; flex-direction: column; min-width: 0; }
.vs-topsearch__title { font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vs-topsearch__meta { font-size: 11px; color: var(--vs-ink-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* A room card's foot (rooms/_room_card_foot.html): two labelled blocks,
   the assigned auditors and the client's manager, side by side. */
.vs-room-foot { display: grid; grid-template-columns: 1fr; gap: .625rem; width: 100%; }
@media (min-width: 640px) { .vs-room-foot { grid-template-columns: 1fr 1fr; gap: 1rem; } }
.vs-room-foot__block { min-width: 0; display: flex; flex-direction: column; gap: .25rem; }
.vs-room-foot__who { display: flex; align-items: center; gap: .5rem; min-width: 0; min-height: 1.75rem; }
.vs-room-foot__name { font-size: 12px; color: var(--vs-ink-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vs-room-foot__none { font-size: 12px; color: var(--vs-ink-4); }
.vs-room-foot__link { font-size: 12px; font-weight: 500; color: var(--color-primary); position: relative; z-index: 1; }
.vs-room-foot__link:hover { text-decoration: underline; }

/* A colour field (components/_color_field.html, the Brand page): the swatch
   is the browser's colour picker hidden behind a square painted with the
   value, beside its #RRGGBB box. */
.vs-color-field__row { display: flex; align-items: center; gap: .5rem; margin-top: .25rem; }
.vs-color-field__swatch {
  position: relative; flex-shrink: 0;
  width: 2rem; height: 2rem; border-radius: var(--vs-radius);
  border: 1px solid var(--vs-rule); cursor: pointer; overflow: hidden;
}
.vs-color-field__picker { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; border: 0; padding: 0; }
.vs-filter-control.vs-color-field__hex { width: 9.5rem; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; text-transform: uppercase; }
.vs-filter-control.vs-color-field__hex::placeholder { text-transform: none; font-family: 'Satoshi', system-ui, sans-serif; }
.vs-color-field__help { margin-top: .375rem; font-size: 12px; color: var(--vs-ink-3); }
.vs-color-field__error { margin-top: .375rem; font-size: 12px; color: var(--vs-sev-critical, #b91c1c); }
.vs-color-field__warning {
  padding: .5rem .75rem; border-radius: var(--vs-radius);
  border: 1px solid #fcd34d; background: #fffbeb; color: #92400e; font-size: 13px;
}

/* A logo field (Edit room's client logo, Task 8k): one full-width row, the
   preview contained in a 40px square on the left, Choose file and Remove on
   the right, the help under it. The file input itself is sr-only. */
.vs-logo-field__row { display: flex; align-items: center; gap: .75rem; margin-top: .25rem; flex-wrap: wrap; }
.vs-logo-field__preview {
  width: 40px; height: 40px; flex-shrink: 0; display: grid; place-items: center; overflow: hidden;
  border: 1px solid var(--vs-rule); border-radius: var(--vs-radius); background: #fff; color: var(--vs-ink-4);
}
.vs-logo-field__preview img { max-width: 100%; max-height: 100%; object-fit: contain; }
.vs-logo-field__name { min-width: 0; flex: 1 1 8rem; font-size: 12px; color: var(--vs-ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vs-logo-field__actions { display: flex; align-items: center; gap: .5rem; margin-left: auto; }
.vs-logo-field__help { margin-top: .375rem; font-size: 12px; color: var(--vs-ink-3); }
/* A section of a single-pane modal (Edit room's "Room colours"): a rule and
   a sentence-case heading, never an eyebrow. */
.vs-modal-section { padding-top: 1rem; border-top: 1px solid var(--vs-surface-3); }
.vs-modal-section__title { font-size: 13px; font-weight: 600; color: var(--vs-ink); }
.vs-modal-section__help { margin-top: .125rem; font-size: 12px; color: var(--vs-ink-3); }

/* A KPI row of five (the client room's Summary with its Recommended fixes
   hidden, Task 8k): no hole at the end of a row at any width. Phone, 2+2+1
   with the last card across; tablet, 3+2 on a six-track grid; desktop, five. */
.vs-kpi-row.vs-kpi-row--5 > :last-child { grid-column: span 2; }
@media (min-width: 768px) {
  .vs-kpi-row.vs-kpi-row--5 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .vs-kpi-row.vs-kpi-row--5 > * { grid-column: span 2; }
  .vs-kpi-row.vs-kpi-row--5 > :nth-child(n+4) { grid-column: span 3; }
}
@media (min-width: 1280px) {
  .vs-kpi-row.vs-kpi-row--5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
  .vs-kpi-row.vs-kpi-row--5 > *, .vs-kpi-row.vs-kpi-row--5 > :nth-child(n+4) { grid-column: auto; }
}

/* One ground for the whole shell, the sidebar's: enterprise.css paints
   `main` slate (#f8fafc) for Violet's pages, which left the room record on a
   grey slab over the brand-tinted page (Nerea, 12-nov-2026). */
.vs-shell-main,
.vs-shell-main main { background: transparent; }
.vs-side__brand {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 16px 16px 12px;
  min-width: 0;
}
.vs-side__logo { width: 36px; height: 36px; object-fit: contain; flex-shrink: 0; }
.vs-side__initial {
  width: 36px;
  height: 36px;
  border-radius: var(--vs-radius);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  font-size: 16px;
  font-weight: 600;
  color: #fff;
  background: var(--color-primary);
}
.vs-side__name {
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--vs-ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.vs-side-link {
  display: flex;
  align-items: center;
  gap: 12px;
  height: 36px;
  padding: 0 12px;
  border-radius: var(--vs-radius);
  font-size: 13px;
  font-weight: 500;
  color: var(--vs-ink-2);
  text-decoration: none;
}
.vs-side-link > i { width: 18px; text-align: center; font-size: 15px; color: var(--vs-ink-3); flex-shrink: 0; }
.vs-side-link:hover { background: var(--vs-surface-3); color: var(--vs-ink); }
.vs-side-link[aria-current="page"] {
  background: var(--vs-surface);
  color: var(--vs-ink);
  font-weight: 600;
  box-shadow: inset 0 0 0 1px var(--vs-rule);
}
.vs-side-link[aria-current="page"] > i { color: var(--color-primary); }
.vs-side__sep { height: 1px; margin: 8px 12px; background: var(--vs-rule); }

/* ══════════════════════════════════════════════════════════════════════════
 * ROOMS: a busy button, a joined control, the email chip field, the cards
 * ══════════════════════════════════════════════════════════════════════════ */

/* components/button.html `busy`: while the button's own request is in flight
   (htmx puts htmx-request on it) its icon is a spinner. */
/* Font Awesome loads after this sheet and sets `display: var(--fa-display,
   inline-block)` on every glyph, which beat a plain `display: none` and left
   the spinner turning for good (Nerea, 12-nov-2026). The variable is FA's
   own switch, so it is set as well. */
.vs-btn-busy { --fa-display: none; display: none; }
.htmx-request > .vs-btn-busy { --fa-display: inline-block; display: inline-block; }
.htmx-request > .vs-btn-idle { --fa-display: none; display: none; }

/* A select and the button that acts on it, read as one control at one
   height: the room record's "30 days | Extend". */
/* components/_email_chips.html: a chip the browser already reads as a
   malformed address. Red is the datum here (it will be refused). */
.vs-chip.vs-email-chip--bad { background: #fef2f2; color: #991b1b; border-color: #fecaca; }

/* components/_record_card.html: an avatar of the people a record names. */
.vs-avatar {
  width: 28px;
  height: 28px;
  border-radius: 9999px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  font-size: 11px;
  font-weight: 600;
  color: var(--vs-ink-2);
  background: var(--vs-surface-3);
  box-shadow: 0 0 0 2px var(--vs-surface);
}
.vs-avatar--muted { color: var(--vs-ink-4); }

/* The sidebar's groups, each under its name (as Violet's rail), and Sign out
   at the very bottom, in red as there: it ends the session. */
.vs-side__group { padding: 12px 24px 4px; }
.vs-side__foot { margin-top: auto; padding: 8px 12px 12px; border-top: 1px solid var(--vs-rule); }
.vs-side-link--danger { width: 100%; background: transparent; border: 0; cursor: pointer; color: var(--vs-critical, #dc2626); }
.vs-side-link--danger > i { color: var(--vs-critical, #dc2626); }
.vs-side-link--danger:hover { background: #fef2f2; color: var(--vs-critical, #dc2626); }

/* components/_record_card.html: the foot keeps one height, people or not. */
.vs-record-card__foot { min-height: 52px; }
/* A card with `href`: the title's link covers the card, the actions sit above
   it (components/_record_card.html). The border answers the hover; nothing
   lifts or moves. */
.vs-record-card { position: relative; }
.vs-record-card--link .vs-record-card__link::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.vs-record-card--link .vs-record-card__link:focus-visible { outline: none; }
.vs-record-card--link:has(.vs-record-card__link:focus-visible) { box-shadow: 0 0 0 2px var(--color-primary); }
.vs-record-card--link { cursor: pointer; transition: border-color 0.15s ease; }
.vs-record-card--link:hover { border-color: color-mix(in srgb, var(--color-primary) 35%, var(--vs-rule)); }
.vs-record-card--link:hover .vs-record-card__link { color: var(--color-primary); }
.vs-record-card__above { position: relative; z-index: 1; }
/* The record's owner above its title (a room's client), never a grey aside. */
.vs-record-card__lead { display: flex; align-items: center; gap: .5rem; margin-bottom: .375rem; min-width: 0; }
.vs-record-card__leadLogo { height: 1.5rem; width: auto; max-width: 6rem; object-fit: contain; flex-shrink: 0; }
.vs-record-card__leadInitial {
  display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 1.5rem; height: 1.5rem; border-radius: 6px;
  background: var(--vs-ink-1, #111827); color: #fff; font-size: 12px; font-weight: 700;
}
.vs-record-card__leadName { font-size: 13px; font-weight: 700; color: var(--vs-ink-1, #111827); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ==========================================================================
 * Moved from components.css and project.css, 12-nov-2026: the client room
 * stands on the external base (portal/base.html), which loads this file and
 * not those, and its modal, filter bar and drill read the same as the
 * consultancy's. Loaded after components.css in base.html, so nothing changes
 * there.
 * ========================================================================== */

/* The page under an open modal does not scroll (components/modal.html). */
body.modal-open { overflow: hidden; }

/* ═══════════════════════════════════════════════════════════════════════════
   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);
}
/* A section whose pane holds a form error (components/_modal_section_tab.html). */
.vs-modal-nav__error {
  flex-shrink: 0; width: 6px; height: 6px; margin-left: auto;
  border-radius: 9999px; background: var(--vs-critical);
}
.vs-modal-nav__item > .ml-auto + .vs-modal-nav__error { margin-left: .375rem; }

/* 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); }

/* ── 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: 180px;
}
/* 150px still cut the Spanish labels ("Todas las audito…", Nerea 12-nov-2026:
   "que se lean bien todos los filtros"). The chevron already lives in the
   control's 2rem right padding, so the selected text may take the whole
   content box instead of losing another 1rem to shell.css's generic rule. */
.vs-filter .ts-wrapper.single .ts-control .item { max-width: 100%; }

.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;
}
/* The chevron of an enhanced select is drawn in a filter bar too (shell.css, Task 8h). */

/* 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;
}
/* A bar with many filters (`--wide`, 7+ boxes: Findings has 15). Beside the
   form, the ~740px action cluster left the grid half the card: five
   columns, four rows, and an empty block above the buttons. Here the form
   and its grid dissolve (`display: contents` — the <form> is still the same
   element, so submission and the fold are untouched) and the ROW becomes the
   grid: the filters take the full width, and the action cluster is its last
   cell, pinned to the last five columns. It sits at the end of the last row
   of filters when that row has room, and starts a row of its own when it
   does not. Below ~820px there are not five columns to span: full row. A
   short bar keeps the flex line above (form and actions side by side). */
.vs-filter--wide { container-type: inline-size; }
.vs-filter--wide .vs-filter__row {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(var(--vs-filter-min), 100%), 1fr));
  gap: 0.5rem;
  align-items: end;
}
.vs-filter--wide .vs-filter__form,
.vs-filter--wide .vs-filter__grid { display: contents; }
.vs-filter--wide .vs-filter__actions {
  grid-column: 1 / -1;
  justify-content: flex-end;
  margin-left: 0;
}
@container (min-width: 820px) {
  .vs-filter--wide .vs-filter__actions { grid-column: span 5 / -1; }
}
.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,
  .vs-filter--wide .vs-filter__row {
    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;
  }
}

/* ── 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; }

/* ── Enhanced Empty State ── */
.v-empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 3rem 1.5rem;
  text-align: center;
}
.v-empty-state-icon {
  width: 4.5rem;
  height: 4.5rem;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 1rem;
  /* Flat tint from the brand colour: the diagonal purple gradient was the
     one thing on an empty screen, and it read as generated. */
  background: color-mix(in srgb, var(--color-primary) 8%, white);
}
.v-empty-state-icon i {
  font-size: 1.5rem;
  color: color-mix(in srgb, var(--color-primary) 55%, white);
}
.v-empty-state-title {
  font-size: 1rem;
  font-weight: 600;
  color: #374151;
  margin-bottom: 0.25rem;
}
.v-empty-state-text {
  font-size: 0.875rem;
  color: #9CA3AF;
  max-width: 320px;
  margin-bottom: 1.25rem;
}
.v-empty-state-action {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 1.25rem;
  border-radius: 10px;
  font-size: 0.875rem;
  font-weight: 500;
  background: var(--btn-primary-bg, var(--color-primary));
  color: #fff;
  box-shadow: var(--btn-primary-shadow, none);
  transition: all 0.2s;
}
.v-empty-state-action:hover {
  background-color: color-mix(in srgb, var(--color-primary) 92%, black);
}

/* One LINE, for a panel that happens to be empty. The block above is the
   right answer for a whole screen with nothing on it; on an area detail with
   five roll-up panels it was five 200px voids stacked down the page. */
.v-empty-state--line {
  flex-direction: row;
  align-items: center;
  justify-content: flex-start;
  gap: 0.5rem;
  padding: 0.625rem 1rem;
  text-align: left;
}
.v-empty-state--line i { font-size: 0.8125rem; }
.v-empty-state--line .v-empty-state-title {
  font-size: 0.8125rem;
  font-weight: 500;
  color: #6b7280;
  margin-bottom: 0;
}
.v-empty-state--line .v-empty-state-text {
  font-size: 0.8125rem;
  max-width: none;
  margin-bottom: 0;
}
.v-empty-state--line .v-empty-state-action {
  margin-left: auto;
  padding: 0.25rem 0.75rem;
  font-size: 0.75rem;
  border-radius: 8px;
  box-shadow: none;
}

/* ==========================================================================
 * CVSS DIAL — components/_cvss_dial.html
 * ==========================================================================
 * A finding's CVSS as a ring filled to score/10 in its CVSS band's token,
 * the number in the hole, the band named beside it. No score: an empty ring
 * and a sentence, never a 0. */
.vs-cvss-dial {
  display: inline-flex; align-items: center; gap: .875rem;
  padding: .625rem 1rem; border: 1px solid var(--vs-rule); border-radius: var(--vs-radius);
  background: var(--vs-surface);
}
.vs-cvss-dial__ring { position: relative; width: 64px; height: 64px; flex-shrink: 0; }
.vs-cvss-dial__ring svg { width: 64px; height: 64px; transform: rotate(-90deg); }
.vs-cvss-dial__num {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-size: 18px; font-weight: 800; line-height: 1; color: var(--vs-ink);
}
.vs-cvss-dial__band { font-size: 13px; font-weight: 600; line-height: 1.3; }

/* ==========================================================================
 * FINDING HEADER — rooms/client/finding.html
 * ==========================================================================
 * Title, badges and facts on the left; the CVSS dial and the finding's one
 * action on the right, under the left column when the card is narrow. */
.vs-finding-head { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: 1rem 1.5rem; }
.vs-finding-head__main { flex: 1 1 22rem; min-width: 0; }
.vs-finding-head__side { flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; gap: .75rem; }
.vs-finding-head__facts {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
  gap: .75rem 1.5rem; margin-top: 1rem; padding-top: .875rem; border-top: 1px solid var(--vs-surface-3);
}
@media (max-width: 639.98px) {
  .vs-finding-head__side { align-items: stretch; width: 100%; }
}

/* ==========================================================================
 * DRILL DRAWER — components/_drill_drawer.html
 * ==========================================================================
 * The side panel a figure opens. A dialog like the modal: a backdrop that
 * does not close it, the panel sliding in from the right (the slide is the
 * panel arriving, not decoration), a close button and Escape. */
.vs-drawer { position: fixed; inset: 0; z-index: 45; visibility: hidden; pointer-events: none; }
.vs-drawer--open { visibility: visible; pointer-events: auto; }
.vs-drawer__backdrop {
  position: absolute; inset: 0; background: rgba(15, 23, 42, .28);
  opacity: 0; transition: opacity .2s ease;
}
.vs-drawer--open .vs-drawer__backdrop { opacity: 1; }
.vs-drawer__panel {
  position: absolute; top: 0; right: 0; bottom: 0; width: min(32rem, 100vw);
  display: flex; flex-direction: column; background: var(--vs-surface);
  border-left: 1px solid var(--vs-rule); box-shadow: -8px 0 24px rgba(15, 23, 42, .08);
  transform: translateX(100%); transition: transform .2s ease; outline: none;
}
.vs-drawer--open .vs-drawer__panel { transform: translateX(0); }
.vs-drawer__close {
  position: absolute; top: .875rem; right: .875rem; z-index: 1;
  width: 2.25rem; height: 2.25rem; display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--vs-rule); border-radius: var(--vs-radius); background: var(--vs-surface);
  color: var(--vs-ink-3); cursor: pointer;
}
.vs-drawer__close:hover { color: var(--vs-ink); background: var(--vs-surface-2); }
.vs-drawer__close:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 1px; }
.vs-drawer__content { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.vs-drawer__body { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.vs-drawer__head { padding: 1.25rem 4rem 1rem 1.25rem; border-bottom: 1px solid var(--vs-surface-3); }
.vs-drawer__list { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.vs-drawer__list > li + li { border-top: 1px solid var(--vs-surface-3); }
.vs-drawer__item { display: flex; align-items: flex-start; gap: .75rem; padding: .75rem 1.25rem; }
.vs-drawer__item:hover { background: var(--vs-surface-2); }
.vs-drawer__item:focus-visible { outline: 2px solid var(--color-primary); outline-offset: -2px; }
.vs-drawer__foot {
  display: flex; align-items: center; justify-content: flex-end; gap: 1rem;
  padding: .875rem 1.25rem; border-top: 1px solid var(--vs-rule); background: var(--vs-surface-2);
}
@media (prefers-reduced-motion: reduce) {
  .vs-drawer__panel, .vs-drawer__backdrop { transition: none; }
}

/* ==========================================================================
 * SEVERITY COUNTS — components/_severity_count_row.html
 * ==========================================================================
 * One cell per band: a round letter badge in the band's tokens (.vs-sev--*),
 * the figure, the band's name. A cell with findings opens them. */
.vs-sevcount { display: grid; grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr)); gap: .5rem; }
.vs-sevcount__cell {
  display: flex; align-items: center; gap: .625rem; width: 100%; text-align: left;
  padding: .625rem .75rem; border: 1px solid var(--vs-rule); border-radius: var(--vs-radius);
  background: var(--vs-surface); font: inherit; color: inherit;
}
button.vs-sevcount__cell { cursor: pointer; }
button.vs-sevcount__cell:hover { border-color: color-mix(in srgb, var(--color-primary) 45%, var(--vs-rule)); }
.vs-sevcount__cell--zero .vs-sevcount__num { color: var(--vs-ink-4); }
.vs-sevcount__letter {
  width: 2rem; height: 2rem; flex-shrink: 0; border-radius: 9999px; border-width: 1px; border-style: solid;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 700; line-height: 1;
}
.vs-sevcount__text { display: flex; flex-direction: column; min-width: 0; }
.vs-sevcount__num { font-size: 20px; font-weight: 700; line-height: 1.1; color: var(--vs-ink); }
.vs-sevcount__label { font-size: 12px; color: var(--vs-ink-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ==========================================================================
 * SEVERITY DONUT — rooms/client/summary.html
 * ==========================================================================
 * The ring, the total in its hole (an overlay that lets clicks through to
 * the slices), and the legend UNDER it, one row per severity: the legend
 * used to be drawn by the chart over the ring itself. */
.vs-donut__chart { position: relative; }
.vs-donut__center {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; pointer-events: none;
}
.vs-donut__total { font-size: 28px; font-weight: 700; line-height: 1; color: var(--vs-ink); }
.vs-donut__unit { margin-top: .25rem; font-size: 12px; color: var(--vs-ink-3); }
.vs-donut__legend { margin-top: .75rem; display: grid; grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr)); gap: .125rem .75rem; }
.vs-donut__row {
  display: flex; align-items: center; gap: .5rem; width: 100%; padding: .3125rem .375rem;
  border-radius: var(--vs-radius); font: inherit; font-size: 13px; color: var(--vs-ink-2);
  background: none; border: 0; text-align: left; cursor: pointer;
}
.vs-donut__row:hover { background: var(--vs-surface-2); }
.vs-donut__dot { width: .625rem; height: .625rem; border-radius: 9999px; flex-shrink: 0; }
.vs-donut__name { flex: 1 1 auto; min-width: 0; }
.vs-donut__count { font-weight: 600; color: var(--vs-ink); }
.vs-donut__pct { width: 2.75rem; text-align: right; color: var(--vs-ink-4); }

/* ==========================================================================
 * SUMMARY KPI ROW — rooms/client/summary.html
 * ==========================================================================
 * Six tiles in one row; three, then two, as the page narrows. */
.vs-kpi-row { display: grid; gap: .75rem; grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (min-width: 768px) { .vs-kpi-row { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 1280px) { .vs-kpi-row { grid-template-columns: repeat(6, minmax(0, 1fr)); } }

/* ==========================================================================
 * COMPACT FILTER BAR — the client portal's list (rooms/client/vulnerabilities.html)
 * ==========================================================================
 * One row, no labels above: each control names itself ("All severities",
 * "Order: most severe first"). The same controls and metrics as the full
 * bar (.vs-filter-control at --vs-filter-h, Tom Select inside .vs-filter);
 * the count sits on the right. On a phone the row wraps. */
.vs-filter-compact {
  display: flex; flex-wrap: wrap; align-items: center; gap: .5rem;
  padding: .625rem .75rem; background: var(--vs-surface);
  border: 1px solid var(--vs-rule); border-radius: var(--vs-radius);
}
.vs-filter-compact__search { flex: 1 1 16rem; max-width: 24rem; min-width: 12rem; }
/* Each select is as wide as its own label (Nerea, 12-nov-2026: "que se lean
   bien todos los filtros"): a fixed width cut "Order: most severe first" to
   "Order: most severe fi…", and the Spanish labels are longer still. */
.vs-filter-compact__select { width: auto; min-width: 11rem; flex: 0 0 auto; }
.vs-filter-compact .ts-wrapper.vs-filter-compact__select { width: auto; min-width: 11rem; flex: 0 0 auto; }
.vs-filter-compact .ts-wrapper.single .ts-control .item { max-width: none; overflow: visible; }
.vs-filter-compact__count { margin-left: auto; font-size: 12px; color: var(--vs-ink-3); white-space: nowrap; }
@media (max-width: 639.98px) {
  .vs-filter-compact__search, .vs-filter-compact__select,
  .vs-filter-compact .ts-wrapper.vs-filter-compact__select { flex: 1 1 100%; width: 100%; max-width: none; }
  .vs-filter-compact__count { margin-left: 0; }
}

/* ==========================================================================
 * READING CARDS — a finding's sections in the client's room
 * ==========================================================================
 * Description, Where, Technical, Evidence, Recommendation and Comments are
 * what the client comes to read: a 15px / 600 title (components/_card_head.html
 * with strong=True) and 14px text with a comfortable line height. */
.vs-section-title { font-size: 15px; font-weight: 600; line-height: 1.35; color: var(--vs-ink); }
.vs-section-text { font-size: 14px; line-height: 1.65; color: var(--vs-ink-2); }

/* A finding "In review" (and a dispute waiting for the consultancy): the
   consultancy has it, so the lozenge wears the consultancy's colour; the
   state tokens have no second blue beside In remediation's. Both shells draw
   .vs-lozenge (components.css, and the client room's base_room.html). */
.vs-lozenge--review { background: color-mix(in srgb, var(--color-primary) 12%, #fff); color: var(--color-primary); }

/* A finding's progress inside its header (rooms/client/finding.html): a thin
   strip under the facts, one line high (components/_stepper.html compact). */
.vs-finding-head__progress { margin-top: .75rem; padding-top: .625rem; border-top: 1px solid var(--vs-surface-3); }

/* ==========================================================================
 * PENDING FROM THE CLIENT (Task 8j) and the client's notices
 * ==========================================================================
 * What waits on the consultancy wears the warning state token, never a
 * severity's red: the bell's count (rooms/_pending_bell.html), a room card's
 * "N pending" (rooms/_room_card_actions.html) and a record tab's counter
 * (components/_sub_nav_tab.html, `count`). A client's unseen answer is a dot
 * in the consultancy's colour on its tab (rooms/client/base_room.html). */
.vs-count--warn {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px;
  font-size: 11px; font-weight: 700; line-height: 1; font-variant-numeric: tabular-nums;
  background: var(--vs-warn); color: #fff;
}
.vs-chip.vs-chip--warn { background: var(--vs-warn-bg); border-color: var(--vs-warn-line); color: #92400e; text-decoration: none; }
/* Room state at a glance (Nerea, 12-nov-2026): open green, closed filled
   dark. The colour is the datum here, not decoration. */
.vs-chip.vs-chip--ok { background: #ecfdf5; border-color: #a7f3d0; color: #065f46; }
.vs-chip.vs-chip--closed { background: #1f2937; border-color: #1f2937; color: #f9fafb; }
.vs-chip.vs-chip--closed i { color: #d1d5db; }
.vs-topbar-btn > .fa-bell { font-size: 15px; color: var(--vs-ink-4); }
.vs-bell { position: relative; }
.vs-bell .vs-count--warn { position: absolute; top: 0; left: 18px; min-width: 16px; height: 16px; font-size: 10px; padding: 0 4px; }
.vs-menu__row.vs-menu__row--tall { height: auto; align-items: flex-start; padding-top: 8px; padding-bottom: 8px; }
.vs-menu__row--tall > :first-child { font-size: 14px; margin-top: 2px; }
.vs-menu__head { padding: 10px 12px 6px; font-size: 13px; font-weight: 600; color: var(--vs-ink); }
.vs-notice-dot { display: inline-block; width: 7px; height: 7px; border-radius: 9999px; background: var(--color-primary); flex-shrink: 0; }

/* ==========================================================================
 * A row that is the way into a record (components/simple_table.html,
 * row_emphasis): a clearer tint on hover and while focus is inside it, its
 * title (`data-row-title`) and its chevron in the primary colour. Colour
 * only: nothing moves (CLAUDE.md, no motion without meaning).
 * The record's Vulnerabilities (rooms/_room_findings.html, Task 8r) draws
 * its filter bar and bulk bar above the table with these.
 * ========================================================================== */
.vs-row-open:hover, .vs-row-open:focus-within { background: color-mix(in srgb, var(--color-primary) 8%, white); }
.vs-row-open:hover [data-row-title], .vs-row-open:focus-within [data-row-title] { color: var(--color-primary); text-decoration: underline; }
.vs-row-open:hover .vs-row-open__chevron { color: var(--color-primary); }
.vs-bulk-strip {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 8px 12px; border: 1px solid var(--vs-rule); border-radius: 8px;
  background: color-mix(in srgb, var(--color-primary) 5%, white);
  font-size: 13px; color: var(--vs-ink);
}
.vs-bulk-strip__count { font-weight: 600; font-variant-numeric: tabular-nums; margin-right: 4px; }

/* =============================================================================
 * CLIENT ROOM POLISH (Nerea, 12-nov-2026) — one section, appended whole.
 * =============================================================================
 * 1. Density. "Se ve algo lejos todo, como con mucho zoom": the room's page
 *    reads at ~1200px centred, card paddings one step down (p-5/p-6 -> p-4),
 *    section gaps from space-y-6 to space-y-4, the KPI figure at 22px. All
 *    of it scoped to the room's page (#room-shell, [data-room-page]), never
 *    the modal or the drawer, so no client template needs re-touching.
 * ========================================================================== */
#room-shell #room-nav,
#room-shell #content-area > main { max-width: 1200px; margin-left: auto; margin-right: auto; }
#room-shell [data-room-page] .px-5 { padding-left: 1rem; padding-right: 1rem; }
#room-shell [data-room-page] .sm\:px-6 { padding-left: 1rem; padding-right: 1rem; }
#room-shell section[data-room-page] > :not([hidden]) ~ :not([hidden]) { margin-top: 1rem; }
#room-shell [data-room-page] .space-y-6 > :not([hidden]) ~ :not([hidden]) { margin-top: 1rem; }
#room-shell [data-room-page] .gap-6 { gap: 1rem; }
#room-shell [data-room-page] .vs-kpi__num { font-size: 22px; }
/* 2. One label is a label; six are a grammar (CLAUDE.md). The summary's KPI
 *    labels read in sentence case here, not as a row of uppercase tracked
 *    eyebrows; the figure's colour still comes only from a severity token. */
#room-shell [data-room-kpis] .vs-kpi__label { text-transform: none; letter-spacing: 0; font-size: 12px; }
/* 3. The finding header's one compact row (rooms/client/finding.html): the
 *    facts share the line with the CVSS dial, the dial loses its own box and
 *    shrinks, and the two actions stand as a tidy vertical pair beside it
 *    instead of overflowing under it. */
.vs-finding-row { display: flex; flex-wrap: wrap; align-items: center; gap: .625rem 1.5rem; padding-top: .625rem; border-top: 1px solid var(--vs-surface-3); }
.vs-finding-row__facts { flex: 1 1 16rem; min-width: 0; display: flex; flex-wrap: wrap; gap: .5rem 2rem; align-items: flex-start; }
.vs-finding-row__side { display: flex; align-items: center; gap: 1.25rem; margin-left: auto; }
.vs-finding-row__side .vs-cvss-dial { border: 0; background: transparent; padding: 0; gap: .625rem; }
.vs-finding-row__side .vs-cvss-dial__ring,
.vs-finding-row__side .vs-cvss-dial__ring svg { width: 56px; height: 56px; }
.vs-finding-row__side .vs-cvss-dial__num { font-size: 15px; }
.vs-finding-row__actions { display: flex; flex-direction: column; align-items: stretch; gap: .375rem; min-width: 10.5rem; }
.vs-finding-row__actions a,
.vs-finding-row__actions button { width: 100%; justify-content: center; }
@media (max-width: 639.98px) {
  .vs-finding-row__side { width: 100%; justify-content: space-between; margin-left: 0; }
}
