/* QSA portal design system.
 *
 * Every colour, size and radius is a token in :root. Components read tokens and
 * never hardcode values, so a rebrand or a dark mode is an edit to this block
 * rather than a sweep through the file.
 *
 * The governing rule: the only saturated colour on screen is a verdict.
 * Everything else is greyscale plus one accent. An assessor scanning a hundred
 * findings should have their eye pulled by FAIL and NOT ASSESSED, not by a
 * button, a heading, or a table header. That is why buttons are quiet by
 * default and why there is at most one filled button per view.
 */

:root {
  color-scheme: light;

  /* Surfaces, lightest last */
  --bg:            #f6f7f9;
  --surface:       #ffffff;
  --surface-sunk:  #f1f3f6;
  --surface-hover: #f7f8fa;

  /* Ink. Every step is contrast-checked against --surface; see
     tests/test_contrast.py, which fails the build rather than leaving it to
     whoever picked the colours. */
  --ink:      #11151a;
  --ink-2:    #4a525c;
  --ink-3:    #67707c;

  --line:        #e3e7ec;
  --line-strong: #cbd2da;

  /* One accent: links, focus, and the single primary action per view */
  --accent:      #1d4ed8;
  --accent-ink:  #1e40af;
  --accent-wash: #eef3ff;
  --accent-on:   #ffffff;

  /* Verdicts. Tinted surface with saturated ink rather than a solid block:
     legible at small sizes, and four solid badges per row is noise. */
  --pass: #146c37;  --pass-wash: #e9f6ee;  --pass-line: #b9e0c8;
  --fail: #a4231a;  --fail-wash: #fdeceb;  --fail-line: #f4c6c2;
  --warn: #855b06;  --warn-wash: #fdf4e3;  --warn-line: #eedaa8;
  --err:  #5b21b6;  --err-wash:  #f3edfd;  --err-line:  #d9c9f5;

  --chrome:     #171c23;
  --chrome-ink: #ffffff;

  /* Intentionally absent from the dark block below. See .qr. */
  --qr-plate: #ffffff;
  --qr-ink:   #000000;

  /* Type. System stack: native everywhere, nothing to self-host, and no
     font-src exception needed in the CSP. */
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  /* Bumped a step from the first pass: this is a tool people read all day,
     and 11.5px table text was too small to work in comfortably. */
  --t-xs: 12px;
  --t-sm: 13px;
  --t-md: 14.5px;
  --t-lg: 16.5px;
  --t-xl: 21px;

  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px;
  --s5: 24px; --s6: 32px; --s7: 48px;

  --r-sm: 5px;
  --r:    7px;
  --r-lg: 11px;

  --shadow-pop:   0 1px 2px rgba(16,22,30,.05), 0 8px 28px rgba(16,22,30,.10);
  --shadow-modal: 0 24px 64px rgba(16,22,30,.26);
  --focus: 0 0 0 3px rgba(29,78,216,.25);
}

/* Dark mode.
 *
 * Only the token block changes; no component rule below knows which mode it is
 * in. Two entry points: the OS preference, and an explicit choice the server
 * renders onto <html> from a cookie. The explicit choice wins, and because it
 * is server-rendered there is no flash of the wrong theme on load, which an
 * inline script would have caused and which the CSP forbids anyway.
 */
:root[data-theme="dark"],
:root:not([data-theme="light"]) {
  --dark-ready: 1;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { color-scheme: dark;
    --bg:            #0e1116;
    --surface:       #161a20;
    --surface-sunk:  #1c222a;
    --surface-hover: #1b2028;
    --ink:           #e9ecf1;
    --ink-2:         #b3bcc8;
    --ink-3:         #8b95a3;
    --line:          #262d37;
    --line-strong:   #3a434f;
    --accent:        #85b4ff;
    --accent-ink:    #a6c8ff;
    --accent-wash:   #15243c;
    --accent-on:     #0a1120;
    --pass: #6ee7a0;  --pass-wash: #0f2419;  --pass-line: #1f4a33;
    --fail: #ff9b91;  --fail-wash: #2b1414;  --fail-line: #5c2523;
    --warn: #f6c352;  --warn-wash: #2a2010;  --warn-line: #574318;
    --err:  #c9b3fb;  --err-wash:  #201a34;  --err-line:  #40356b;
    --chrome:     #0a0d12;
    --chrome-ink: #f2f4f7;
    --shadow-pop:   0 1px 2px rgba(0,0,0,.5), 0 8px 28px rgba(0,0,0,.55);
    --shadow-modal: 0 24px 64px rgba(0,0,0,.7);
    --focus: 0 0 0 3px rgba(133,180,255,.32);
  }
}

:root[data-theme="dark"] { color-scheme: dark;
  --bg:            #0e1116;
  --surface:       #161a20;
  --surface-sunk:  #1c222a;
  --surface-hover: #1b2028;
  --ink:           #e9ecf1;
  --ink-2:         #b3bcc8;
  --ink-3:         #8b95a3;
  --line:          #262d37;
  --line-strong:   #3a434f;
  --accent:        #85b4ff;
  --accent-ink:    #a6c8ff;
  --accent-wash:   #15243c;
  --accent-on:     #0a1120;
  --pass: #6ee7a0;  --pass-wash: #0f2419;  --pass-line: #1f4a33;
  --fail: #ff9b91;  --fail-wash: #2b1414;  --fail-line: #5c2523;
  --warn: #f6c352;  --warn-wash: #2a2010;  --warn-line: #574318;
  --err:  #c9b3fb;  --err-wash:  #201a34;  --err-line:  #40356b;
  --chrome:     #0a0d12;
  --chrome-ink: #f2f4f7;
  --shadow-pop:   0 1px 2px rgba(0,0,0,.5), 0 8px 28px rgba(0,0,0,.55);
  --shadow-modal: 0 24px 64px rgba(0,0,0,.7);
  --focus: 0 0 0 3px rgba(133,180,255,.32);
}

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  font-family: var(--font);
  font-size: var(--t-md);
  line-height: 1.55;
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

