/* ===================================================================
   OpsAdvisor — Executive Brief
   Hand-written, no framework, no build step (Decision 018).

   Tokens first, components after. Everything reads from the tokens so
   the whole product can be retuned from this block alone.
   =================================================================== */

:root {
  /* Paper and ink */
  --paper:        #FBFAF8;
  --paper-sunk:   #F4F2ED;
  --ink:          #191917;
  --ink-soft:     #45433E;
  --ink-muted:    #78746C;
  --ink-faint:    #A5A199;
  --rule:         #E2DED6;
  --rule-strong:  #C9C4B9;

  /* One accent, used sparingly */
  --accent:       #8C2F39;
  --accent-soft:  #F3E9E9;
  --accent-ink:   #FFFFFF;

  /* Semantic only — never decorative */
  --up:           #A8621B;
  --down:         #2F6B4F;
  --flat:         #8A867E;
  --danger:       #9B2226;
  --danger-soft:  #FAECEC;

  /* Type */
  --serif: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif;
  --sans:  ui-sans-serif, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono:  ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;

  /* Scale */
  --measure: 34rem;
  --gutter: 2.5rem;
  --step-1: 0.8rem;
  --step0:  1rem;
  --step1:  1.2rem;
  --step2:  1.55rem;
  --step3:  2.1rem;
  --step4:  3rem;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --paper:       #16161A;
    --paper-sunk:  #1D1D22;
    --ink:         #EDEAE4;
    --ink-soft:    #C7C3BA;
    --ink-muted:   #918C82;
    --ink-faint:   #67635B;
    --rule:        #2C2C32;
    --rule-strong: #3E3E45;
    --accent:      #D98A90;
    --accent-soft: #2A1E20;
    --accent-ink:  #16161A;
    --up:          #D9A05B;
    --down:        #7FB394;
    --flat:        #7A756C;
    --danger:      #E39396;
    --danger-soft: #2C1C1D;
  }
}

:root[data-theme="dark"] {
  --paper:       #16161A;
  --paper-sunk:  #1D1D22;
  --ink:         #EDEAE4;
  --ink-soft:    #C7C3BA;
  --ink-muted:   #918C82;
  --ink-faint:   #67635B;
  --rule:        #2C2C32;
  --rule-strong: #3E3E45;
  --accent:      #D98A90;
  --accent-soft: #2A1E20;
  --accent-ink:  #16161A;
  --up:          #D9A05B;
  --down:        #7FB394;
  --flat:        #7A756C;
  --danger:      #E39396;
  --danger-soft: #2C1C1D;
}

*, *::before, *::after { box-sizing: border-box; }

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

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

/* ---------- Layout ---------- */

.wrap {
  width: 100%;
  max-width: 62rem;
  margin: 0 auto;
  padding: 0 var(--gutter);
}

main.wrap { flex: 1; padding-bottom: 5rem; }

/* ---------- Masthead ---------- */

.masthead {
  border-bottom: 2px solid var(--ink);
  margin-bottom: 3rem;
}

.masthead-inner {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  padding-top: 1.1rem;
  padding-bottom: 1.1rem;
}

.wordmark {
  font-family: var(--serif);
  font-size: 1.15rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--ink);
  text-decoration: none;
}
.wordmark span { color: var(--accent); }

.masthead nav {
  display: flex;
  align-items: baseline;
  gap: 1.75rem;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.09em;
}
.masthead nav a,
.masthead nav .linkish {
  color: var(--ink-muted);
  text-decoration: none;
  padding-bottom: 2px;
  border-bottom: 1px solid transparent;
}
.masthead nav a:hover,
.masthead nav .linkish:hover {
  color: var(--ink);
  border-bottom-color: var(--accent);
}

.inline-form { display: inline; margin: 0; }

.linkish {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  text-transform: inherit;
  letter-spacing: inherit;
  cursor: pointer;
}

/* ---------- Typography ---------- */

h1 {
  font-family: var(--serif);
  font-size: clamp(2.1rem, 5vw, var(--step4));
  line-height: 1.12;
  font-weight: 600;
  letter-spacing: -0.015em;
  margin: 0 0 0.65rem;
  text-wrap: balance;
}

h2 {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  font-weight: 600;
  color: var(--ink-muted);
  margin: 0 0 1.5rem;
  padding-bottom: 0.65rem;
  border-bottom: 1px solid var(--rule);
}

.eyebrow {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--ink-muted);
  margin: 0 0 0.9rem;
}

.lede {
  font-family: var(--serif);
  font-size: var(--step1);
  line-height: 1.6;
  color: var(--ink-soft);
  max-width: var(--measure);
  margin: 0 0 1.5rem;
  text-wrap: pretty;
}

a { color: var(--accent); text-underline-offset: 3px; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ---------- Buttons ---------- */

.btn {
  display: inline-block;
  background: var(--ink);
  color: var(--paper);
  border: 1px solid var(--ink);
  border-radius: 2px;
  padding: 0.6rem 1.15rem;
  font-size: 0.82rem;
  font-family: var(--sans);
  text-transform: uppercase;
  letter-spacing: 0.09em;
  text-decoration: none;
  cursor: pointer;
}
.btn:hover { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }

/* ---------- Forms ---------- */

.stack { display: flex; flex-direction: column; gap: 1.15rem; }

label { display: flex; flex-direction: column; gap: 0.35rem; }

label > span {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.11em;
  color: var(--ink-muted);
}

input[type="text"],
input[type="email"],
input[type="password"],
input[type="file"] {
  font: inherit;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--rule-strong);
  border-radius: 2px;
  padding: 0.6rem 0.7rem;
  width: 100%;
}
input:focus { border-color: var(--accent); }

.form-error {
  background: var(--danger-soft);
  border-left: 2px solid var(--danger);
  color: var(--danger);
  padding: 0.7rem 0.9rem;
  font-size: 0.9rem;
  margin: 0 0 1.25rem;
}

/* ---------- Flash ---------- */

.flash {
  border-left: 2px solid var(--accent);
  background: var(--accent-soft);
  padding: 0.7rem 0.9rem;
  font-size: 0.9rem;
  margin: 0 0 1.75rem;
}
.flash.is-error { border-left-color: var(--danger); background: var(--danger-soft); color: var(--danger); }
.flash.is-info { border-left-color: var(--rule-strong); background: var(--paper-sunk); color: var(--ink-soft); }
.flash.is-success { border-left-color: var(--down); background: var(--paper-sunk); color: var(--ink-soft); }

/* ---------- Auth ---------- */

.auth { max-width: 22rem; margin: 3rem auto 0; text-align: left; }
.auth h1 { font-size: var(--step3); }
.auth-sub { color: var(--ink-muted); font-size: 0.92rem; margin: 0 0 2rem; }

/* ---------- Panels ---------- */

.dataset-head { padding-bottom: 3rem; border-bottom: 1px solid var(--rule); margin-bottom: 3rem; }

.panel { margin-bottom: 3rem; }

.facts-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  gap: 1.5rem;
  margin: 0;
}
.facts-grid > div { border-left: 1px solid var(--rule); padding-left: 1.1rem; }
.facts-grid dt {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-muted);
  margin-bottom: 0.25rem;
}
.facts-grid dd {
  margin: 0;
  font-family: var(--serif);
  font-size: var(--step1);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* ---------- Footer ---------- */

.site-footer {
  border-top: 2px solid var(--ink);
  padding-top: 1.2rem;
  padding-bottom: 2.5rem;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-faint);
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}

/* ---------- Utility ---------- */

.scroller { overflow-x: auto; }

@media (max-width: 34rem) {
  :root { --gutter: 1.35rem; }
  .facts-grid > div { border-left: 0; border-top: 1px solid var(--rule); padding: 0.8rem 0 0; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* ===================================================================
   Phase 2 components
   Markup is owned by the Phase 2 build; these class names were agreed
   with the chair before that markup landed. State is carried on
   `.is-*` modifiers throughout — see the note under Flash below.
   =================================================================== */

/* ---------- Dataset list ---------- */

.dataset-list { list-style: none; margin: 0; padding: 0; }

.dataset-row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: baseline;
  gap: 0.35rem 1.5rem;
  padding: 1.15rem 0;
  border-bottom: 1px solid var(--rule);
}
.dataset-row:first-child { border-top: 1px solid var(--rule); }

.dataset-row a {
  font-family: var(--serif);
  font-size: var(--step1);
  color: var(--ink);
  text-decoration: none;
}
.dataset-row a:hover { color: var(--accent); text-decoration: underline; }

.dataset-row .meta {
  grid-column: 1;
  font-size: 0.82rem;
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}

/* Status reads as a state, never as decoration. Queued and processing
   are deliberately quiet — nothing has been established yet. */
.dataset-status {
  grid-row: 1 / span 2;
  grid-column: 2;
  align-self: center;
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--ink-muted);
  white-space: nowrap;
}
.dataset-status::before {
  content: "";
  display: inline-block;
  width: 6px; height: 6px;
  border-radius: 50%;
  margin-right: 0.5rem;
  vertical-align: 0.1em;
  background: currentColor;
}
/* These are `datasets.status` values, not job states. The two
   vocabularies are deliberately not shared — see the ledger section
   for the job states that appear on Screen 2.

   `is-rejected` and `is-failed` are both dead ends, and they are
   styled differently on purpose. Rejected means we read the file and
   it cannot be used: that is a true statement about the user's export
   and an answer, so it looks definite. Failed means we broke: that is
   a statement about us and should read as an apology, not a verdict.
   Hence rejected carries the accent and failed stays muted. */
.dataset-status.is-uploaded   { color: var(--ink-faint); }
.dataset-status.is-validating { color: var(--ink-soft); }
.dataset-status.is-ready      { color: var(--down); }
.dataset-status.is-rejected   { color: var(--accent); }
.dataset-status.is-failed     { color: var(--ink-muted); }
.dataset-status.is-failed::before { background: var(--ink-faint); }

/* ---------- Upload ---------- */

.upload-form { max-width: 38rem; }