h1, h2, h3 { margin: 0; font-weight: 600; letter-spacing: -0.011em; }
h1 { font-size: var(--t-xl); }
h2 { font-size: var(--t-lg); }
h3 { font-size: var(--t-md); }

.muted  { color: var(--ink-2); }
.small  { font-size: var(--t-sm); }
.hint   { color: var(--ink-3); font-size: var(--t-sm); font-weight: 400; }
.mono   { font-family: var(--mono); font-size: var(--t-sm); }

/* Timestamps. The zone recedes: it is there so the time is unambiguous, not
   because anyone is reading it. Both parts stay on one line, because a
   timestamp split across two is worse than no zone at all. */
time { white-space: nowrap; }
time .tz { color: var(--ink-3); font-size: 0.85em; font-weight: 400;
           letter-spacing: .02em; }
.grow   { flex: 1; }

/* ---------------------------------------------------------------- chrome */

.topbar {
  display: flex; align-items: center; gap: var(--s5);
  background: var(--chrome); color: var(--chrome-ink);
  padding: 0 var(--s5); height: 48px;
  /* Pinned. On a long page the navigation was scrolling away, which on a
     browsable Drive export means scrolling back to the top to get anywhere.
     A no-op on the two-pane pages, where the body does not scroll at all, so
     one rule covers both. Above the cards, below the modals, which sit in the
     browser's top layer and are not affected by z-index here. */
  position: sticky; top: 0; z-index: 20;
}
.topbar a { color: color-mix(in srgb, var(--chrome-ink) 74%, transparent);
            font-size: var(--t-md); }
.topbar a:hover { color: var(--chrome-ink); text-decoration: none; }
.brand { font-weight: 600; color: var(--chrome-ink) !important; letter-spacing: -0.01em; }
.topbar nav { display: flex; gap: var(--s4); flex: 1; }
.who { color: color-mix(in srgb, var(--chrome-ink) 58%, transparent);
       font-size: var(--t-sm); margin-right: var(--s2); }
.who em { font-style: normal;
          color: color-mix(in srgb, var(--chrome-ink) 42%, transparent); }

main { max-width: 1180px; margin: var(--s5) auto var(--s7); padding: 0 var(--s5); }
footer { max-width: 1180px; margin: 0 auto var(--s6); padding: 0 var(--s5);
         color: var(--ink-3); font-size: var(--t-sm); }

.crumbs { color: var(--ink-3); font-size: var(--t-sm); margin: 0 0 var(--s1); }
.crumbs a { color: var(--ink-2); }

.page-head { display: flex; align-items: baseline; gap: var(--s3);
             margin: 0 0 var(--s4); flex-wrap: wrap; }

/* ---------------------------------------------------------------- cards */

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: var(--s4) var(--s5);
  margin-bottom: var(--s4);
}
.card > h2, .card > h3 { margin-bottom: var(--s3); }
.card.narrow { max-width: 380px; margin: 12vh auto; padding: var(--s5); }

/* ---------------------------------------------------------------- forms */

label { display: block; margin-bottom: var(--s3); font-size: var(--t-sm);
        font-weight: 500; color: var(--ink-2); }

input, select, textarea {
  width: 100%; margin-top: var(--s1);
  padding: 7px 10px;
  font: inherit; font-size: var(--t-md); font-weight: 400;
  color: var(--ink); background: var(--surface);
  border: 1px solid var(--line-strong); border-radius: var(--r);
  transition: border-color .12s, box-shadow .12s;
}
input:hover, select:hover, textarea:hover { border-color: var(--ink-3); }
input:focus, select:focus, textarea:focus {
  outline: none; border-color: var(--accent); box-shadow: var(--focus);
}
input::placeholder, textarea::placeholder { color: var(--ink-3); }
select { appearance: none; padding-right: 26px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%2367707c' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 9px center; }
input[type=file] { padding: 6px; border-style: dashed; background: var(--surface-sunk);
                   color: var(--ink); }

.row { display: flex; gap: var(--s2); align-items: flex-end; flex-wrap: wrap; }
.row input, .row select { width: auto; min-width: 190px; margin-top: 0; }
.inline { display: inline; }

/* -------------------------------------------------------------- buttons */
/* Quiet by default. .primary is the one filled button a view is allowed. */

button, .btn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 12px;
  font: inherit; font-size: var(--t-md); font-weight: 500;
  color: var(--ink); background: var(--surface);
  border: 1px solid var(--line-strong); border-radius: var(--r);
  cursor: pointer; white-space: nowrap;
  transition: background .12s, border-color .12s, box-shadow .12s;
}
button:hover, .btn:hover { background: var(--surface-hover); border-color: var(--ink-3);
                           text-decoration: none; }
button:active, .btn:active { background: var(--surface-sunk); }
button:focus-visible, .btn:focus-visible { outline: none; box-shadow: var(--focus); }

.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-on); }
.primary:hover { background: var(--accent-ink); border-color: var(--accent-ink); }
.primary:active { background: var(--accent-ink); }

.ghost { background: transparent; border-color: transparent; color: var(--accent);
         padding: 4px 6px; }
.ghost:hover { background: var(--accent-wash); border-color: transparent; }

button.link, .link { background: none; border: 0; color: var(--accent);
                     padding: 2px 4px; font-weight: 500; }
button.link:hover { background: none; text-decoration: underline; }
/* The one destructive link in the portal. Coloured as a verdict rather than as
   an accent, because the rule everywhere else is that saturated colour means a
   result: here it means the row goes and does not come back. */
button.link.danger { color: var(--fail); }

.btn.small, button.small { padding: 3px 8px; font-size: var(--t-sm); }

/* --------------------------------------------------------------- tables */

table.grid {
  width: 100%; border-collapse: collapse;
  background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--r-lg);
  overflow: hidden; margin-bottom: var(--s4);
}
table.grid th {
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  text-align: left; padding: 9px var(--s3);
  font-size: var(--t-xs); font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--ink-3);
  white-space: nowrap;
}
table.grid td { padding: 10px var(--s3); border-top: 1px solid var(--line);
                vertical-align: middle; }
table.grid tbody tr:first-child td { border-top: 0; }
table.grid tbody tr:hover { background: var(--surface-hover); }
table.tight td, table.tight th { padding: 7px var(--s3); }

/* Findings table. Column widths are fixed so every requirement group lines
   up with the next, instead of each table sizing itself independently. */
table.findings { table-layout: fixed; }
table.findings td { font-size: var(--t-sm); vertical-align: top;
                    padding-top: 10px; padding-bottom: 10px; }
/* Wide enough for "NOT ASSESSED" at --t-xs plus pill padding. With
   table-layout: fixed a nowrap pill silently overflows into the next column
   rather than widening the cell, which is exactly what happened before. */
.col-status   { width: 136px; }
.col-region   { width: 118px; }
.col-account  { width: 136px; }
.col-integrity { width: 112px; }
.col-counts   { width: 172px; }
.col-role     { width: 130px; }
.col-active   { width: 88px; }
/* Trimmed to pay for the columns above. A rendered timestamp is "11 Aug 2026,
   14:23", which does not need 190px. */
.col-lastlogin { width: 176px; }
.col-useractions { width: 210px; }
/* Sized to the header, not to the cell. These two were sized to their contents
   ("S3", a "JSON" link), which is the narrower of the two and the wrong one:
   "SERVICE" and "EVIDENCE" are 8 uppercase characters at --t-xs with letter
   spacing, about 63px, and 24px of cell padding on top. Under-size them and the
   nowrap header runs out of its column and lands on top of the next heading. */
.col-service  { width: 96px; }
.col-resource { width: 160px; }
.col-check    { width: 146px; }
.col-evidence { width: 104px; }
.col-assessor { width: 220px; }
/* The guard, so this is a layout to fix rather than a collision to read. A
   header too long for its column is clipped; two headings printed over each
   other are unreadable, and worse, they look like a data problem. */
table.findings th { overflow: hidden; text-overflow: ellipsis; }
td.detail { color: var(--ink-2); line-height: 1.55; }
td.resource { font-family: var(--mono); font-size: var(--t-sm); color: var(--ink);
              overflow-wrap: anywhere; }

/* ----------------------------------------------------------------- meta */

dl.meta { display: grid; grid-template-columns: 150px 1fr; gap: 2px var(--s3);
          margin: 0 0 var(--s3); font-size: var(--t-sm); }
dl.meta dt { color: var(--ink-3); }
dl.meta dd { margin: 0; }

/* ---------------------------------------------------------------- pills */

.pill {
  display: inline-block; max-width: 100%; padding: 3px 9px; border-radius: 20px;
  overflow: hidden; text-overflow: ellipsis; vertical-align: middle;
  font-size: var(--t-xs); font-weight: 600; letter-spacing: .01em;
  white-space: nowrap; border: 1px solid transparent;
}
.pill.pass { color: var(--pass); background: var(--pass-wash); border-color: var(--pass-line); }
.pill.fail { color: var(--fail); background: var(--fail-wash); border-color: var(--fail-line); }
.pill.warn { color: var(--warn); background: var(--warn-wash); border-color: var(--warn-line); }
.pill.err  { color: var(--err);  background: var(--err-wash);  border-color: var(--err-line); }
a.pill:hover { text-decoration: none; filter: brightness(.97); }

.tag { background: var(--surface-sunk); color: var(--ink-2); padding: 2px 8px;
       border-radius: var(--r-sm); font-size: var(--t-xs); font-weight: 500; }

/* Count chips used as filters above the findings table */
.counts { display: flex; gap: var(--s2); flex-wrap: wrap; align-items: center; }
.count {
  display: inline-flex; align-items: baseline; gap: 6px;
  padding: 5px 11px; border-radius: var(--r);
  border: 1px solid var(--line); background: var(--surface);
  font-size: var(--t-sm); color: var(--ink-2);
}
.count:hover { border-color: var(--line-strong); text-decoration: none; }
.count b { font-size: var(--t-md); font-weight: 600; color: var(--ink); }
.count.on { border-color: var(--accent); background: var(--accent-wash); }
.count .dot { width: 7px; height: 7px; border-radius: 50%; align-self: center; }
.dot.pass { background: var(--pass); } .dot.fail { background: var(--fail); }
.dot.warn { background: var(--warn); } .dot.err  { background: var(--err); }

/* --------------------------------------------------------------- alerts */

.alert { padding: 10px var(--s3); border-radius: var(--r);
         font-size: var(--t-sm); margin: var(--s3) 0; border: 1px solid; }
.alert.pass { color: var(--pass); background: var(--pass-wash); border-color: var(--pass-line); }
.alert.fail { color: var(--fail); background: var(--fail-wash); border-color: var(--fail-line); }
.alert.warn { color: var(--warn); background: var(--warn-wash); border-color: var(--warn-line); }
.alert.err  { color: var(--err); background: var(--err-wash);  border-color: var(--err-line); }

/* -------------------------------------------------- assessor note editor */

.adjudicate { display: flex; gap: 6px; align-items: center; }
.adjudicate select { width: 112px; flex: none; margin-top: 0;
                     padding: 4px 22px 4px 8px; font-size: var(--t-sm); }
/* The Save button only exists for browsers where the note dialog cannot run.
   portal.js sets .has-dialog once it is enhancing, and then the dropdown
   commits on change instead. */
.has-dialog .adjudicate .save-fallback { display: none; }

.note-open {
  flex: 1; min-width: 0;
  padding: 4px 8px; font-size: var(--t-sm); font-weight: 400;
  color: var(--ink-3); background: var(--surface);
  border: 1px dashed var(--line-strong);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  display: block; text-align: left;
}
.note-open:hover { color: var(--ink); background: var(--surface-hover); }
.note-open.has-note { color: var(--ink); border-style: solid; background: var(--surface); }