.dropzone {
  border: 1px dashed var(--rule-strong);
  border-radius: 3px;
  background: var(--paper-sunk);
  padding: 2.5rem 1.5rem;
  text-align: center;
  color: var(--ink-muted);
}
.dropzone:hover { border-color: var(--accent); }
.dropzone input[type="file"] { width: auto; margin-top: 0.75rem; background: transparent; }
.dropzone .hint { font-size: 0.82rem; color: var(--ink-faint); margin: 0.5rem 0 0; }

/* Facts read off the file before it is accepted. Source data, so it
   gets the same tabular treatment as every other source figure. */
.file-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: 1.25rem;
  margin: 1.75rem 0 0;
  font-variant-numeric: tabular-nums;
}
.file-facts > div { border-left: 1px solid var(--rule); padding-left: 1rem; }
.file-facts dt {
  font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--ink-muted); margin-bottom: 0.2rem;
}
.file-facts dd { margin: 0; font-family: var(--serif); font-size: var(--step1); }

/* ---------- Column capability ---------- */
/* Which analyses this export can actually support. A missing column is
   not an error — it narrows what we can honestly conclude, and saying
   so up front is the same promise the dashboard makes later. */

.capability-list { list-style: none; margin: 0; padding: 0; }

.capability {
  display: grid;
  grid-template-columns: 1.5rem 1fr;
  gap: 0.6rem;
  padding: 0.6rem 0;
  border-bottom: 1px solid var(--rule);
  font-size: 0.92rem;
}
.capability::before {
  font-family: var(--mono);
  font-size: 0.9rem;
  line-height: 1.5;
}
.capability.is-available::before { content: "\2713"; color: var(--down); }
.capability.is-missing::before   { content: "\2013"; color: var(--ink-faint); }
.capability.is-missing { color: var(--ink-muted); }
/* The li is a grid, so this span is a third grid item and would
   otherwise wrap back into the narrow marker column — one word per
   line. Pin it under the label. */
.capability .consequence {
  grid-column: 2;
  display: block;
  font-size: 0.82rem;
  color: var(--ink-faint);
  margin-top: 0.15rem;
}

/* Nine rows of good news is nine rows of no news. When nothing is
   missing, the block should cost a couple of lines rather than
   dominating the page — it earns its height only when it has height's
   worth to say. Done with :has() so it needs no markup change;
   browsers without :has() fall back to the full list, which is the
   current behaviour, so nothing breaks. */
.capability-list:not(:has(.capability:not(.is-available))) {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem 1.75rem;
}
.capability-list:not(:has(.capability:not(.is-available))) .capability {
  grid-template-columns: 1.1rem auto;
  border-bottom: 0;
  padding: 0.1rem 0;
  font-size: 0.88rem;
  color: var(--ink-muted);
}

/* Four states, matching ColumnMap: missing | empty | partial |
   available. The middle two are the honest ones and the reason this
   block is worth having — a column that exists but holds nothing
   supports exactly as much as not having it, and saying "available"
   because a header is present would overclaim on precisely the
   datasets this section exists to warn about.

   `empty` reads like `missing`, because that is what it is in
   practice; the label differs only so the user knows the column was
   there and the export, not the mapping, is what let them down. */
.capability.is-empty::before { content: "\2013"; color: var(--up); }
.capability.is-empty { color: var(--ink-muted); }
.capability.is-empty .consequence { color: var(--up); }

.capability.is-partial::before { content: "\00B7"; color: var(--up); font-weight: 700; }
.capability.is-partial { color: var(--ink-soft); }
.capability.is-partial .consequence { color: var(--up); }

/* The backing figure travels with the claim. A capability qualified as
   partial is only meaningful next to how much of the export backs it. */
.capability .backing {
  grid-column: 2;
  display: block;
  font-family: var(--mono);
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink-faint);
  margin-top: 0.1rem;
}

/* ---------- Validation issues ---------- */

.issue-list { list-style: none; margin: 0; padding: 0; }

.issue {
  border-left: 2px solid var(--rule-strong);
  padding: 0.7rem 0.95rem;
  margin-bottom: 0.75rem;
  font-size: 0.9rem;
  background: var(--paper-sunk);
}
.issue .where {
  font-family: var(--mono);
  font-size: 0.78rem;
  color: var(--ink-muted);
  display: block;
  margin-bottom: 0.2rem;
}
.issue.is-error   { border-left-color: var(--danger); background: var(--danger-soft); }
.issue.is-warning { border-left-color: var(--up); }
.issue.is-info    { border-left-color: var(--rule-strong); }

/* A rejection stops the upload. It gets the weight of a headline,
   because the user has to act on it. */
.rejection {
  border-left: 2px solid var(--danger);
  background: var(--danger-soft);
  padding: 1.1rem 1.25rem;
  margin: 0 0 2rem;
}
.rejection h3 {
  font-family: var(--serif);
  font-size: var(--step1);
  color: var(--danger);
  margin: 0 0 0.4rem;
}
.rejection p { margin: 0 0 0.5rem; font-size: 0.92rem; color: var(--ink-soft); }
.rejection p:last-child { margin: 0; }

.format-summary {
  font-size: 0.85rem;
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
  margin: 1.5rem 0 0;
}