/* ----------------------------------------------------------------- sheets */
/* One modal shape, used by the note editor and by every action form. Head,
   body and a footer holding the buttons. Native <dialog>, so focus trapping,
   Escape and the backdrop come from the browser rather than from us. */

dialog.sheet {
  width: min(680px, calc(100vw - 32px));
  padding: 0; border: 0; border-radius: var(--r-lg);
  background: var(--surface); color: var(--ink);
  box-shadow: var(--shadow-modal);
}
dialog.sheet::backdrop { background: rgba(16,22,30,.42); }
.sheet-head { padding: var(--s4) var(--s5) var(--s3);
              border-bottom: 1px solid var(--line); }
.sheet-head h3 { font-size: var(--t-lg); margin-bottom: 6px; }
.sheet-head p { margin: 0; display: flex; align-items: center;
                gap: var(--s2); flex-wrap: wrap; }
.sheet-head p.hint { display: block; }
.sheet-body { padding: var(--s4) var(--s5); }
.sheet-body label { margin-bottom: var(--s2); }
.sheet-body label + label { margin-top: var(--s3); }
.sheet-body > p:last-child { margin-bottom: 0; }
.sheet-foot {
  display: flex; align-items: center; gap: var(--s2);
  padding: var(--s3) var(--s5) var(--s4);
  border-top: 1px solid var(--line); background: var(--surface-sunk);
  border-radius: 0 0 var(--r-lg) var(--r-lg);
}
dialog#note-dialog textarea {
  min-height: 260px; resize: vertical; line-height: 1.6; padding: var(--s3);
  font-size: var(--t-md);
}

/* A row of buttons, each opening one of the sheets above. */
.actions { display: flex; flex-wrap: wrap; gap: var(--s2); margin: 0 0 var(--s4); }

/* Without scripting there is nothing to open a modal, so the sheet is shown as
   an ordinary card instead and its trigger is hidden. The forms inside are
   plain posts with real submit buttons, so they work either way; only the
   modal is lost. .no-modal covers a browser that has JavaScript but not
   <dialog>, which is the same situation arrived at differently. */
html:not(.has-js) dialog.form-dialog,
html.no-modal dialog.form-dialog {
  display: block; position: static; margin: 0 0 var(--s4);
  max-width: 620px; box-shadow: none; border: 1px solid var(--line);
}
html:not(.has-js) .modal-open, html.no-modal .modal-open,
html:not(.has-js) .sheet-cancel, html.no-modal .sheet-cancel { display: none; }

/* ------------------------------------------------------------- assistant */
/* The scope is stated at the top of the conversation, not only enforced in the
   query. On a portal holding several clients' evidence, an assessor who cannot
   see which client an answer is about has to take it on trust, and that is the
   wrong thing to ask of them. */

.scopebar { display: flex; align-items: center; gap: var(--s2);
            margin: 0 0 var(--s4); flex-wrap: wrap; }
.pill.scope { background: var(--accent-wash); color: var(--accent);
              border-color: var(--accent-wash); font-weight: 600; }

.chat { display: flex; flex-direction: column; gap: var(--s3);
        margin-bottom: var(--s4); }
.turn { display: flex; }
.turn.you { justify-content: flex-end; }
.turn.claude { flex-direction: column; align-items: flex-start; }
.turn .bubble {
  max-width: 74ch; padding: var(--s3) var(--s4);
  border: 1px solid var(--line); border-radius: var(--r-lg);
  background: var(--surface); white-space: pre-wrap; overflow-wrap: anywhere;
}
.turn.you .bubble { background: var(--accent-wash); border-color: var(--accent-wash);
                    color: var(--ink); }
/* Every answer carries the model and the date, and says to review it. The PCI
   SSC guidance requires AI output be reviewed and approved by a human
   assessor; a label on each answer is what makes that a habit rather than a
   policy nobody reads. */
.turn.claude .hint { margin: var(--s1) 0 0 var(--s2); }