/* ===================================================================
   Processing ledger (Screen 2)
   Append-only record of what has been ESTABLISHED. Deliberately has
   no progress bar, no spinner, and no shimmer: the worker is a
   per-minute cron with no continuous progress to report, so any
   smooth motion here would be fabricated. Stillness is accurate.
   Direction: design/mockups/processing-ledger.html
   =================================================================== */

.ledger { list-style: none; margin: 0; padding: 0; }

.ledger-entry {
  display: grid;
  grid-template-columns: 5.5rem 1.25rem 1fr;
  align-items: start;
  column-gap: 0.75rem;
  padding: 0.85rem 0;
  border-bottom: 1px solid var(--rule);
}

.ledger-time {
  font-family: var(--mono);
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink-faint);
  padding-top: 0.2rem;
  white-space: nowrap;
}

/* The marker column carries the connecting rule, so the ledger reads
   as one continuous record rather than a stack of unrelated rows. */
/* `align-self: stretch` overrides the row's `align-items: start` so
   the marker column spans the full entry height — otherwise the
   connecting rule stops short and the ledger reads as loose rows
   rather than one continuous record. */
.ledger-mark { position: relative; align-self: stretch; min-height: 1.6rem; }
.ledger-mark::before {
  content: "";
  position: absolute; left: 50%; top: 0.55rem;
  width: 7px; height: 7px; margin-left: -3.5px;
  border-radius: 50%; background: var(--ink); z-index: 1;
}
.ledger-mark::after {
  content: "";
  position: absolute; left: 50%; top: 0.95rem; bottom: -0.85rem;
  width: 1px; margin-left: -0.5px; background: var(--rule);
}
.ledger-entry:last-child .ledger-mark::after { display: none; }

.ledger-fact { margin: 0; font-family: var(--serif); font-size: 1.05rem; color: var(--ink); }
.ledger-detail {
  margin: 0.15rem 0 0;
  font-size: 0.85rem;
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}

/* Counts set aside are surfaced as they happen, not withheld until
   the end — invariant 6 showing up before the dashboard does. */
.ledger-aside { color: var(--up); }

.ledger-entry.is-current .ledger-mark::before {
  background: var(--paper);
  border: 1.5px solid var(--ink);
  width: 9px; height: 9px; margin-left: -4.5px;
}
.ledger-entry.is-current .ledger-detail { color: var(--ink-faint); font-style: italic; }

.ledger-entry.is-failed .ledger-mark::before { background: var(--danger); border: 0; }
.ledger-entry.is-failed .ledger-fact { color: var(--danger); }
.ledger-entry.is-failed .ledger-detail { color: var(--ink-soft); }

.ledger-entry.is-complete .ledger-mark::before { background: var(--down); }
.ledger-entry.is-complete .ledger-fact { color: var(--down); font-weight: 600; }

/* Stages not yet reached — quiet, and with no motion of any kind. */
.pending { list-style: none; margin: 1.75rem 0 0; padding: 0; }
.pending-label {
  font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.13em;
  color: var(--ink-faint); margin: 0 0 0.75rem;
}
.pending li {
  display: grid;
  grid-template-columns: 5.5rem 1.25rem 1fr;
  column-gap: 0.75rem;
  padding: 0.3rem 0;
  color: var(--ink-faint);
  font-size: 0.95rem;
}
.pending li span:first-child { font-family: var(--mono); font-size: 0.78rem; }

/* Retry sits on the failed entry itself, never in a banner above the
   ledger. A banner would put the failure above the evidence and teach
   the wrong lesson on the one screen where it matters most. */
.ledger-retry {
  display: inline-block;
  margin-top: 0.5rem;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--accent);
  border: 1px solid var(--rule-strong);
  border-radius: 2px;
  padding: 0.3rem 0.7rem;
  text-decoration: none;
}
.ledger-retry:hover { border-color: var(--accent); }

/* `jobs.status`, distinct from `datasets.status` above. */
.job-status.is-queued    { color: var(--ink-faint); }
.job-status.is-running   { color: var(--ink-soft); }
.job-status.is-completed { color: var(--down); }
.job-status.is-failed    { color: var(--danger); }
.job-status.is-cancelled { color: var(--ink-faint); font-style: italic; }

/* Naming the poll interval turns a still page from "broken" into
   "accurate". Without this line the honesty above reads as a hang.
   The stamp is also the liveness proof: if it advances and nothing
   else does, the user learns the page is alive and the work is slow,
   which is true. That is what replaces the spinner. */
.checked {
  margin: 1.75rem 0 0;
  font-size: 0.8rem;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}
.checked strong { font-weight: 600; color: var(--ink-muted); }

/* ===================================================================
   Confidence
   Chair's ruling C-UI-4: band AND number, always together. A word
   alone discards information we went to real trouble to compute
   honestly — 0.70 and 0.84 both say "Medium" and are not the same
   bet. The number is never rendered bare, because a naked 0.73
   invites false precision about what is itself an estimate.
   Bands are configuration (docs/architecture/rules-engine.md):
   >=0.85 high, 0.70-0.84 medium, 0.60-0.69 low, <0.60 not shown.
   =================================================================== */