.toolcall { font-size: var(--t-sm); }
.toolcall > summary { color: var(--ink-3); padding: 2px 4px; }
.toolcall > summary:hover { color: var(--accent); }
.toolcall code { font-size: var(--t-xs); }
.toolout {
  margin: var(--s2) 0 0; padding: var(--s3);
  background: var(--surface-sunk); border: 1px solid var(--line);
  border-radius: var(--r); color: var(--ink-2);
  font-family: var(--mono); font-size: var(--t-xs); line-height: 1.5;
  max-height: 320px; overflow: auto; white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.askbar { display: flex; gap: var(--s2); align-items: flex-end; }
.askbar textarea { flex: 1; margin-top: 0; resize: vertical; min-height: 52px; }
.askbar button { flex: none; }

/* The one-time password. Loud on purpose: it is shown once, it is the only
   copy, and an administrator who scrolls past it has to reset the account to
   get another. */
.card.credential { border-color: var(--warn-line); background: var(--warn-wash); }
.card.credential h2 { color: var(--warn); }
.card.credential .hint { color: var(--ink-2); }
code.pw { font-size: var(--t-lg); letter-spacing: .04em; padding: 4px 10px;
          background: var(--surface); border: 1px solid var(--warn-line);
          user-select: all; }

/* Two quiet actions per row, kept on one line so the column does not grow. */
td.rowactions { white-space: nowrap; }
td.rowactions form + form { margin-left: var(--s3); }

.checkline { display: flex; align-items: flex-start; gap: var(--s2);
             font-weight: 400; color: var(--ink-2); }
.checkline input { width: auto; margin: 3px 0 0; flex: none; }

.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
           overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

/* ----------------------------------------------------------------- misc */

code { background: var(--surface-sunk); padding: 1px 5px; border-radius: var(--r-sm);
       font-family: var(--mono); font-size: var(--t-sm); }
.empty { text-align: center; color: var(--ink-3); padding: var(--s6) var(--s4); }

/* ---------------------------------------------------------- theme toggle */

.theme-form { display: flex; }
.theme-toggle {
  background: transparent; border: 1px solid transparent;
  color: color-mix(in srgb, var(--chrome-ink) 74%, transparent);
  padding: 3px 9px; font-size: var(--t-sm); border-radius: var(--r);
}
.theme-toggle:hover {
  background: color-mix(in srgb, var(--chrome-ink) 12%, transparent);
  border-color: transparent; color: var(--chrome-ink);
}

/* ------------------------------------------------------------ evidence upload */
/* The drop zone replaces the raw file input only when scripting is available,
   so the form still works without it. */

.dropzone { display: none; }
.has-js .dropzone { display: block; }
.has-js .file-fallback { display: none; }

.dropzone {
  border: 1.5px dashed var(--line-strong);
  border-radius: var(--r-lg);
  background: var(--surface-sunk);
  padding: var(--s6) var(--s5);
  text-align: center;
  cursor: pointer;
  transition: border-color .12s, background .12s;
}
.dropzone:hover { border-color: var(--accent); background: var(--accent-wash); }
.dropzone:focus-visible { outline: none; box-shadow: var(--focus); border-color: var(--accent); }
.dropzone.over {
  border-color: var(--accent); background: var(--accent-wash);
  border-style: solid;
}
.dropzone .dz-title { font-size: var(--t-lg); font-weight: 600; margin-bottom: var(--s1); }
.dropzone .dz-sub { color: var(--ink-2); font-size: var(--t-sm); margin: 0; }
.dropzone .dz-icon {
  width: 34px; height: 34px; margin: 0 auto var(--s2);
  border: 1.5px solid var(--line-strong); border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  color: var(--ink-3); font-size: 17px; line-height: 1;
}
.dropzone.over .dz-icon, .dropzone:hover .dz-icon {
  border-color: var(--accent); color: var(--accent);
}

.chosen {
  display: flex; align-items: center; gap: var(--s3);
  margin-top: var(--s3); padding: var(--s3);
  background: var(--surface-sunk); border: 1px solid var(--line);
  border-radius: var(--r); font-size: var(--t-sm);
}
.chosen .name { font-weight: 600; overflow-wrap: anywhere; }
.chosen .size { color: var(--ink-3); }

.upload-actions { display: flex; align-items: center; gap: var(--s3); margin-top: var(--s3); }

button.busy { opacity: .75; cursor: progress; }
button[disabled] { opacity: .5; cursor: not-allowed; }

/* ------------------------------------------------------------------- brand */
/* The top bar is dark in both themes, so one asset serves both. Measured
   against --chrome: the lettering ring sits around 3.5:1 and the green disc
   above 10:1, so the mark reads without a light chip behind it. */

.brand { display: inline-flex; align-items: center; gap: var(--s2); }
.brand:hover { text-decoration: none; }
.brand-logo { display: block; width: 30px; height: 30px; flex: none; }

.signin-head { text-align: center; margin-bottom: var(--s5); }
.signin-head img { display: block; margin: 0 auto var(--s3); }
.signin-head h1 { font-size: var(--t-lg); margin-bottom: var(--s1); }
.signin-head p { margin: 0; }

/* --------------------------------------------------------- document browser */

.filepath { display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  padding: var(--s2) var(--s3); margin-bottom: var(--s3);
  background: var(--surface-sunk); border-radius: var(--r);
  font-family: var(--mono); font-size: var(--t-sm); }
.filepath span[aria-hidden] { color: var(--ink-3); }
.filepath .here { color: var(--ink); font-weight: 600; }

a.entry { display: inline-flex; align-items: center; gap: var(--s2);
          color: var(--ink); overflow-wrap: anywhere; }
a.entry:hover { color: var(--accent); text-decoration: none; }
.ficon { display: inline-flex; align-items: center; justify-content: center;
  width: 18px; flex: none; color: var(--ink-3); font-size: 11px; }
.ficon.dir { color: var(--accent); }
a.entry:hover .ficon { color: var(--accent); }

/* Only the first column of a package table may wrap. A region name broken as
   "eu-north-" / "1" is misread as easily as it is read, and verdict counts
   wrapping onto a second row turn four numbers into what looks like eight.
   The filename column absorbs the slack instead, since it is the one that is
   legitimately long. */
table.packages td:not(:first-child), table.packages th:not(:first-child),
td.counts, th.counts { white-space: nowrap; }
table.packages td:first-child { overflow-wrap: anywhere; }

table.files td { vertical-align: middle; }
table.grid tr.on { background: var(--accent-wash); }

/* ---------------------------------------------- document tree, in the aside */
/* The assessment page puts the client's Drive structure beside the work, so an
   assessor can open the policy a finding refers to without leaving the page
   they are adjudicating on. The column holds still while the page scrolls,
   which is the whole reason it is useful: a tree that scrolls away with the
   findings is just the documents page again. */

/* Two panes that scroll independently.
 *
 * This has to be driven from <body>, because "fill the viewport and no more" is
 * a claim about the whole document: the height has to come down from html and
 * body through main to the panes, and a percentage height on a descendant of an
 * auto-height ancestor resolves to auto. So the page opts in with a class on
 * <body> rather than styling its own container and hoping.
 *
 * min-height: 0 on the flex children is the load-bearing part. A flex item's
 * default min-height is its content, so without it the panes grow to fit the
 * findings and the shell scrolls as one page again, which is the bug this
 * layout exists to avoid.
 */
/* The navbar keeps its place, and the two columns below it fill the rest of the
   window: the tree on the left, the assessment on the right, each scrolling
   only itself. The page as a whole does not scroll at all. */
body.app-shell { height: 100vh; overflow: hidden;
                 display: flex; flex-direction: column; }
body.app-shell > main { flex: 1; min-height: 0; display: flex;
                        flex-direction: column;
                        margin: var(--s4) auto 0; padding-bottom: var(--s4);
                        /* The 1180px measure exists to keep prose readable. An
                           app shell with a fixed sidebar is not prose, and the
                           cap was squeezing the package tables into wrapping
                           filenames, wrapped region names and verdict counts
                           spilling onto a second row. */
                        max-width: none; }
/* Nothing below the panes: they reach the bottom of the window. The footer note
   is still shown on every other page. */
body.app-shell > footer { display: none; }

/* 300px, not 268: folder names in a Drive export are sentences ("2.
   Documentation Preparation and GAP Analysis"), and the tree indents each
   level, so the narrower column spent most of its width on wrapping. */
.split { display: grid; grid-template-columns: 300px minmax(0, 1fr);
         gap: var(--s5); align-items: start; }

/* Documents, the assessment, the assistant. The assistant gets a real share of
   the width rather than a strip: it is where answers are read, and an answer
   quoting an evidence path in a 320px column is all wrapping. */
.split.with-chat { grid-template-columns: 300px minmax(0, 1.3fr) minmax(360px, 1fr); }

/* The collection page has no document tree, so two columns rather than three.
   The work gets the extra width the tree would have taken. */
.split.with-chat.collection-split {
  grid-template-columns: minmax(0, 1.6fr) minmax(380px, 1fr);
}

/* The findings table is the widest thing in the portal: status, service,
   resource, check, detail, evidence and the assessor's own column. It gets the
   larger share, and the assistant a workable minimum rather than a fair half. */
.split.with-chat.package-split {
  grid-template-columns: minmax(0, 2.1fr) minmax(340px, 1fr);
}
body.app-shell .split { flex: 1; min-height: 0; align-items: stretch; }

/* The right column is a fixed heading over a scrolling body, so the assessment
   you are on does not scroll off the top of a long collection list. */
.split-main { min-height: 0; display: flex; flex-direction: column; }

/* ------------------------------------------------------------- chat pane */
/* Head, scrolling log, ask box pinned to the bottom: the same shape as the
   other two panes, so all three scroll only themselves. Reading a long answer
   must not move the findings, and adjudicating must not lose your place in the
   answer. */
.chat-pane {
  min-height: 0; display: flex; flex-direction: column;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-lg); overflow: hidden;
}
.chat-head { flex: none; display: flex; align-items: center; gap: var(--s2);
             padding: var(--s3) var(--s4); border-bottom: 1px solid var(--line);
             background: var(--surface-sunk); }
.chat-head h2 { font-size: var(--t-sm); text-transform: uppercase;
                letter-spacing: .04em; color: var(--ink-3); }
.chat-log { flex: 1; min-height: 0; overflow-y: auto;
            padding: var(--s4); display: flex; flex-direction: column;
            gap: var(--s3); }
.chat-log .turn .bubble { max-width: none; }
.chat-log .alert { margin: 0; }
.chat-examples { margin: 0; padding-left: var(--s4); }
.chat-examples li { margin-bottom: var(--s1); }
.chat-ask { flex: none; display: flex; gap: var(--s2); align-items: flex-end;
            padding: var(--s3) var(--s4) var(--s4);
            border-top: 1px solid var(--line); background: var(--surface-sunk); }
.chat-ask textarea { flex: 1; margin-top: 0; resize: vertical; min-height: 46px; }
.chat-ask button { flex: none; }
.split-main > .pagehead { flex: none; }
.split-main > .pane-scroll { flex: 1; min-height: 0; overflow-y: auto;
                             /* Keeps the scrollbar off the cards. */
                             padding-right: var(--s2); }
.pagehead h1 { margin-bottom: var(--s4); }

.docs-aside {
  /* Sticky is the fallback for when the shell is not in force, below the
     breakpoint and on short viewports, where the page scrolls normally. */
  position: sticky; top: var(--s4);
  max-height: calc(100vh - var(--s6));
  display: flex; flex-direction: column;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-lg); overflow: hidden;
}
body.app-shell .docs-aside { position: static; max-height: none; min-height: 0; }
.docs-aside > .aside-head {
  padding: var(--s3) var(--s4); border-bottom: 1px solid var(--line);
  background: var(--surface-sunk); flex: none;
}
.aside-head h2 { font-size: var(--t-sm); text-transform: uppercase;
                 letter-spacing: .04em; color: var(--ink-3); margin: 0 0 2px; }
.aside-head .aside-sub { margin: 0; font-size: var(--t-xs); color: var(--ink-2);
                         overflow-wrap: anywhere; }
.aside-switch { display: flex; flex-wrap: wrap; gap: 6px; margin-top: var(--s2); }
.aside-switch a { font-size: var(--t-xs); padding: 1px 6px;
                  border: 1px solid var(--line-strong); border-radius: var(--r-sm);
                  color: var(--ink-2); }
.aside-switch a.on { background: var(--accent-wash); border-color: var(--accent);
                     color: var(--accent); }
.docs-aside > .tree { overflow: auto; padding: var(--s2) var(--s3) var(--s4); }
.docs-aside > .aside-foot { flex: none; padding: var(--s2) var(--s4) var(--s3);
                            border-top: 1px solid var(--line); }

/* One indent step per level, drawn by nesting rather than counted, so the
   markup does not have to know how deep it is. */
.tkids { margin-left: 9px; padding-left: var(--s2);
         border-left: 1px solid var(--line); }

.tnode > summary, .tfile {
  display: flex; align-items: center; gap: 6px;
  padding: 2px 4px; border-radius: var(--r-sm);
  font-size: var(--t-sm); line-height: 1.45; color: var(--ink-2);
  overflow-wrap: anywhere;
}
.tnode > summary { list-style: none; font-weight: 500; color: var(--ink); }
.tnode > summary::marker { content: ""; }
.tnode > summary::-webkit-details-marker { display: none; }
.tnode > summary:hover, .tfile:hover { background: var(--surface-hover);
                                       color: var(--accent); text-decoration: none; }