.confidence {
  display: inline-flex;
  align-items: baseline;
  gap: 0.45rem;
  white-space: nowrap;
}

.confidence-band {
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-weight: 600;
}

.confidence-value {
  font-family: var(--mono);
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink-muted);
}

.confidence.is-high   .confidence-band { color: var(--ink); }
.confidence.is-medium .confidence-band { color: var(--ink-soft); }
.confidence.is-low    .confidence-band { color: var(--ink-muted); }

/* Below the display threshold. Never shown alongside a finding as if
   it were a result; reserved for evidence and audit views. */
.confidence.is-withheld .confidence-band { color: var(--ink-faint); font-style: italic; }
.confidence.is-withheld .confidence-value { display: none; }

/* Uncalibrated. We may ship this way: a reliability curve fitted to
   fixtures we generated ourselves measures fit to our own generator,
   not real-world accuracy, so the number is the model's own estimate
   and nothing has yet checked it against outcomes. The mark is not
   decoration — it is the difference between "we measured this" and
   "this is what the model said about itself", and dropping it would
   overclaim precisely where the product promises not to. */
.confidence.is-uncalibrated .confidence-value::after {
  content: "\00B0";
  color: var(--ink-faint);
  margin-left: 0.1rem;
}

.confidence-key {
  font-size: 0.78rem;
  color: var(--ink-faint);
  margin: 1rem 0 0;
}
.confidence-key code { font-family: var(--mono); }

/* ===================================================================
   Lower bounds
   Deterministic counts under-report: a rule counting restarts by
   subcategory finds 480 incidents where the true figure is 520, and
   misses 38% on the multi-application fixture. So the brief says
   "at least 480" and never "480 incidents were resolved the same way".
   The qualifier is part of the figure and must not be stylable into
   invisibility. Figures here are always deduplicated incident counts,
   never raw CSV row counts — the same fixture holds 489 and 529
   matching rows against those 480 and 520 incidents.
   =================================================================== */

.count-bound {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--ink-muted);
  margin-right: 0.3rem;
}

.count-figure { font-variant-numeric: tabular-nums; }

/* Bounds run in BOTH directions and the direction is never optional.
   Rules under-report, so classification counts are lower bounds. But
   conflicting duplicates invert it: a joined export at three rows per
   case retains every row (correct, as evidence) and inflates every
   frequency derived from them threefold. One brief can legitimately
   carry "at least 480" and "at most 620", and printed bare and side by
   side the reader cannot tell what either means. `is-upper` is
   weighted slightly heavier than the default so the two never blur
   together at a glance. */
.count-bound.is-upper { color: var(--up); font-weight: 600; }

/* Dataset-level caveat. A per-figure qualifier is not enough when the
   whole export's frequencies are suspect — this sits above the
   findings it qualifies, because a reader who has already read the
   number has already believed it. */
.brief-caveat {
  border: 1px solid var(--rule-strong);
  border-left: 2px solid var(--up);
  background: var(--paper-sunk);
  padding: 1rem 1.2rem;
  margin: 0 0 2rem;
  font-size: 0.9rem;
  color: var(--ink-soft);
}
.brief-caveat h3 {
  font-family: var(--sans);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.13em;
  color: var(--up);
  margin: 0 0 0.5rem;
}
.brief-caveat p { margin: 0 0 0.5rem; }
.brief-caveat p:last-child { margin: 0; }

/* ===================================================================
   Findings
   The patterns screen. Three groups, not one ranked list — see
   PatternPresenter::group() for why that is a product position rather
   than a layout choice.

   The order inside a finding is load-bearing and the CSS enforces it
   rather than merely permitting it: the NOUN reads largest, because a
   reader who meets "at least 620 records" before meeting "pin pad,
   tcp2com and till" has already filed the finding under abstract. The
   count is second and carries its own direction. Everything after is
   scope, and scope is quiet.
   =================================================================== */

.dataset-meta {
  font-size: 0.875rem;
  color: var(--ink-muted);
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.1rem;
  margin: 0;
}
.dataset-meta span + span::before {
  content: "\00B7";
  margin-right: 1.1rem;
  color: var(--ink-faint);
}

.findings h2 { margin-bottom: 0.35rem; }
.findings .lede { margin-bottom: 2.25rem; }

.pattern-row {
  border-top: 1px solid var(--rule);
  padding: 1.9rem 0 1.75rem;
}
.pattern-row:last-child { border-bottom: 1px solid var(--rule); }

/* The kind of finding, said in words before anything is counted. A
   coloured chip alone would make the reader learn a legend; the words
   cost one line and need no key. */
.pattern-type {
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.13em;
  color: var(--ink-muted);
  margin: 0 0 0.5rem;
}

/* The noun. This is the fix that turns "a repeat manual action" into
   something a reader can schedule work against, so it gets the
   largest type on the row. */
.pattern-subject {
  font-family: var(--serif);
  font-size: var(--step2);
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.01em;
  margin: 0 0 0.6rem;
}

.pattern-claim {
  margin: 0 0 0.55rem;
  font-size: 1.02rem;
  color: var(--ink-soft);
  max-width: var(--measure);
}
.pattern-claim.is-detail { font-size: 1.1rem; max-width: none; }
.pattern-claim .count-figure { font-weight: 600; color: var(--ink); }
.pattern-unit { color: var(--ink-muted); }
.pattern-meaning { color: var(--ink-soft); }

/* The ratio sits under the count, never beside it. Side by side, the
   exact denominator gets read as a second hedged figure and the one
   number on the row that is NOT a bound loses the only property that
   made it worth stating. */
.pattern-ratio {
  margin: 0 0 0.7rem;
  padding-left: 0.9rem;
  border-left: 2px solid var(--rule-strong);
  font-size: 0.95rem;
  color: var(--ink);
  max-width: var(--measure);
}

.pattern-scope {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3rem 1.1rem;
  margin: 0 0 0.6rem;
  font-size: 0.82rem;
  color: var(--ink-muted);
}

.pattern-evidence-link { margin: 0; font-size: 0.88rem; }

/* The three kinds are distinguished by a left rule, not by fill.
   A green card for "what is working" would make the page read as a
   status board, and these are findings about records, not health. */
.findings .pattern-row { padding-left: 1rem; border-left: 2px solid transparent; }
.pattern-row.is-operational  { border-left-color: var(--accent); }
.pattern-row.is-strength     { border-left-color: var(--down); }
.pattern-row.is-data_quality { border-left-color: var(--rule-strong); }
.pattern-row.is-data_quality .pattern-subject { color: var(--ink-soft); }

/* A subject in the customer's words versus one in ours. The quotation
   marks carry the distinction; this only keeps them from hanging the
   text out of alignment with everything below. */
.pattern-subject.is-quoted { text-indent: -0.42em; }

/* ---- Detail: the finding, verbatim ---- */

.pattern-title-verbatim {
  font-family: var(--serif);
  font-size: 1.15rem;
  line-height: 1.45;
  color: var(--ink);
  margin: 0 0 0.9rem;
  max-width: var(--measure);
}

.facts-grid .fact-words {
  font-family: var(--sans);
  font-size: 0.95rem;
  line-height: 1.35;
}

/* ---- Detail: why these records are together ---- */

.subject-basis {
  font-size: 0.85rem;
  color: var(--ink-muted);
  max-width: var(--measure);
  margin: 0 0 1.1rem;
}
.subject-basis strong { color: var(--ink-soft); font-weight: 600; }

.signature-line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem;
  margin: 0 0 0.8rem;
}
.signature-label {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.11em;
  color: var(--ink-muted);
  margin-right: 0.35rem;
}
.token {
  font-family: var(--mono);
  font-size: 0.78rem;
  padding: 0.12rem 0.45rem;
  border: 1px solid var(--rule-strong);
  border-radius: 2px;
  color: var(--ink-soft);
}
.token.is-action { border-style: dashed; color: var(--ink-muted); }

/* ===================================================================
   The executive brief
   Designed for the DEGRADED case, which is the normal one: coverage is
   arithmetic and always present, while headline/narrative/first_action
   are NULL whenever no model was reachable. QA has no API key and will
   not get one from a deploy, so every brief there is coverage-only
   indefinitely. A layout that reads as broken without prose is broken
   where the product owner actually looks — the structure carries the
   meaning and the prose is an addition, never the frame.
   =================================================================== */

.brief-head { padding-bottom: 2.25rem; }

.dataset-actions { display: flex; align-items: baseline; gap: 1.4rem; flex-wrap: wrap; }

.brief-summary { border-left: 2px solid var(--accent); padding-left: 1.4rem; }
.brief-headline {
  font-family: var(--serif);
  font-size: var(--step2);
  line-height: 1.25;
  font-weight: 600;
  margin: 0 0 0.9rem;
  max-width: var(--measure);
}
.brief-narrative { max-width: var(--measure); margin: 0 0 0.9rem; color: var(--ink-soft); }
.brief-first-action {
  max-width: var(--measure);
  margin: 1.2rem 0 0;
  padding-top: 0.9rem;
  border-top: 1px solid var(--rule);
}

.opportunity {
  border-top: 1px solid var(--rule);
  padding: 2.1rem 0 1.9rem 1rem;
  border-left: 2px solid transparent;
}
.opportunity:last-child { border-bottom: 1px solid var(--rule); }
.opportunity.is-operational  { border-left-color: var(--accent); }
.opportunity.is-strength     { border-left-color: var(--down); }
.opportunity.is-data_quality { border-left-color: var(--rule-strong); }

/* Rank and record count on one line. On the reference export a
   120-record finding outranks a 645-record one, because impact is
   source-recorded severity and the smaller group is priority 1 on all
   120 with the SLA missed on all 120. That is defensible but it is a
   judgement encoded in weights, and a reader seeing 120 above 645 must
   be able to answer "why" in the same glance rather than after a click. */