.tnode > summary:focus-visible, .tfile:focus-visible {
  outline: none; box-shadow: var(--focus); }
.tnode > summary .ficon { color: var(--ink-3); }
.tnode[open] > summary .ficon { color: var(--accent); }
.thidden { margin: 2px 0 2px 4px; font-size: var(--t-xs); color: var(--ink-3); }

.aside-empty { padding: var(--s4); font-size: var(--t-sm); color: var(--ink-2); }
.aside-empty p { margin-bottom: var(--s2); }
.aside-links { display: flex; flex-wrap: wrap; gap: var(--s3);
               font-size: var(--t-xs); }
.aside-foot .thidden { margin: 0 0 var(--s1); }

/* Two panes need two columns and enough height to be worth splitting. Below
   either threshold the shell is switched off and the page scrolls as one, which
   is the right behaviour on a phone and on a laptop with a short window: a
   400px column with its own scrollbar is worse than no column at all. */
/* Three columns need the width for three columns. Between these two
   thresholds the chat sits under the assessment, still in its own scroll; below
   the lower one the shell switches off entirely. */
@media (max-width: 1400px) {
  .split.with-chat { grid-template-columns: 300px minmax(0, 1fr); }
  .split.with-chat.collection-split,
  .split.with-chat.package-split { grid-template-columns: minmax(0, 1fr); }
  body.app-shell .split.with-chat .chat-pane { grid-column: 1 / -1;
                                               max-height: 420px; }
}

@media (max-width: 940px), (max-height: 620px) {
  body.app-shell { height: auto; overflow: visible; display: block; }
  body.app-shell > main { display: block; margin: var(--s5) auto var(--s7);
                          padding-bottom: 0; }
  body.app-shell > footer { display: block; }
  body.app-shell .split { display: block; }
  body.app-shell .split-main { display: block; }
  body.app-shell .split-main > .pane-scroll { overflow: visible;
                                              padding-right: 0; }
  body.app-shell .docs-aside { max-height: none; margin-bottom: var(--s4); }
  body.app-shell .docs-aside > .tree { max-height: 420px; }
  body.app-shell .chat-pane { max-height: 480px; margin-bottom: var(--s4); }
}
@media (max-width: 940px) {
  .split { grid-template-columns: minmax(0, 1fr); }
  .docs-aside { position: static; max-height: none; margin-bottom: var(--s4); }
  .docs-aside > .tree { max-height: 420px; }
}

details > summary { cursor: pointer; }
details > summary:hover { color: var(--ink); }


/* Columns added when the dashboard started listing the whole practice rather
   than one assessor's assignments. Widths follow the rule in the block above:
   sized to the heading, not to the cell. */
.col-year        { width: 78px; }
.col-yours       { width: 104px; }
.col-packages    { width: 106px; }
.col-latest      { width: 190px; }
.col-status-word { width: 106px; }

/* The assigned-QSA list. Each name carries its role and, for an administrator,
   a remove control, so the three sit on one line and wrap as a unit rather than
   letting "remove" end up under the wrong name. */
.assignee { display: inline-flex; align-items: baseline; gap: var(--s1);
            white-space: nowrap; margin-right: var(--s3); }
.assignee form { display: inline; }

/* The client's QSA team table. */
.col-designation { width: 150px; }


/* --------------------------------------------------------------------------
   Dashboard
   --------------------------------------------------------------------------
   The headline figures, the per-assessment progress bars and the filter chips
   on the findings queue.

   Nothing here is sized by an inline style attribute, and that is a constraint
   rather than a preference: the CSP is `style-src 'self'` with no
   unsafe-inline, so a bar whose width came from `style="width:70%"` would be
   dropped by the browser and render as an empty box. Progress is a native
   <progress>, which takes value and max as attributes and carries the
   accessible semantics with it. */

.tiles { display: grid; gap: var(--s3); margin-bottom: var(--s5);
         grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }

.tile { display: flex; flex-direction: column; gap: 2px;
        background: var(--surface); border: 1px solid var(--line);
        border-radius: var(--r); padding: var(--s4);
        color: inherit; text-decoration: none; }
.tile:hover { border-color: var(--line-strong); background: var(--surface-hover);
              text-decoration: none; }
.tile-n { font-size: var(--t-xl); font-weight: 650; letter-spacing: -0.02em;
          line-height: 1.1; }
.tile-l { font-size: var(--t-sm); font-weight: 600; color: var(--ink-2); }
.tile .hint { font-size: var(--t-xs); }

/* Coloured only when the number is not zero. A dashboard where every tile is
   red on a finished assessment teaches people to stop reading the colour. */
.tile.needs { border-color: var(--accent-wash); background: var(--accent-wash); }
.tile.needs .tile-n { color: var(--accent); }
.tile.warns { border-color: var(--warn-line); background: var(--warn-wash); }
.tile.warns .tile-n { color: var(--warn); }
.tile.fails { border-color: var(--fail-line); background: var(--fail-wash); }
.tile.fails .tile-n { color: var(--fail); }

.card.notice { border-color: var(--warn-line); background: var(--warn-wash); }
.card.notice p:last-child, .card.notice ul:last-child { margin-bottom: 0; }
.card.notice ul { margin: 0; padding-left: var(--s4); }
.card.notice li + li { margin-top: var(--s2); }

.col-progress  { width: 190px; }
.col-attention { width: 230px; }
.col-actor     { width: 220px; }

.progress-cell progress {
  /* Appearance reset first: the platform widget is a different shape, colour
     and height on every OS, and this sits in a table column next to itself
     twenty times. */
  -webkit-appearance: none; appearance: none;
  display: block; width: 100%; height: 7px; border: 0;
  border-radius: 20px; background: var(--surface-sunk);
}
.progress-cell progress::-webkit-progress-bar {
  background: var(--surface-sunk); border-radius: 20px; }