.opportunity-head { margin-bottom: 0.75rem; }
.opportunity-rank {
  display: flex;
  align-items: baseline;
  gap: 0.9rem;
  margin: 0 0 0.35rem;
}
.rank-number {
  font-family: var(--mono);
  font-size: 0.78rem;
  color: var(--accent);
  font-weight: 600;
  letter-spacing: 0.04em;
}
.rank-count {
  font-size: 0.82rem;
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}
.rank-count .count-figure { font-weight: 600; color: var(--ink-soft); }

.opportunity-title {
  font-family: var(--serif);
  font-size: var(--step2);
  line-height: 1.2;
  font-weight: 600;
  letter-spacing: -0.01em;
  margin: 0;
  max-width: var(--measure);
}

.opportunity-statement {
  margin: 0 0 1rem;
  color: var(--ink-soft);
  max-width: var(--measure);
}

/* Evidence, ordered so the finding precedes its context. The ratio line
   is the one number on the card that is not a bound, so it is the one
   that must not read as another hedge. */
.evidence-lines {
  list-style: none;
  margin: 0 0 1.2rem;
  padding: 0;
  max-width: var(--measure);
}
.evidence-lines li {
  position: relative;
  padding-left: 1rem;
  margin-bottom: 0.4rem;
  font-size: 0.9rem;
  color: var(--ink-soft);
}
.evidence-lines li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.62em;
  width: 4px;
  height: 4px;
  background: var(--rule-strong);
  border-radius: 50%;
}
.evidence-lines li.is-observation { color: var(--ink); }
.evidence-lines li.is-ratio { font-weight: 500; color: var(--ink); }
.evidence-lines li.is-ratio::before { background: var(--accent); }

.opportunity-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 2rem;
  margin: 0 0 0.9rem;
}
.opportunity-facts > div { min-width: 0; }
.opportunity-facts dt {
  font-size: 0.66rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--ink-faint);
  margin-bottom: 0.15rem;
}
.opportunity-facts dd {
  margin: 0;
  font-family: var(--serif);
  font-size: 1.05rem;
  font-variant-numeric: tabular-nums;
}
.opportunity-facts dd.fact-words { font-family: var(--sans); font-size: 0.92rem; }
.is-trend-rising  { color: var(--up); }
.is-trend-falling { color: var(--down); }

.opportunity-scope {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem 1.2rem;
  margin: 0 0 1.1rem;
  font-size: 0.82rem;
  color: var(--ink-muted);
}
.opportunity-scope strong { color: var(--ink-soft); font-weight: 600; }

.opportunity-step {
  background: var(--paper-sunk);
  padding: 0.9rem 1.1rem;
  margin: 0 0 0.9rem;
  max-width: var(--measure);
}
.step-label {
  font-size: 0.66rem;
  text-transform: uppercase;
  letter-spacing: 0.13em;
  color: var(--ink-muted);
  margin: 0 0 0.3rem;
}
.step-text { margin: 0; font-size: 0.92rem; color: var(--ink); }

/* The success criterion, kept visually subordinate to the step itself
   but inside the same block — it answers "how will I know it worked?",
   which is a different question from "should I believe this?" and
   belongs beside the action rather than beside the proof. */
.step-measure {
  margin: 0.7rem 0 0;
  padding-top: 0.7rem;
  border-top: 1px solid var(--rule);
  font-size: 0.85rem;
  color: var(--ink-muted);
}
.step-measure-label {
  display: block;
  font-size: 0.64rem;
  text-transform: uppercase;
  letter-spacing: 0.13em;
  color: var(--ink-faint);
  margin-bottom: 0.2rem;
}

.opportunity-evidence-link { margin: 0; font-size: 0.88rem; }

/* ===================================================================
   Evidence table
   What ServiceNow said. There is no derived column here and there must
   not be one: a per-record "permanent fix: yes" invites the reader to
   audit our inference one row at a time, and an arbitrary record's
   verdict is the weakest thing we produce. The pattern asserts; these
   rows evidence.
   =================================================================== */

.evidence-count {
  font-size: 0.82rem;
  color: var(--ink-muted);
  margin: 0 0 1rem;
  font-variant-numeric: tabular-nums;
}

table.evidence {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.85rem;
  line-height: 1.45;
}
table.evidence th {
  text-align: left;
  font-family: var(--sans);
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.11em;
  color: var(--ink-muted);
  font-weight: 600;
  padding: 0 0.9rem 0.5rem 0;
  border-bottom: 1px solid var(--rule-strong);
  white-space: nowrap;
}
table.evidence td {
  vertical-align: top;
  padding: 0.75rem 0.9rem 0.75rem 0;
  border-bottom: 1px solid var(--rule);
  color: var(--ink-soft);
}
table.evidence .case {
  font-family: var(--mono);
  font-size: 0.78rem;
  color: var(--ink);
  white-space: nowrap;
}
table.evidence .when { white-space: nowrap; font-variant-numeric: tabular-nums; }
table.evidence .resolution { min-width: 16rem; }
table.evidence .reason { color: var(--ink-muted); font-size: 0.8rem; }

.resolution-code {
  display: block;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-muted);
  margin-bottom: 0.15rem;
}
.resolution-notes { display: block; }

/* An absent source value is stated, never blanked. A gap in a table is
   ambiguous between "empty" and "we did not show it", and on the one
   screen whose job is verification that ambiguity is expensive. */
.qualifier {
  font-size: 0.75rem;
  font-style: italic;
  color: var(--ink-faint);
}

.pager {
  display: flex;
  align-items: baseline;
  gap: 1.5rem;
  margin: 1.5rem 0 0;
  font-size: 0.85rem;
}
.pager span { color: var(--ink-faint); }
.pager-position { font-variant-numeric: tabular-nums; }

.note-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-width: var(--measure);
}
.note-list li {
  border-top: 1px solid var(--rule);
  padding: 0.85rem 0;
  font-size: 0.86rem;
  color: var(--ink-muted);
}

.analysis-notes h2 {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.13em;
  font-family: var(--sans);
  color: var(--ink-muted);
}

@media (max-width: 34rem) {
  .ledger-entry, .pending li { grid-template-columns: 4.5rem 1rem 1fr; }
  .ledger-time { font-size: 0.72rem; }
  .dataset-row { grid-template-columns: 1fr; }
  .dataset-status { grid-row: auto; grid-column: 1; justify-self: start; }
  .file-facts > div { border-left: 0; border-top: 1px solid var(--rule); padding: 0.7rem 0 0; }
  .pattern-scope { gap: 0.25rem 0.9rem; }
  table.evidence { font-size: 0.8rem; }
}

/* ===================================================================
   Print
   Chair's ruling C-UI-3: this is an MVP acceptance criterion, not
   Phase 9 polish. "I need to show this to my boss" is the entire
   success condition, and nobody forwards their boss a login URL to a
   QA box — they send a PDF or hand over a printout. If the brief
   cannot leave the screen, the product cannot do the one thing
   success was defined as.

   The editorial direction makes this nearly free: paper and ink
   already ARE the design, so print is mostly a matter of removing
   chrome and refusing to break a finding across a page.
   =================================================================== */

@media print {

  /* A dark-mode reader must not print white-on-black. Force the paper
     palette regardless of scheme or explicit theme choice. */
  :root,
  :root[data-theme="dark"],
  :root:not([data-theme="light"]) {
    --paper: #FFFFFF;  --paper-sunk: #FFFFFF;
    --ink: #000000;    --ink-soft: #222222;
    --ink-muted: #555555; --ink-faint: #767676;
    --rule: #CCCCCC;   --rule-strong: #999999;
    --accent: #6B1F27; --accent-soft: #FFFFFF;
    --danger: #6B1F27; --danger-soft: #FFFFFF;
    /* Trend arrows must survive greyscale on an office laser, so they
       stop carrying colour and rely on the glyph alone. */
    --up: #000000; --down: #000000; --flat: #000000;
  }

  @page { margin: 18mm 16mm; }

  body {
    background: #FFFFFF;
    font-size: 10.5pt;
    line-height: 1.45;
  }

  .wrap { max-width: none; padding: 0; }
  main.wrap { padding-bottom: 0; }

  /* Chrome the reader does not need on paper. The masthead rule stays
     as a hairline so the page still reads as a document with a top. */
  .masthead nav,
  .site-footer,
  .btn,
  .inline-form,
  .linkish,
  .dropzone,
  .upload-form,
  .ledger-retry,
  .opportunity-evidence-link,
  .pending { display: none !important; }

  .masthead { border-bottom: 1pt solid #000; margin-bottom: 1.5rem; }

  /* Identity must print as content. A brief with no name, date range
     or row count is useless to whoever it gets handed to, and the row
     count printed here is the USABLE count — what was actually
     analysed — because a header saying 2,595 above findings computed
     over 2,555 prints a discrepancy onto a page a CIO forwards. */
  .dataset-head {
    border-bottom: 1pt solid #000;
    padding-bottom: 1rem;
    margin-bottom: 1.5rem;
    break-after: avoid;
  }

  /* Never orphan a headline from the evidence that supports it, and
     never slice a finding across a page break — a half-finding is
     exactly the kind of thing that gets quoted out of context. */
  .opportunity,
  .opportunity-step,
  .evidence-lines,
  .brief-summary,
  .pattern-row,
  .panel,
  .ledger-entry,
  .rejection,
  .issue,
  .capability { break-inside: avoid; }

  h1, h2, h3, .eyebrow { break-after: avoid; }

  /* The confidence figure is load-bearing on paper: it is what stops
     a printed finding from reading as settled fact. */
  .confidence, .count-bound { color: #000 !important; }

  /* Evidence links are the trust claim. On paper the href is the only
     way to follow one, so surface it. Internal routes only — a bare
     "#" or a javascript: href would print as noise. */
  a[href^="/"]::after,
  a[href^="http"]::after {
    content: " (" attr(href) ")";
    font-size: 8pt;
    color: #555;
    word-break: break-all;
  }

  /* The evidence link is chrome on paper — the reader is holding the
     evidence or they are not, and a "see the 620 records" line with a
     URL after it is a promise the printout cannot keep. */
  .pager { display: none !important; }

  /* Tables must repeat their header and never slice a row. */
  thead { display: table-header-group; }
  tr, tbody { break-inside: avoid; }
  .scroller { overflow: visible !important; }
}