.progress-cell progress::-webkit-progress-value {
  background: var(--pass); border-radius: 20px; }
.progress-cell progress::-moz-progress-bar {
  background: var(--pass); border-radius: 20px; }
.progress-cell .hint { margin-top: var(--s1); }

.attention .pill { margin-right: var(--s1); }
.old-evidence { color: var(--warn); }

.assessments td { vertical-align: top; }

/* Filter chips on the findings queue. Links, not buttons: they navigate, they
   are shareable, and the back button works. */
.filters { display: flex; flex-direction: column; gap: var(--s2); }
.filter-row { display: flex; align-items: baseline; gap: var(--s2);
              flex-wrap: wrap; }
.filter-label { font-size: var(--t-xs); font-weight: 600; color: var(--ink-3);
                text-transform: uppercase; letter-spacing: .04em;
                min-width: 84px; }

.chip { display: inline-block; padding: 3px 10px; border-radius: 20px;
        border: 1px solid var(--line); background: var(--surface);
        font-size: var(--t-xs); font-weight: 500; color: var(--ink-2);
        text-decoration: none; white-space: nowrap; }
.chip:hover { border-color: var(--line-strong); color: var(--ink);
              text-decoration: none; }
.chip.on { background: var(--accent); border-color: var(--accent);
           color: var(--accent-on); font-weight: 600; }
.chip.on:hover { color: var(--accent-on); }

.pager { display: flex; gap: var(--s2); margin-top: var(--s4); }

/* The one row shape worth spotting from across the table: the assessor
   concluded the opposite of what the analyzer did. A left border rather than a
   background, so it reads at a glance without making the text harder to read
   or fighting the status pill in the first column. */
tr.disagrees td:first-child { box-shadow: inset 3px 0 0 var(--err); }

@media (max-width: 940px) {
  .filter-label { min-width: 0; width: 100%; }
}

/* Utilities that exist because inline style attributes do not work here.
   `style-src 'self'` with no unsafe-inline blocks the style attribute as well
   as the <style> element, so a dozen `style="margin-top:0"` in these templates
   were being dropped by the browser and had been doing nothing for months. A
   named class is the fix, and a test refuses the attribute in any template. */
.w-full     { width: 100%; }
.mt-0       { margin-top: 0; }
.m-0        { margin: 0; }
.mt-3       { margin-top: var(--s3); }
.mt-2       { margin-top: var(--s2); }
.mb-3       { margin-bottom: var(--s3); }
.col-w-110  { width: 110px; }
.col-w-130  { width: 130px; }
.col-w-170  { width: 170px; }
.col-w-220  { width: 220px; }

/* --------------------------------------------------------------------------
   Two-step verification
   -------------------------------------------------------------------------- */

.code-input { font-family: var(--mono); font-size: var(--t-xl);
              letter-spacing: .28em; text-align: center; }

/* .card.narrow is (0,2,0) and a bare .mfa-enrol is (0,1,0), so the first
   version of this rule lost and the card stayed at 380px with a 305px QR and
   its plate inside a 308px content box. Matched on both classes rather than
   raising it with !important. */
.card.narrow.mfa-enrol { max-width: 520px; }
.mfa-enrol .steps { margin: 0 0 var(--s4); padding-left: var(--s5); }
.mfa-enrol .steps li + li { margin-top: var(--s4); }
.mfa-enrol .signin-head { margin-bottom: var(--s5); }

/* The one place in the stylesheet that deliberately does not follow the theme,
   which is why it goes through named variables that are defined once and never
   overridden in the dark block rather than through literals.
   
   A QR code is dark-on-light with a light quiet zone around it. Some scanners
   cope with an inverted one and plenty do not, and a person enrolling a second
   factor at eight in the morning should not be the one discovering which kind
   of phone they own. So the plate stays white in both themes and the modules
   stay black on it. */
.qr {
  background: var(--qr-plate); color: var(--qr-ink);
  border-radius: var(--r);
  /* Square by declaration, so the box has a definite height for the svg's
     height:100% to resolve against. */
  width: 256px; max-width: 100%; aspect-ratio: 1 / 1;
  /* Centred in the step's content column. Left-aligned it sat flush with the
     key box below but stopped 90px short on the right, which reads as a
     misplaced image rather than a deliberately smaller one. */
  margin: var(--s3) auto;

  /* No padding, and no overflow:hidden. The quiet zone is segno's four modules,
     inside the viewBox, so it is symmetric by construction at any size and is
     the margin the spec defines. A second margin in CSS was doing the same job
     twice.

     overflow:hidden was here and is deliberately gone. It hid nothing useful
     and cropped the code when the svg came out larger than this box, which
     turns a layout slip into an unscannable QR — the worst possible failure for
     the one element on this page that has to work. Without it an oversized svg
     spills visibly and gets fixed. */
}

/* Both dimensions pinned to the box, not left to the SVG's intrinsic size.
   That is the bug this had twice: an inline <svg> carrying only a viewBox has
   an intrinsic size of 305px here, and whether `height: auto` derives the
   height from the viewBox's ratio is exactly the sort of thing that differs.
   With width and height both 100% and the parent square, preserveAspectRatio
   (xMidYMid meet, the default) centres the code and it cannot overflow. */
.qr svg { display: block; width: 100%; height: 100%; }

/* The key, in groups of four. break-word rather than break-all so it wraps
   between groups instead of mid-group, which is the difference between a key
   somebody can read off and one they mistype. */
.secret { background: var(--surface-sunk); border: 1px solid var(--line);
          border-radius: var(--r-sm); padding: var(--s2) var(--s3);
          overflow-wrap: break-word; word-break: normal; user-select: all;
          line-height: 1.7; }

.pill.mfa-on  { color: var(--pass); background: var(--pass-wash);
                border-color: var(--pass-line); }
.pill.mfa-off { color: var(--warn); background: var(--warn-wash);
                border-color: var(--warn-line); }
.col-mfa { width: 108px; }
