/* ============================================================================
 * IntelliForia admin portal — the whole stylesheet.
 *
 * Replaces Bootstrap 5.1.3 + Bootstrap 4.6.0 + AdminLTE 3.2 + Font Awesome, all
 * of which layout.html loads from third-party CDNs on pages that render PHI.
 * Bootstrap 4 loads last there and wins the cascade, so Bootstrap 5 utility
 * classes written in those templates silently do nothing — which is the
 * mechanical reason the portal could not be designed on.
 *
 * Section numbers track DESIGN.md. Change a token here and in DESIGN.md together.
 *
 * Nothing in this file makes an external request. No @import, no font CDN, no
 * background-image URL off-origin.
 * ========================================================================== */

/* --- §2 Tokens ----------------------------------------------------------- */
:root {
  /* Ink. --if-ink-faint is the floor for small text and must clear 4.5:1 on
     EVERY surface it lands on, not just white. #6b7689 passes on white (4.59)
     and fails on --if-bg (4.05), --if-surface-sunk (4.31) and --if-accent-soft
     (3.91 — the selected row). This value measures 5.39 / 4.76 / 5.07 / 4.59. */
  --if-ink: #182135;
  --if-ink-soft: #55617a;
  --if-ink-faint: #5f6b80;

  /* Ground */
  --if-bg: #eef1f7;
  --if-surface: #ffffff;
  --if-surface-sunk: #f6f8fc;
  --if-border: #e3e8f1;
  --if-border-strong: #d3dae7;

  /* Rail — the second neutral layer, cooler and deeper than content. */
  --if-rail: #171d2e;
  --if-rail-2: #1f2637;
  --if-rail-ink: #b8c1d4;
  --if-rail-ink-dim: #7b869c;

  /* Accent — action and current selection only, never decoration. */
  --if-accent: #4f46e5;
  --if-accent-hi: #6366f1;
  --if-accent-dark: #3730a3;
  --if-accent-soft: #ecebfe;
  --if-on-accent: #ffffff;

  /* §4 Clinical status scale. One vocabulary: Passing / Needs work / Blocked /
     Processing. Never carried by colour alone — every chip pairs it with a word. */
  --if-pass: #047857;  --if-pass-soft: #e3f5ee;
  --if-watch: #9a4404; --if-watch-soft: #fdecdf;
  --if-fail: #be123c;  --if-fail-soft: #fdeaea;
  --if-idle: #556070;  --if-idle-soft: #eef1f6;

  /* Depth — offset plus blur. A zero-offset colour halo is decoration. */
  --if-shadow: 0 1px 2px rgba(20, 30, 55, .04), 0 6px 20px -8px rgba(20, 30, 55, .14);
  --if-shadow-lift: 0 2px 4px rgba(20, 30, 55, .06), 0 16px 34px -12px rgba(30, 40, 80, .28);

  --if-r-sm: 7px;
  --if-r: 10px;
  --if-r-lg: 14px;
  --if-r-pill: 999px;

  /* §7 Motion */
  --if-ease: cubic-bezier(.22, .61, .36, 1);
  --if-t: 170ms;

  --if-rail-w: 248px;
  --if-topbar-h: 56px;
  --if-measure: 1440px;

  /* DESIGN.md §3 specifies Inter. It is not self-hosted yet — the font files are
     not in the repo — and loading it from Google Fonts would reintroduce exactly
     the third-party request this stylesheet exists to remove. The system stack
     below is metrically close and costs nothing. Dropping Inter in later is a
     @font-face block plus prepending the family here; no other change. */
  --if-font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
             "Helvetica Neue", Arial, sans-serif;
  --if-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  color-scheme: light;
}

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

body {
  margin: 0;
  background: var(--if-bg);
  color: var(--if-ink);
  font-family: var(--if-font);
  font-size: .875rem;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4, p, ul, ol, dl, dd, figure { margin: 0; }
ul, ol { padding: 0; list-style: none; }
button { font: inherit; color: inherit; }
img, svg { vertical-align: middle; }
a { color: var(--if-accent); text-decoration: none; }
a:hover { text-decoration: underline; }

/* Focus is never removed. Inside the rail it turns white: --if-accent on
   --if-rail measures 2.67:1, under the 3:1 floor for a non-text indicator, so
   the ring would be invisible exactly where the navigation is. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--if-accent);
  outline-offset: 2px;
  border-radius: var(--if-r-sm);
}
.if-rail :where(a, button, [tabindex]):focus-visible { outline-color: #fff; }

.if-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;
}

/* --- §3 Type ------------------------------------------------------------- */
/* Fixed rem scale, ratio ~1.15. No fluid clamps: users view product UI at a
   consistent DPI and a heading that shrinks in a sidebar looks worse, not
   better. Every number that can be compared down a column is tabular. */
.if-tnum { font-variant-numeric: tabular-nums; font-feature-settings: "tnum"; }

.if-display { font-size: 1.75rem; font-weight: 700; letter-spacing: -.025em; line-height: 1.15; }
.if-h2 { font-size: 1.05rem; font-weight: 700; letter-spacing: -.01em; }
.if-h3 { font-size: .875rem; font-weight: 650; }
.if-sm { font-size: .8125rem; }
.if-meta { font-size: .75rem; color: var(--if-ink-faint); }
.if-label {
  font-size: .6875rem; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--if-ink-faint);
}
/* Prose holds 65–75ch. Tables may run wider. */
.if-prose { max-width: 72ch; line-height: 1.65; }

.if-code {
  font-family: var(--if-mono); font-size: .75rem; font-weight: 600;
  color: var(--if-accent-dark); background: var(--if-accent-soft);
  padding: 1px 6px; border-radius: 5px;
}

/* --- §5 Layout ----------------------------------------------------------- */
.if-shell { display: grid; grid-template-columns: var(--if-rail-w) 1fr; min-height: 100vh; }

/* Fixed from the start. Left in flow this becomes a grid item and takes the
   rail's column, pushing the whole shell one track right. */
.if-scrim {
  position: fixed; inset: 0; z-index: 50; display: none;
  background: rgba(15, 20, 35, .44); opacity: 0; pointer-events: none;
  transition: opacity 240ms var(--if-ease);
}

.if-rail {
  background: var(--if-rail); color: var(--if-rail-ink);
  display: flex; flex-direction: column;
  position: sticky; top: 0; height: 100vh;
}
.if-brand { display: flex; align-items: center; gap: 10px; padding: 18px 18px 14px; }
.if-brand-mark {
  width: 28px; height: 28px; border-radius: 8px; flex: none;
  background: linear-gradient(150deg, var(--if-accent-hi), var(--if-accent-dark));
  display: grid; place-items: center; color: #fff;
  font-weight: 800; font-size: .8rem; letter-spacing: -.03em;
}
.if-brand-name { color: #fff; font-weight: 700; font-size: .95rem; letter-spacing: -.015em; }

/* Org scope is a correctness concern in a multi-tenant portal, so it is stated,
   not implied. It also appears beside every page title, because below 1024px the
   rail is off-canvas and this would be the only place it lived. */
.if-org {
  margin: 0 12px 14px; padding: 9px 11px; border-radius: var(--if-r);
  background: var(--if-rail-2); display: flex; align-items: center; gap: 9px;
  border: 1px solid transparent; width: calc(100% - 24px); text-align: left;
  cursor: pointer; transition: border-color var(--if-t) var(--if-ease);
}
.if-org:hover { border-color: #2e364a; }
.if-org .k {
  font-size: .625rem; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--if-rail-ink-dim); display: block;
}
.if-org .v {
  color: #fff; font-size: .8125rem; font-weight: 600; display: block;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.if-org .chev { margin-left: auto; color: var(--if-rail-ink-dim); flex: none; }

.if-nav { padding: 4px 12px 20px; overflow-y: auto; flex: 1; }
/* Grouped by the job the person came to do, not by permission tier. The old rail
   used "SECURE NAVIGATION" / "SECURE MANAGEMENT", which told an already
   authenticated user nothing. */
.if-nav-group {
  font-size: .625rem; font-weight: 700; letter-spacing: .09em;
  text-transform: uppercase; color: var(--if-rail-ink-dim); padding: 16px 10px 6px;
}
.if-nav-link {
  display: flex; align-items: center; gap: 11px; padding: 8px 10px;
  border-radius: var(--if-r-sm); color: var(--if-rail-ink);
  font-size: .8125rem; font-weight: 500; text-decoration: none;
  transition: background var(--if-t) var(--if-ease), color var(--if-t) var(--if-ease);
}
.if-nav-link:hover { background: #232b3e; color: #fff; text-decoration: none; }
.if-nav-link[aria-current="page"] { background: var(--if-accent); color: #fff; font-weight: 600; }
.if-nav-link svg { flex: none; opacity: .9; }
.if-nav-link .count {
  margin-left: auto; font-size: .6875rem; font-weight: 700; padding: 1px 7px;
  border-radius: var(--if-r-pill); background: var(--if-fail); color: #fff;
  font-variant-numeric: tabular-nums;
}
.if-nav-link[aria-current="page"] .count { background: rgba(255, 255, 255, .22); }

.if-rail-foot {
  padding: 14px 18px; border-top: 1px solid #262e42;
  display: flex; align-items: center; gap: 10px;
}
.if-avatar {
  width: 28px; height: 28px; border-radius: var(--if-r-pill);
  background: var(--if-accent-dark); color: #fff; display: grid; place-items: center;
  font-size: .75rem; font-weight: 700; flex: none;
}
.if-rail-foot .who { font-size: .75rem; color: #fff; font-weight: 600; line-height: 1.25; }
.if-rail-foot .role { font-size: .6875rem; color: var(--if-rail-ink-dim); }

.if-main { min-width: 0; display: flex; flex-direction: column; }
.if-topbar {
  background: var(--if-surface); border-bottom: 1px solid var(--if-border);
  padding: 0 24px; height: var(--if-topbar-h);
  position: sticky; top: 0; z-index: 20;
}
.if-topbar-in {
  display: flex; align-items: center; gap: 14px; height: 100%;
  max-width: var(--if-measure); margin-inline: auto;
}
.if-rail-toggle { display: none; }
.if-content {
  padding: 22px 24px 56px; max-width: var(--if-measure); width: 100%;
  margin-inline: auto;
}

.if-search { flex: 1; max-width: 420px; position: relative; }
.if-search input {
  width: 100%; padding: 7px 11px 7px 32px; font: inherit; font-size: .8125rem;
  border: 1px solid var(--if-border-strong); border-radius: var(--if-r-sm);
  background: var(--if-surface-sunk); color: var(--if-ink);
}
.if-search input::placeholder { color: var(--if-ink-faint); }
.if-search input:focus {
  background: #fff; border-color: var(--if-accent); outline: none;
  box-shadow: 0 0 0 3px var(--if-accent-soft);
}
.if-search .ic {
  position: absolute; left: 10px; top: 50%; transform: translateY(-50%);
  color: var(--if-ink-faint); pointer-events: none;
}
.if-spacer { flex: 1; }

/* Page header is one row. Never three titles for one screen — the old list
   screens stacked a breadcrumb, an <h1> and a coloured card header all naming
   the same page. */
.if-page-head { display: flex; align-items: flex-start; gap: 16px; margin-bottom: 20px; }
.if-titleline { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.if-page-head .sub { color: var(--if-ink-soft); margin-top: 5px; max-width: 68ch; }
.if-page-head .actions { margin-left: auto; display: flex; gap: 8px; flex: none; }

.if-org-chip {
  display: inline-flex; align-items: center; gap: 6px; font-size: .75rem;
  font-weight: 650; color: var(--if-accent-dark); background: var(--if-accent-soft);
  padding: 3px 10px; border-radius: var(--if-r-pill); white-space: nowrap;
}
.if-org-chip::before {
  content: ""; width: 6px; height: 6px; border-radius: 2px; background: currentColor;
}

/* Section heading: more space above than below. */
.if-sec { display: flex; align-items: center; gap: 10px; margin: 34px 0 12px; }
.if-sec:first-child { margin-top: 0; }
.if-sec .rule { flex: 1; height: 1px; background: var(--if-border-strong); }
.if-sec .n {
  font-size: .75rem; font-weight: 650; color: var(--if-ink-faint);
  background: var(--if-surface-sunk); border: 1px solid var(--if-border);
  padding: 1px 8px; border-radius: var(--if-r-pill);
}

/* --- §6 Components ------------------------------------------------------- */

/* Buttons: exactly three ranks. One primary per view. Destructive is --if-fail
   and is only ever used for destruction. The old toolbars put blue, grey and
   teal buttons at equal weight, so nothing read as primary. */
.if-btn {
  display: inline-flex; align-items: center; gap: 7px; padding: 7px 13px;
  border-radius: var(--if-r-sm); font-size: .8125rem; font-weight: 600;
  cursor: pointer; border: 1px solid transparent; white-space: nowrap;
  text-decoration: none;
  transition: background var(--if-t) var(--if-ease),
              border-color var(--if-t) var(--if-ease), color var(--if-t) var(--if-ease);
}
.if-btn:hover { text-decoration: none; }
.if-btn-primary { background: var(--if-accent); color: var(--if-on-accent); }
.if-btn-primary:hover { background: var(--if-accent-dark); color: var(--if-on-accent); }
.if-btn-secondary {
  background: var(--if-surface); border-color: var(--if-border-strong); color: var(--if-ink);
}
.if-btn-secondary:hover { border-color: #b9c3d6; background: var(--if-surface-sunk); }
.if-btn-quiet { background: none; color: var(--if-ink-soft); padding: 7px 9px; }
.if-btn-quiet:hover { background: var(--if-surface-sunk); color: var(--if-ink); }
.if-btn-danger { background: var(--if-fail); color: #fff; }
.if-btn-danger:hover { filter: brightness(.93); color: #fff; }
/* A row's primary action, as a LABEL rather than a glyph. The uploads list used a
   bare 14px eye inside a 28px transparent box: the most-used control on the row
   and the hardest thing on it to see or hit. Compact enough for a table cell,
   but it says what it does and gives the pointer something to aim at. */
.if-btn-row {
  padding: 6px 10px; font-size: .75rem; gap: 4px; min-height: 30px;
  background: var(--if-surface); border-color: var(--if-border-strong);
  color: var(--if-ink-soft);
}
.if-btn-row:hover {
  border-color: var(--if-accent); background: var(--if-surface-sunk);
  color: var(--if-accent);
}
.if-btn-row .if-chev { opacity: .55; }
.if-btn-row:hover .if-chev { opacity: 1; }

.if-btn:disabled, .if-btn[aria-disabled="true"] { opacity: .45; cursor: not-allowed; }
.if-btn:active { transform: translateY(1px); }

/* Cards have no coloured header bars. Section identity comes from the heading.
   Nested cards are prohibited. */
.if-card {
  background: var(--if-surface); border: 1px solid var(--if-border);
  border-radius: var(--if-r-lg); box-shadow: var(--if-shadow);
}
.if-card-head {
  padding: 14px 17px; border-bottom: 1px solid var(--if-border);
  display: flex; align-items: center; gap: 10px;
}
.if-card-head .aside { margin-left: auto; font-size: .75rem; color: var(--if-ink-faint); }
.if-card-body { padding: 17px; }

/* Status chip — colour plus a word, always. */
.if-chip {
  display: inline-flex; align-items: center; gap: 5px; font-size: .75rem;
  font-weight: 650; padding: 3px 9px; border-radius: var(--if-r-pill); white-space: nowrap;
}
.if-chip::before {
  content: ""; width: 6px; height: 6px; border-radius: var(--if-r-pill);
  background: currentColor;
}
.if-chip.pass { color: var(--if-pass); background: var(--if-pass-soft); }
.if-chip.watch { color: var(--if-watch); background: var(--if-watch-soft); }
.if-chip.fail { color: var(--if-fail); background: var(--if-fail-soft); }
.if-chip.idle { color: var(--if-idle); background: var(--if-idle-soft); }

/* Score object — renders identically on every screen. A bare "8.0" in body text,
   as the old note detail shipped, is not a score object. */
.if-score {
  display: inline-flex; align-items: baseline; gap: 1px; font-weight: 750;
  font-size: .9375rem; font-variant-numeric: tabular-nums;
}
.if-score s { text-decoration: none; font-size: .625rem; color: var(--if-ink-faint); font-weight: 600; }
.if-score.pass { color: var(--if-pass); }
.if-score.watch { color: var(--if-watch); }
.if-score.fail { color: var(--if-fail); }
.if-score.idle { color: var(--if-idle); }

/* Check run — one tick per compliance check. Passing ticks sit low and lightened
   so a single failing one is findable in a long column. A clean run carries no
   information tick-by-tick, so it collapses to one hairline and colour stays
   spent only on rows that need attention. */
.if-run { display: inline-flex; align-items: flex-end; gap: 2px; height: 15px; }
.if-run i {
  display: block; width: 3px; height: 8px; border-radius: 1px;
  background: var(--if-pass); opacity: .5;
}
.if-run i.warn { height: 15px; opacity: 1; background: var(--if-watch); }
.if-run i.fail { height: 15px; opacity: 1; background: var(--if-fail); }
.if-run.clean i { width: 26px; height: 3px; opacity: .35; }
.if-run.clean i + i { display: none; }

/* Toolbar and saved views */
.if-toolbar {
  background: var(--if-surface); border: 1px solid var(--if-border);
  border-radius: var(--if-r-lg) var(--if-r-lg) 0 0; border-bottom: 0;
  padding: 11px 14px; display: flex; align-items: center; gap: 9px; flex-wrap: wrap;
}
.if-views {
  display: inline-flex; background: var(--if-surface-sunk);
  border: 1px solid var(--if-border); border-radius: var(--if-r-sm); padding: 2px; gap: 2px;
}
/* A saved view can be a button (client-side) or a link (a real URL you can
   bookmark and share, which is what the notes screen wants). Both are styled, and
   a link marks its active state with aria-current — aria-pressed is only valid on
   a button. */
.if-views button, .if-views a {
  border: 0; background: none; font-size: .78125rem; font-weight: 600;
  color: var(--if-ink-soft); padding: 5px 11px; border-radius: 5px; cursor: pointer;
  display: inline-flex; align-items: center; gap: 6px; text-decoration: none;
  transition: background var(--if-t) var(--if-ease), color var(--if-t) var(--if-ease);
}
.if-views button:hover, .if-views a:hover { color: var(--if-ink); text-decoration: none; }
.if-views button[aria-pressed="true"],
.if-views a[aria-current="page"] {
  background: var(--if-surface); color: var(--if-ink); box-shadow: 0 1px 2px rgba(20, 30, 55, .10);
}
.if-views button .n, .if-views a .n {
  font-variant-numeric: tabular-nums; font-size: .6875rem;
  color: var(--if-ink-faint); font-weight: 700;
}
/* min-width:0 so a filter can shrink below its intrinsic width — without it a
   fixed-width search input pushes the page sideways on a phone instead of
   narrowing. */
.if-filt { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
.if-filt select, .if-filt input { max-width: 100%; min-width: 0; }
.if-filt label { font-size: .75rem; color: var(--if-ink-faint); font-weight: 600; }
.if-filt select, .if-filt input {
  font: inherit; font-size: .78125rem; padding: 5px 8px; border-radius: var(--if-r-sm);
  border: 1px solid var(--if-border-strong); background: var(--if-surface); color: var(--if-ink);
}

/* Table — the portal's primary component.
   No overflow:hidden on the wrapper: it would make the wrapper the nearest
   scrollport, so the sticky <thead> would pin to the top of the table instead of
   the viewport, leaving a gap that rows scroll up into. Corners clip on the
   footer instead. */
.if-twrap {
  background: var(--if-surface); border: 1px solid var(--if-border);
  border-radius: 0 0 var(--if-r-lg) var(--if-r-lg); box-shadow: var(--if-shadow);
}
.if-twrap.standalone { border-radius: var(--if-r-lg); }
.if-table { width: 100%; border-collapse: collapse; table-layout: fixed; }
.if-table thead th {
  background: var(--if-surface-sunk); text-align: left;
  font-size: .6875rem; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
  color: var(--if-ink-faint); padding: 9px 14px;
  border-bottom: 1px solid var(--if-border); white-space: nowrap;
  position: sticky; top: var(--if-topbar-h); z-index: 5;
  /* table-layout is fixed and this is nowrap, so a label wider than its column does
     not widen the column — it SPILLS out of it. On the last column that spill leaves
     the viewport and scrolls the whole page sideways: the session tracker's 13-column
     board pushed 14px at 1100 and 9px at 1180, on a page where every other converted
     table was clean. Clip rather than spill.

     Safe for the sticky header, and this is the part worth being sure about: overflow
     breaks `position: sticky` when it is set on an ANCESTOR of the sticky element —
     which is why .if-twrap deliberately has none. Here the th IS the sticky element,
     and its own overflow does not affect it. */
  overflow: hidden;
}
/* The control lives inside the <th>: a <th> with a click handler is not
   focusable and never reaches a keyboard. aria-sort stays on the cell. */
.if-table thead th.sortable { padding: 0; }
.if-th-btn {
  display: inline-flex; align-items: center; width: 100%; border: 0; background: none;
  cursor: pointer; user-select: none; font: inherit; font-size: .6875rem;
  font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
  color: var(--if-ink-faint); padding: 9px 14px;
}
.if-th-btn:hover { color: var(--if-ink); }
.if-table th.num .if-th-btn { justify-content: flex-end; }
.if-table thead th .arrow { opacity: 0; margin-left: 4px; flex: none; }
.if-table thead th[aria-sort] .if-th-btn { color: var(--if-ink); }
.if-table thead th[aria-sort] .arrow { opacity: 1; }
/* The glyph points down; down is descending. So ascending is what rotates. */
.if-table thead th[aria-sort="ascending"] .arrow { transform: rotate(180deg); }
.if-table th.num, .if-table td.num { text-align: right; }

.if-table tbody tr {
  border-bottom: 1px solid var(--if-border);
  /* Keeps a row clear of the pinned header when scrolled to by keyboard or
     anchor — otherwise the sticky <th> covers the row just navigated to. */
  scroll-margin-top: calc(var(--if-topbar-h) + 56px);
  transition: background var(--if-t) var(--if-ease);
}
.if-table tbody tr:last-child { border-bottom: 0; }
.if-table tbody tr.clickable { cursor: pointer; }
.if-table tbody tr.clickable:hover { background: var(--if-surface-sunk); }
.if-table tbody tr.is-picked { background: var(--if-accent-soft); }
.if-table tbody td { padding: 11px 14px; font-size: .8125rem; vertical-align: middle; }

/* Two lines maximum per cell, and never a third from a wrap. Ragged row heights
   are what make a long list tiring to scan. */
.if-l1 {
  font-weight: 600; color: var(--if-ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.if-l2 {
  font-size: .75rem; color: var(--if-ink-faint); margin-top: 1px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Opt-in, because the ellipsis default is right for a single short value (an
   email, a provider type) and wrong for a compound one. A secondary line reading
   "10d ago · session submitted" clipped to "10d ago · session sub…" has lost the
   fact it existed to carry, and a second line of text costs less than that. */
.if-l1.wrap, .if-l2.wrap {
  white-space: normal; overflow: visible; text-overflow: clip; line-height: 1.35;
}
.if-cellrow { display: flex; align-items: center; gap: 8px; }

.if-tfoot {
  display: flex; align-items: center; gap: 12px; padding: 11px 14px;
  border-top: 1px solid var(--if-border); background: var(--if-surface);
  border-radius: 0 0 var(--if-r-lg) var(--if-r-lg);
  font-size: .8125rem; color: var(--if-ink-soft);
}
/* Wraps: a numbered pager on a large table can run to four-digit page numbers,
   and at 390px an unwrapped row of them pushed the page 82px sideways. */
.if-pager { display: inline-flex; gap: 4px; margin-left: auto;; flex-wrap: wrap; }
.if-pager button, .if-pager a {
  border: 1px solid var(--if-border-strong); background: var(--if-surface);
  border-radius: var(--if-r-sm); padding: 4px 10px; font-size: .78125rem;
  font-weight: 600; cursor: pointer; color: var(--if-ink-soft); text-decoration: none;
}
.if-pager button:hover:not(:disabled), .if-pager a:hover {
  border-color: #b9c3d6; color: var(--if-ink); text-decoration: none;
}
.if-pager button:disabled { opacity: .4; cursor: not-allowed; }
.if-pager [aria-current="page"] {
  background: var(--if-accent); border-color: var(--if-accent); color: var(--if-on-accent);
}

/* Empty states teach the interface and name the next action. */
.if-empty { padding: 52px 24px; text-align: center; }
.if-empty h3 { font-size: 1rem; font-weight: 700; margin-bottom: 6px; }
.if-empty p { color: var(--if-ink-soft); max-width: 46ch; margin: 0 auto 16px; }

/* Loading is a skeleton of the real layout, never a centred spinner. */
.if-sk {
  background: linear-gradient(90deg, #e9edf5 25%, #f2f5fa 37%, #e9edf5 63%);
  background-size: 400% 100%; border-radius: 5px; height: 11px;
  animation: if-shimmer 1.4s ease-in-out infinite;
}
@keyframes if-shimmer {
  0% { background-position: 100% 50%; }
  100% { background-position: 0 50%; }
}

/* Dialog. The UA centres a modal <dialog> with margin:auto; any `* { margin:0 }`
   reset kills that and pins it top-left, so it is restored explicitly. */
.if-dialog {
  border: 0; padding: 0; border-radius: var(--if-r-lg); margin: auto;
  max-width: 520px; width: calc(100% - 32px);
  background: var(--if-surface); color: var(--if-ink); box-shadow: var(--if-shadow-lift);
}
.if-dialog::backdrop { background: rgba(15, 20, 35, .5); }
.if-dialog-head { padding: 18px 20px 14px; border-bottom: 1px solid var(--if-border); }
.if-dialog-body { padding: 16px 20px; max-height: 52vh; overflow: auto; }
.if-dialog-foot {
  padding: 14px 20px; border-top: 1px solid var(--if-border);
  display: flex; gap: 8px; justify-content: flex-end;
}

/* Flash messages */
.if-flash { display: flex; flex-direction: column; gap: 8px; margin-bottom: 18px; }
.if-alert {
  display: flex; align-items: flex-start; gap: 10px; padding: 11px 14px;
  border-radius: var(--if-r); font-size: .8125rem; border: 1px solid;
}
.if-alert.success { color: var(--if-pass); background: var(--if-pass-soft); border-color: #bfe6d5; }
.if-alert.warning { color: var(--if-watch); background: var(--if-watch-soft); border-color: #f3d3b6; }
.if-alert.danger, .if-alert.error { color: var(--if-fail); background: var(--if-fail-soft); border-color: #f5c9d2; }
.if-alert.info { color: var(--if-accent-dark); background: var(--if-accent-soft); border-color: #cfcdfa; }
.if-alert .close {
  margin-left: auto; border: 0; background: none; cursor: pointer;
  color: inherit; opacity: .6; padding: 0 2px;
}
.if-alert .close:hover { opacity: 1; }

/* Error pages (404 / 500) render in the shell — they used to be raw Werkzeug. */
.if-errorpage { padding: 72px 24px; text-align: center; max-width: 52ch; margin-inline: auto; }
.if-errorpage .code {
  font-size: 3.25rem; font-weight: 750; letter-spacing: -.04em;
  color: var(--if-ink-faint); line-height: 1;
}
.if-errorpage h1 { font-size: 1.35rem; font-weight: 700; margin: 12px 0 8px; }
.if-errorpage p { color: var(--if-ink-soft); margin-bottom: 20px; }

/* Score ring — the note-detail focal point. Encodes the score itself, so it is
   content rather than the decorative progress ring the craft floor warns about:
   one per page, and the number is always printed inside it. */
.if-ring {
  --p: 0; --c: var(--if-watch);
  position: relative; width: 76px; height: 76px; border-radius: 50%; flex: none;
  display: grid; place-items: center;
  background: conic-gradient(var(--c) calc(var(--p) * 1%), #e9edf5 0);
}
.if-ring::after {
  content: ""; position: absolute; inset: 8px; border-radius: 50%;
  background: var(--if-surface);
}
.if-ring b {
  position: relative; z-index: 1; font-size: 1.35rem; font-weight: 750;
  letter-spacing: -.02em; font-variant-numeric: tabular-nums;
}
.if-ring small {
  position: relative; z-index: 1; font-size: .625rem; font-weight: 600;
  color: var(--if-ink-faint);
}
.if-score-hero {
  display: flex; align-items: center; gap: 15px; padding: 17px;
  border-bottom: 1px solid var(--if-border);
}

/* Findings — what the scorer actually objected to. Shape as well as colour, so
   severity survives greyscale and colour-vision deficiency. */
.if-findings { display: flex; flex-direction: column; gap: 8px; }
.if-findings li { display: flex; gap: 9px; font-size: .8125rem; line-height: 1.45;
  color: var(--if-ink); }
.if-findings li::before {
  content: ""; width: 6px; height: 6px; flex: none; margin-top: 6px; border-radius: 1px;
  background: var(--if-watch);
}
.if-findings.good li::before { border-radius: var(--if-r-pill); background: var(--if-pass); }

/* Clinical note body. The source is plain text whose paragraph breaks are real
   newlines; rendering it in a plain div collapses them into one wall, which is
   how the old note-detail page presented 150 words as a single block. */
.if-notebody {
  white-space: pre-wrap; font-size: .875rem; line-height: 1.65;
  max-width: 72ch; color: var(--if-ink);
}

/* Key/value facts */
.if-kv { display: grid; grid-template-columns: auto 1fr; gap: 9px 16px; font-size: .8125rem; }
.if-kv dt { color: var(--if-ink-faint); font-weight: 600; white-space: nowrap; }
.if-kv dd { margin: 0; font-variant-numeric: tabular-nums; }

/* Two-column detail layout: content left, verdict right. */
/* The verdict column is usually much shorter than a long note, so it sticks:
   the score and the findings stay on screen while the note is read, instead of
   scrolling away and leaving a tall void beside the body text. */
.if-split { display: grid; grid-template-columns: 1fr 360px; gap: 16px; align-items: start; }
.if-split > aside {
  position: sticky; top: calc(var(--if-topbar-h) + 16px);
  max-height: calc(100vh - var(--if-topbar-h) - 32px); overflow: auto;
}
@media (max-width: 1100px) {
  .if-split { grid-template-columns: 1fr; }
  /* Stacked, a sticky aside would pin over the content it belongs to. */
  .if-split > aside { position: static; max-height: none; overflow: visible; }
}

/* Multi-select with search — replaces bootstrap-select's .selectpicker, the
   reason layout.html loads Bootstrap 4 *and* Bootstrap 5 "for Bootstrap Select
   compatibility". Worth knowing: that widget is already broken in production.
   jQuery is loaded twice in layout.html (head and footer), and the second load
   replaces window.jQuery with a plugin-less instance, so the selectpicker() call
   in {% block scripts %} throws and the provider filter on All Notes never
   initialises. Loading jQuery twice silently unregisters every jQuery plugin.

   Progressive enhancement: the native <select multiple> stays the source of
   truth and is only hidden once the widget is successfully built. */
.if-ms { position: relative; display: inline-block; min-width: 200px; }
.if-ms-native { display: none; }

.if-ms-trigger {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 5px 9px; font-size: .78125rem; text-align: left; cursor: pointer;
  border: 1px solid var(--if-border-strong); border-radius: var(--if-r-sm);
  background: var(--if-surface); color: var(--if-ink-faint);
  transition: border-color var(--if-t) var(--if-ease);
}
.if-ms-trigger:hover { border-color: #b9c3d6; }
.if-ms.has-value .if-ms-trigger { color: var(--if-ink); font-weight: 600; }
.if-ms-trigger[aria-expanded="true"] {
  border-color: var(--if-accent); box-shadow: 0 0 0 3px var(--if-accent-soft);
}
.if-ms-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.if-ms-chev { flex: none; opacity: .7; transition: transform var(--if-t) var(--if-ease); }
.if-ms-trigger[aria-expanded="true"] .if-ms-chev { transform: rotate(180deg); }

/* position: fixed, placed from the trigger's rect. DESIGN.md §6 — the filter bar
   sits directly above a scrolling table, so an absolutely positioned panel would
   be clipped by it. */
.if-ms-panel {
  position: fixed; z-index: 70; background: var(--if-surface);
  border: 1px solid var(--if-border-strong); border-radius: var(--if-r);
  box-shadow: var(--if-shadow-lift); width: max-content; max-width: min(90vw, 380px);
  display: flex; flex-direction: column; overflow: hidden;
}
.if-ms-panel[hidden] { display: none; }
.if-ms-search { padding: 9px 10px; border-bottom: 1px solid var(--if-border); }
.if-ms-q {
  width: 100%; font: inherit; font-size: .8125rem; padding: 6px 9px;
  border: 1px solid var(--if-border-strong); border-radius: var(--if-r-sm);
  background: var(--if-surface-sunk); color: var(--if-ink);
}
.if-ms-q:focus {
  outline: none; background: #fff; border-color: var(--if-accent);
  box-shadow: 0 0 0 3px var(--if-accent-soft);
}
.if-ms-actions {
  display: flex; align-items: center; gap: 6px; padding: 7px 10px;
  border-bottom: 1px solid var(--if-border); background: var(--if-surface-sunk);
}
.if-ms-act {
  border: 0; background: none; cursor: pointer; padding: 2px 6px;
  font-size: .75rem; font-weight: 650; color: var(--if-accent); border-radius: 4px;
}
.if-ms-act:hover { background: var(--if-accent-soft); }
.if-ms-n {
  margin-left: auto; font-size: .6875rem; font-weight: 700; letter-spacing: .04em;
  color: var(--if-ink-faint); font-variant-numeric: tabular-nums;
}
.if-ms-list { max-height: 240px; overflow-y: auto; padding: 5px; margin: 0; }
.if-ms-list li[hidden] { display: none; }
.if-ms-opt {
  display: flex; align-items: center; gap: 9px; padding: 6px 8px;
  border-radius: var(--if-r-sm); font-size: .8125rem; cursor: pointer;
  color: var(--if-ink);
}
.if-ms-opt:hover { background: var(--if-surface-sunk); }
.if-ms-opt input { width: 15px; height: 15px; margin: 0; flex: none; accent-color: var(--if-accent); }
.if-ms-opt:has(input:checked) { background: var(--if-accent-soft); font-weight: 600; }
.if-ms-opt span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.if-ms-none { padding: 18px 12px; text-align: center; font-size: .8125rem; color: var(--if-ink-faint); }

/* ── disclosure ──────────────────────────────────────────────────────────────
   Collapsible sections, as a native <details>. The session-lifecycle pair carries
   35 of them between the two screens; each used to be a <div> with an onclick and
   a jQuery slideToggle. Three things a style attribute cannot express: Safari's
   marker pseudo-element, the chevron rotating on [open], and dropping the head's
   hairline while collapsed.

   Everything else a disclosure needs is already here — .if-card-head lays the
   summary out, and the global :focus-visible rule (~line 105) already covers
   <summary>, so focus comes free.

   Additive only: every selector is scoped under `.if-disclosure >`, so none of the
   other converted screens can be reached by it. */
.if-disclosure > details > summary { cursor: pointer; list-style: none; }
.if-disclosure > details > summary::-webkit-details-marker { display: none; }
.if-disclosure > details > summary .chev {
  margin-left: auto; flex: none; color: var(--if-ink-faint);
  transition: transform var(--if-t) var(--if-ease);
}
.if-disclosure > details[open] > summary .chev { transform: rotate(180deg); }
.if-disclosure > details:not([open]) > summary { border-bottom: 0; }

@media (max-width: 560px) {
  .if-ms { width: 100%; }
  .if-ms-panel { max-width: calc(100vw - 24px); }
}

/* --- Overview: triage strip, attention cards, rollups -------------------- */

/* One sentence of state in the product's own voice, above the numbers. The old
   dashboard opened with four saturated counters and no sentence, so the screen
   never said what was true -- it only said how much of it there was. */
.if-state-line {
  font-size: 1rem; color: var(--if-ink); margin: 0 0 16px; max-width: 72ch;
  line-height: 1.55;
}
.if-state-line b { font-weight: 700; }
.if-state-line .ok { color: var(--if-pass); font-weight: 650; }

.if-triage { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 6px; }
/* An <a>, not a <button>: every tile navigates, and a button that navigates
   loses middle-click, cmd-click and "copy link" for no gain. */
a.if-tri {
  background: var(--if-surface); border: 1px solid var(--if-border);
  border-radius: var(--if-r-lg); padding: 15px 17px; box-shadow: var(--if-shadow);
  text-align: left; display: block; position: relative; color: inherit;
  text-decoration: none;
  transition: border-color var(--if-t) var(--if-ease), transform var(--if-t) var(--if-ease),
    box-shadow var(--if-t) var(--if-ease);
}
a.if-tri:hover {
  border-color: var(--if-border-strong); transform: translateY(-1px);
  box-shadow: var(--if-shadow-lift);
}
.if-tri .k { display: flex; align-items: center; gap: 7px; }
.if-tri .dot { width: 8px; height: 8px; border-radius: 99px; flex: none; }
.if-tri .lbl {
  font-size: .6875rem; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--if-ink-faint);
}
.if-tri .v {
  font-size: 2rem; font-weight: 750; letter-spacing: -.03em; line-height: 1;
  margin: 10px 0 4px;
}
.if-tri .cap { font-size: .75rem; color: var(--if-ink-soft); line-height: 1.45; }
.if-tri .go {
  position: absolute; right: 14px; bottom: 13px; color: var(--if-ink-faint);
  opacity: 0; transition: opacity var(--if-t) var(--if-ease);
}
a.if-tri:hover .go, a.if-tri:focus-visible .go { opacity: 1; }

/* Attention cards. An <article>, never a <button>: a button may only contain
   phrasing content, so wrapping the reasons list in one collapses the whole card
   into a single ~175-character accessible name. The name carries the only link
   and stretches over the card so the whole surface stays clickable. */
.if-att-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
.if-att {
  position: relative; background: var(--if-surface); border: 1px solid var(--if-border);
  border-radius: var(--if-r-lg); padding: 15px 16px; box-shadow: var(--if-shadow);
  display: flex; flex-direction: column; gap: 11px; text-align: left;
  transition: border-color var(--if-t) var(--if-ease), transform var(--if-t) var(--if-ease),
    box-shadow var(--if-t) var(--if-ease);
}
.if-att:hover {
  border-color: var(--if-border-strong); transform: translateY(-2px);
  box-shadow: var(--if-shadow-lift);
}
.if-att:has(.if-att-open:focus-visible) {
  border-color: var(--if-accent);
  box-shadow: 0 0 0 3px var(--if-accent-soft), var(--if-shadow-lift);
}
.if-att-top { display: flex; align-items: flex-start; gap: 12px; }
.if-att-id { min-width: 0; flex: 1; }
.if-att-open {
  font-size: .9375rem; font-weight: 700; letter-spacing: -.01em; color: inherit;
  text-decoration: none;
}
.if-att-open::after { content: ""; position: absolute; inset: 0; border-radius: var(--if-r-lg); }
.if-att-open:focus-visible { outline: none; }
.if-att-meta { font-size: .75rem; color: var(--if-ink-faint); margin-top: 2px; }
.if-att-why {
  list-style: none; padding: 11px 0 0; margin: 0;
  border-top: 1px dashed var(--if-border-strong);
  display: flex; flex-direction: column; gap: 6px;
}
.if-att-why li {
  display: flex; gap: 8px; font-size: .8125rem; color: var(--if-ink-soft); line-height: 1.4;
}
/* Shape as well as colour, so severity survives greyscale and colour-vision
   deficiency. A hollow ring is "watch", a filled square is worse. */
.if-att-why li::before {
  content: ""; width: 6px; height: 6px; border-radius: 99px; flex: none;
  margin-top: 7px; box-shadow: inset 0 0 0 1.6px var(--if-watch);
}
.if-att-why li.idle::before { box-shadow: inset 0 0 0 1.6px var(--if-idle); }
.if-att-foot { display: flex; align-items: center; gap: 10px; }
.if-go-link {
  margin-left: auto; display: inline-flex; align-items: center; gap: 3px;
  font-size: .8125rem; font-weight: 650; color: var(--if-accent); text-decoration: none;
}
.if-att:hover .if-go-link svg { transform: translateX(2px); }
.if-go-link svg { transition: transform var(--if-t) var(--if-ease); }

/* Per-provider pass/watch rollup. flex-basis, not width, so it survives the
   card-collapse layout below 900px where the cell becomes a grid item. */
.if-rollup {
  display: flex; height: 8px; border-radius: 99px; overflow: hidden;
  background: var(--if-idle-soft); flex: 0 1 96px; min-width: 64px;
}
.if-rollup i { display: block; height: 100%; }
.if-rollup i.p { background: var(--if-pass); }
.if-rollup i.w { background: var(--if-watch); }
.if-rollup i.f { background: var(--if-fail); }

/* Score trend. Server-rendered inline SVG, not Chart.js: a six-point polyline
   does not justify a 3rd-party render-blocking script on a PHI page, and the
   shell's own test forbids one. */
.if-spark { display: block; width: 100%; height: auto; overflow: visible; }
.if-spark .ln { fill: none; stroke: var(--if-accent); stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round; }
.if-spark .ar { fill: var(--if-accent-soft); }
.if-spark .pt { fill: var(--if-surface); stroke: var(--if-accent); stroke-width: 2; }
.if-spark .gl { stroke: var(--if-border); stroke-width: 1; stroke-dasharray: 3 4; }
.if-spark-x {
  display: flex; justify-content: space-between; margin-top: 7px;
  font-size: .6875rem; color: var(--if-ink-faint);
}
.if-trend-stats { display: flex; gap: 22px; margin-top: 14px; }
.if-trend-stats div { display: flex; flex-direction: column; gap: 2px; }

@media (max-width: 900px) {
  .if-triage { grid-template-columns: 1fr; }
  .if-att-grid { grid-template-columns: 1fr; }
}

/* --- Providers: cohort strip, neutral tags, row states ------------------- */

/* Cohort filter. This replaces SEVEN stat cards plus a redundant Status
   <select> that set the same state -- two controls that could disagree, and a
   block of vanity counters that pushed the table itself below the fold. The
   counts belong ON the filter that uses them. */
.if-cohorts {
  display: flex; flex-wrap: wrap; align-items: stretch; gap: 6px;
  margin-bottom: 12px;
}
.if-cohort {
  display: flex; flex-direction: column; gap: 2px; padding: 8px 13px;
  border: 1px solid var(--if-border); border-radius: var(--if-r);
  background: var(--if-surface); color: var(--if-ink-soft); cursor: pointer;
  font: inherit; text-align: left; min-width: 92px;
  transition: border-color var(--if-t) var(--if-ease), background var(--if-t) var(--if-ease);
}
.if-cohort:hover { border-color: var(--if-border-strong); }
.if-cohort .n {
  font-size: 1.125rem; font-weight: 700; letter-spacing: -.02em; color: var(--if-ink);
  font-variant-numeric: tabular-nums;
}
.if-cohort .t {
  font-size: .6875rem; font-weight: 650; letter-spacing: .05em; text-transform: uppercase;
  color: var(--if-ink-faint);
}
/* aria-pressed, not aria-current: these are filter toggles, not navigation. */
.if-cohort[aria-pressed="true"] {
  border-color: var(--if-accent); background: var(--if-accent-soft);
  box-shadow: inset 0 0 0 1px var(--if-accent);
}
.if-cohort[aria-pressed="true"] .t { color: var(--if-accent); }
.if-cohort-facts {
  display: flex; align-items: center; gap: 14px; margin-left: auto;
  font-size: .75rem; color: var(--if-ink-soft); padding: 0 4px;
}
.if-cohort-facts b { color: var(--if-ink); font-weight: 700; }

/* A NEUTRAL tag family, deliberately colourless.
   DESIGN.md 4b: "the row's colour budget is already spent on clinical status."
   Adoption state (is this person using the product) and documentation quality
   (is their work billable) are different facts; the old page gave both a
   green/amber/red badge, so two hues competed in one row and neither read as
   authoritative. Colour now belongs to the score alone, and adoption state is
   carried by text plus a filled-vs-hollow dot -- which survives greyscale. */
.if-tag {
  display: inline-flex; align-items: center; gap: 6px; padding: 2px 9px 2px 8px;
  border-radius: 99px; background: var(--if-surface-sunk);
  border: 1px solid var(--if-border-strong);
  font-size: .6875rem; font-weight: 650; color: var(--if-ink-soft);
  white-space: nowrap;
}
.if-tag::before {
  content: ""; width: 6px; height: 6px; border-radius: 99px; flex: none;
  box-shadow: inset 0 0 0 1.5px var(--if-idle);
}
.if-tag.on { color: var(--if-ink); }
.if-tag.on::before { background: var(--if-idle); box-shadow: none; }
/* Record state, not adoption state -- squared off so it never reads as one of
   the four adoption values. */
.if-tag.gone { border-style: dashed; }
.if-tag.gone::before { border-radius: 1px; box-shadow: inset 0 0 0 3px var(--if-ink-faint); }
.if-tag.role {
  padding: 1px 6px; font-size: .625rem; letter-spacing: .04em;
  background: var(--if-accent-soft); border-color: transparent; color: var(--if-accent);
}
.if-tag.role::before { display: none; }

/* Row states. TWO different notions of "inactive", kept apart on purpose:
   is_active is the RECORD (this person left), status is ADOPTION (this person
   isn't using it). A current employee can be never_used and a departed one can
   have been the heaviest user, so dimming both identically read as one fact and
   hid the other. Dimming is reserved for the record state. */
.if-table tr.if-row-gone td { opacity: .62; }
.if-table tr.if-row-gone td:first-child { opacity: 1; }
.if-table tr.if-row-quiet { background: color-mix(in srgb, var(--if-watch) 3%, transparent); }

/* Neutral, deliberately. 395 of 410 providers in this roster have no reviewer, so
   colouring "Not assigned" put a warning hue on 96% of rows -- which is not signal,
   it is a wash. The exception is what has to stand out, and an ASSIGNED row already
   does: it carries a name and an accent-coloured role badge. Colour stays on the
   score (DESIGN.md 4b). */
.if-unassigned { display: inline-flex; align-items: center; gap: 6px;
  color: var(--if-ink-faint); font-size: .75rem; font-weight: 650; }
.if-assignees { display: flex; flex-direction: column; gap: 3px; }
.if-assignee { display: flex; align-items: center; gap: 5px; font-size: .75rem; }

/* Row actions. Icon-only controls need an accessible name and a real hit area;
   the old markup used <i class="fas fa-key"> inside a bare <button> with only a
   title attribute, which is not a name for most screen readers. */
/* Wraps rather than overflows. table-layout is `fixed`, so a cell never grows to
   fit its contents -- four flex:none buttons in a too-narrow column pushed the
   whole page sideways instead. */
.if-rowacts { display: flex; align-items: center; gap: 3px; flex-wrap: wrap; }
.if-iconbtn {
  display: grid; place-items: center; width: 32px; height: 32px; flex: none;
  border: 1px solid transparent; border-radius: var(--if-r-sm); background: none;
  color: var(--if-ink-faint); cursor: pointer; padding: 0;
  transition: background var(--if-t) var(--if-ease), color var(--if-t) var(--if-ease);
}
.if-iconbtn:hover { background: var(--if-surface-sunk); color: var(--if-accent);
  border-color: var(--if-border); }
.if-iconbtn:disabled { opacity: .45; cursor: default; }

/* ── Review state (Story 37.10) ──────────────────────────────────────────────
   The review column is the one that spends colour. DESIGN.md §4b reserves
   pass/watch/fail for the clinical verdict, but the board OPENS on a
   review-state queue, sorts on it and filters on it — it is the axis, not a
   secondary fact, and rendering it flat made the primary signal the only thing
   on the row you had to read rather than see.

   `cleared` stays colourless and specifically stays NOT green: a person
   overriding real findings must not read like the system finding nothing
   (app/models.py records why those two must not read alike).

   The hairline border is doing real work — it is what makes these read as crisp
   objects rather than washes, and it keeps `cleared` legible as a state rather
   than as an unstyled fallback. Colour never travels alone: every one of them
   carries its word, and an unrecognised state lands on the colourless base
   rather than on someone else's hue.

   Shared rather than page-local: the tracker and the upload job's session list
   render the same states, and a second copy would drift into two answers to
   "what does Cleared look like?". */
.st-rv {
  display: inline-block; padding: 2px 9px; border-radius: var(--if-r-pill);
  font-size: .6875rem; font-weight: 700; letter-spacing: .01em; white-space: nowrap;
  max-width: 100%; overflow: hidden; text-overflow: ellipsis;
  color: var(--if-idle); background: var(--if-idle-soft);
  border: 1px solid var(--if-border-strong);
}
.st-rv-needs_review {
  color: var(--if-watch); background: var(--if-watch-soft); border-color: #f0c39a;
}
.st-rv-not_fixed {
  color: var(--if-fail); background: var(--if-fail-soft); border-color: #f3b4c1;
}
.st-rv-sent_to_provider {
  color: var(--if-accent-dark); background: var(--if-accent-soft); border-color: #c3c0f8;
}
.st-rv-no_issues, .st-rv-resolved {
  color: var(--if-pass); background: var(--if-pass-soft); border-color: #a8d9c4;
}
.st-rv-cleared {
  color: var(--if-idle); background: var(--if-idle-soft); border-color: var(--if-border-strong);
}

.if-cell-num { font-variant-numeric: tabular-nums; font-weight: 650; }

/* Feedback for the three POST actions, replacing alert(). aria-live so the
   result is announced instead of only appearing. */
.if-live:empty { display: none; }
.if-live { margin-bottom: 12px; }

@media (max-width: 900px) {
  .if-cohort-facts { margin-left: 0; width: 100%; }
  .if-cohort { flex: 1 1 auto; }
}

/* --- Phase 5: forms, layout grid, tiles, meter ---------------------------- */
/*
 * The 34 templates still on the old layout are mostly CRUD forms and detail
 * screens, and between them they use Bootstrap's `form-group`/`form-control` 19
 * times, `row`/`col-*` 112 times and AdminLTE's `info-box` 60 times. These are
 * the replacements. They live here, once, so a screen conversion never has to
 * add a component -- an inline <style> per screen is how the old portal ended up
 * with 4,652 lines of it.
 *
 * Focus comes free: the global :where(...):focus-visible rule is zero-specificity
 * and already covers every control below.
 */

.if-form { display: flex; flex-direction: column; gap: 16px; }
.if-form fieldset { border: 0; padding: 0; margin: 0; display: contents; }

/* One label, one control, one optional hint, one optional error -- always in that
   order, because a hint placed after the control is read out after the value has
   already been entered. */
.if-field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.if-field :where(label, .if-flabel):not(.if-check),
.if-field > .if-flabel {
  font-size: .75rem; font-weight: 650; color: var(--if-ink-soft); letter-spacing: .01em;
}
.if-field > label .req { color: var(--if-fail); margin-left: 2px; font-weight: 700; }
.if-field .hint { font-size: .75rem; color: var(--if-ink-faint); line-height: 1.45; }
/* Never colour-only: the error carries a glyph and is wired up with
   aria-describedby / aria-invalid at the markup level. */
.if-field .err { font-size: .75rem; color: var(--if-fail); font-weight: 600; }
.if-field .err::before { content: "! "; font-weight: 800; }

.if-field :where(input, select, textarea) {
  font: inherit; font-size: .875rem; width: 100%; box-sizing: border-box;
  padding: 8px 11px; border-radius: var(--if-r-sm);
  border: 1px solid var(--if-border-strong); background: var(--if-surface);
  color: var(--if-ink);
  transition: border-color var(--if-t) var(--if-ease), background var(--if-t) var(--if-ease);
}
.if-field :where(input, select, textarea):hover:not(:disabled):not([readonly]) {
  border-color: var(--if-ink-faint);
}
.if-field :where(input, select, textarea):disabled,
.if-field :where(input, textarea)[readonly] {
  background: var(--if-surface-sunk); color: var(--if-ink-faint); cursor: default;
}
.if-field textarea { min-height: 104px; resize: vertical; line-height: 1.55; }
.if-field :where(input, select, textarea)[aria-invalid="true"] { border-color: var(--if-fail); }
/* Checkbox and radio keep their intrinsic size -- a stretched one is a hit target
   that no longer looks like what it is. */
.if-field :where(input[type="checkbox"], input[type="radio"]) { width: auto; }

.if-check { display: flex; align-items: flex-start; gap: 9px; }
.if-check input { margin: 2px 0 0; flex: none; }
.if-check label { font-size: .875rem; color: var(--if-ink); font-weight: 500; cursor: pointer; }
.if-check .hint { display: block; font-size: .75rem; color: var(--if-ink-faint); margin-top: 2px; }

/* Form footer. Primary action last on the line, because it is the one the eye
   lands on when it stops. */
.if-formfoot {
  display: flex; align-items: center; gap: 9px; padding-top: 4px;
  border-top: 1px solid var(--if-border); margin-top: 4px; padding-top: 16px;
  flex-wrap: wrap;
}

/* Layout grid. `--n` columns above 760px, one below -- no intermediate step,
   because a 3-column form collapsing to 2 leaves an orphan field whose label
   reads as belonging to its neighbour. */
.if-grid { display: grid; gap: 16px; grid-template-columns: 1fr; }
@media (min-width: 760px) {
  .if-grid { grid-template-columns: repeat(var(--n, 2), minmax(0, 1fr)); }
  .if-grid > .span-all { grid-column: 1 / -1; }
}

/* Tiles replace AdminLTE's info-box. No saturated background: the number is the
   content, and a coloured panel behind every one of them is what made the old
   dashboards unreadable (PRODUCT.md). A tile that navigates is an <a>. */
.if-tiles { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); }
.if-tile {
  background: var(--if-surface); border: 1px solid var(--if-border);
  border-radius: var(--if-r-lg); padding: 14px 16px; box-shadow: var(--if-shadow);
  display: flex; flex-direction: column; gap: 3px; min-width: 0;
}
.if-tile .k {
  font-size: .6875rem; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
  color: var(--if-ink-faint);
}
.if-tile .v {
  font-size: 1.5rem; font-weight: 750; letter-spacing: -.02em; color: var(--if-ink);
  font-variant-numeric: tabular-nums; line-height: 1.1;
}
.if-tile .sub { font-size: .75rem; color: var(--if-ink-soft); }
a.if-tile { text-decoration: none; color: inherit;
  transition: border-color var(--if-t) var(--if-ease), transform var(--if-t) var(--if-ease); }
a.if-tile:hover { border-color: var(--if-border-strong); transform: translateY(-1px); }

/* Meter. A bar with no accessible value is decoration, so the markup carries
   role="img" + aria-label or a visible figure beside it. */
.if-meter {
  height: 7px; border-radius: 99px; background: var(--if-idle-soft);
  overflow: hidden; display: block;
}
.if-meter > i { display: block; height: 100%; background: var(--if-accent); }
.if-meter > i.pass { background: var(--if-pass); }
.if-meter > i.watch { background: var(--if-watch); }
.if-meter > i.fail { background: var(--if-fail); }

/* Parent link, for the few screens whose parent is genuinely navigable.
   DESIGN.md 5: no breadcrumb on depth-1 screens, and the rail already names the
   section -- so this is one link back, not a trail. */
.if-back {
  display: inline-flex; align-items: center; gap: 5px; font-size: .8125rem;
  font-weight: 600; color: var(--if-ink-soft); text-decoration: none; margin-bottom: 4px;
}
.if-back:hover { color: var(--if-accent); }

/* Definition rows for read-only detail screens that are not a <table>. */
.if-deflist { display: grid; gap: 0; }
.if-deflist > div {
  display: grid; grid-template-columns: minmax(140px, 22%) 1fr; gap: 14px;
  padding: 9px 0; border-bottom: 1px solid var(--if-border);
}
.if-deflist > div:last-child { border-bottom: 0; }
.if-deflist dt { font-size: .8125rem; color: var(--if-ink-faint); font-weight: 600; }
.if-deflist dd { margin: 0; font-size: .875rem; color: var(--if-ink); min-width: 0;
  overflow-wrap: anywhere; }
@media (max-width: 620px) {
  .if-deflist > div { grid-template-columns: 1fr; gap: 2px; }
}

/* File drop target. The one control in the portal with no CSS-only equivalent and
   no existing primitive to lean on: a <input type="file"> cannot be styled into
   something droppable, so the input is visually hidden and this stands in for it.
   It is a real focusable control (role=button, tabindex=0) rather than a styled
   div, so it is reachable without a mouse — `.over` is applied on dragover only. */
.if-drop {
  border: 2px dashed var(--if-border-strong); border-radius: var(--if-r);
  padding: 22px 16px; text-align: center; cursor: pointer;
  color: var(--if-ink-soft); font-size: .8125rem; background: var(--if-surface);
  transition: border-color var(--if-t) var(--if-ease), background var(--if-t) var(--if-ease);
}
.if-drop:hover, .if-drop.over { border-color: var(--if-accent); background: var(--if-accent-soft); }
.if-drop:focus-visible { outline: 2px solid var(--if-accent); outline-offset: 2px; }
.if-drop .name { display: block; font-weight: 650; color: var(--if-ink); margin-top: 4px; }

.if-drop .ic { display: block; margin: 0 auto 2px; color: var(--if-ink-faint); }

/* Busy indicator for a button mid-request. #720 used `fa-spinner fa-spin`; the shell
   loads no Font Awesome, and a text change alone ("Uploading…") is easy to miss on a
   button that is also now disabled. The global prefers-reduced-motion rule at the end
   of this file stops the rotation without hiding the glyph. */
.if-spin { animation: if-rotate .8s linear infinite; flex: none; }
@keyframes if-rotate { to { transform: rotate(360deg); } }

/* Tally row. The pills themselves are `.if-chip`, so a count reads in the same
   colour language as every other status in the portal. */
.if-tally { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; }

/* Per-row log for a batch operation: an import reports one line per CSV row, which
   is a list of outcomes rather than a table of data — no column is shared between
   a success and a parse error beyond the line number. */
.if-rowlog {
  max-height: 240px; overflow-y: auto;
  border: 1px solid var(--if-border); border-radius: var(--if-r-sm);
}
.if-rowlog > div {
  display: flex; gap: 10px; padding: 6px 11px; font-size: .78125rem;
  border-bottom: 1px solid var(--if-border);
}
.if-rowlog > div:last-child { border-bottom: 0; }
.if-rowlog .ln {
  color: var(--if-ink-faint); font-variant-numeric: tabular-nums;
  min-width: 2.4rem; flex: none;
}
.if-rowlog > div.error .msg { color: var(--if-fail); }

/* Terminal state of a long-running action. A finished import is a DIFFERENT state,
   not a form with a note under it: the inputs are gone, so there is exactly one
   thing left to do — read what happened and dismiss it. */
.if-outcome { text-align: center; padding: 6px 0 2px; }
.if-outcome .mark {
  width: 52px; height: 52px; border-radius: var(--if-r-pill); margin: 0 auto 12px;
  display: flex; align-items: center; justify-content: center; font-size: 1.5rem;
}
.if-outcome .mark.ok { background: var(--if-pass-soft); color: var(--if-pass); }
.if-outcome .mark.part { background: var(--if-watch-soft); color: var(--if-watch); }
.if-outcome .mark.none { background: var(--if-surface-sunk); color: var(--if-ink-soft); }
.if-outcome h3 { font-size: 1.0625rem; font-weight: 700; margin-bottom: 4px; }
.if-outcome .said { font-size: .84375rem; color: var(--if-ink-soft); line-height: 1.5; }
.if-outcome details { margin-top: 16px; text-align: left; }
.if-outcome summary {
  cursor: pointer; font-size: .8125rem; font-weight: 600;
  color: var(--if-ink-soft); padding: 5px 0;
}
.if-outcome summary:hover { color: var(--if-ink); }

/* --- §10 Auth: the pre-auth surface -------------------------------------- */
/*
  The front door, and the only screen in the product where nobody has arrived to
  do a job yet — there is no data, no PHI, and no density pressure. §7 spends its
  one authored moment here, but spends it on the STEP ADVANCE, never on first
  paint: arriving at a login and watching it assemble is the choreography §7
  rules out, and it delays the field you came to type in.

  Composition is the shell's own two layers, held still. The rail carries the
  mark; the working ground carries the form. A second factor advances the right
  column while the rail does not move, so /login and /login/verify read as one
  room rather than two pages.
*/
.if-auth {
  min-height: 100dvh;
  display: grid;
  grid-template-columns: clamp(340px, 38%, 600px) 1fr;
  background: var(--if-bg);
}

.if-auth-rail {
  background: var(--if-rail);
  display: flex;
  flex-direction: column;
  padding: 30px 34px 28px;
  gap: 30px;
}

/* The mark reuses .if-brand-mark's gradient — the one place this system spends a
   gradient, and it is a material of the world rather than an effect applied to it. */
.if-auth-brand { display: flex; align-items: center; gap: 11px; }
.if-auth-brand .if-brand-mark { width: 32px; height: 32px; border-radius: 9px; font-size: .875rem; }
.if-auth-brand b {
  color: #fff; font-weight: 700; font-size: 1rem; letter-spacing: -.015em;
}

/* Optical centre, not geometric: the eye reads a block of text as centred when it
   sits slightly high, and the foot line needs room that does not fight it. */
/* 24ch, not 30: `text-wrap: balance` only evens the rag it is given, and at 30ch
   this sentence broke after "it will", which reads as a stumble. */
.if-auth-say { margin-top: auto; margin-bottom: 14px; max-width: 26ch; }
.if-auth-say p {
  color: #e8ecf5;
  font-size: 1.375rem;
  line-height: 1.38;
  font-weight: 550;
  letter-spacing: -.02em;
  margin: 0;
  text-wrap: balance;
}
/* Tinted from the rail's own hue per the contrast rule — never a flat grey. */
.if-auth-say .who {
  display: block;
  margin-top: 18px;
  padding-top: 16px;
  border-top: 1px solid rgba(255,255,255,.09);
  color: var(--if-rail-ink);
  font-size: .8125rem;
  font-weight: 500;
  line-height: 1.5;
  letter-spacing: 0;
}

.if-auth-foot {
  color: var(--if-rail-ink-dim);
  font-size: .75rem;
  line-height: 1.6;
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
  align-items: baseline;
}
.if-auth-foot a { color: var(--if-rail-ink); text-decoration: none; }
.if-auth-foot a:hover { color: #fff; text-decoration: underline; }
.if-auth-foot a:focus-visible { outline: 2px solid var(--if-accent-hi); outline-offset: 3px; border-radius: 3px; }

.if-auth-main {
  display: grid;
  place-items: center;
  padding: 40px 32px;
  min-width: 0;
}

.if-auth-card { width: 100%; max-width: 384px; }

.if-auth-head { margin-bottom: 22px; }
.if-auth-head h1 {
  font-size: 1.5rem; font-weight: 700; letter-spacing: -.022em;
  color: var(--if-ink); margin: 0 0 6px;
}
.if-auth-head p {
  margin: 0; color: var(--if-ink-soft); font-size: .875rem; line-height: 1.55;
}

.if-auth-form { display: flex; flex-direction: column; gap: 14px; }
.if-auth-form .if-btn-primary { width: 100%; justify-content: center; padding-block: 9px; }

/* One row that carries a label on the left and its escape hatch on the right, so
   "Forgot password?" sits with the field it rescues instead of orphaned below the
   button where it reads as a second primary action. */
.if-auth-labelrow { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.if-auth-labelrow a { font-size: .75rem; font-weight: 600; color: var(--if-accent); text-decoration: none; }
.if-auth-labelrow a:hover { text-decoration: underline; }
.if-auth-labelrow a:focus-visible { outline: 2px solid var(--if-accent); outline-offset: 2px; border-radius: 3px; }

.if-auth-alt {
  margin-top: 20px; padding-top: 18px; border-top: 1px solid var(--if-border);
  font-size: .8125rem; color: var(--if-ink-soft); text-align: center;
}
.if-auth-alt a { color: var(--if-accent); font-weight: 600; text-decoration: none; }
.if-auth-alt a:hover { text-decoration: underline; }

/* --- Step advance: the one authored moment ------------------------------- */
/*
  Only on a step, never on first paint. `.is-step` is set by the template that
  represents an advance (verify, enrol), so /login itself is instant and the
  field is typeable the moment it renders. Exponential ease-out from a state that
  is already legible — the card is never invisible, it settles.
*/
@keyframes if-auth-advance {
  from { opacity: .55; transform: translateY(6px) scale(.994); }
  to   { opacity: 1;   transform: none; }
}
.if-auth-card.is-step { animation: if-auth-advance 240ms var(--if-ease) both; }

/* --- The code field ------------------------------------------------------- */
/*
  ONE input, not six. Six boxes look tidier in a screenshot and are a genuine
  accessibility failure: they fragment the accessible name, break paste, and
  defeat the platform's own one-time-code autofill. The slot rhythm here is
  painted underneath a single field, so `autocomplete="one-time-code"` still
  fires on iOS and Android and a pasted code lands whole.

  Monospace is doing its sanctioned job — §8 permits it for codes and IDs, which
  is exactly what this is.
*/
.if-otp {
  font-family: var(--if-mono);
  font-size: 1.6rem;
  font-weight: 600;
  /* Tracking wide enough that six digits read as six, not as a number. The
     matching text-indent cancels the trailing space the last character's
     tracking would otherwise add, which is what keeps the run optically
     centred rather than sitting left by half a space. Kept modest: at .58em
     the caret in an EMPTY field lands a third of the way across and looks
     like a stray cursor rather than an insertion point. */
  letter-spacing: .38em;
  text-indent: .38em;
  text-align: center;
  padding: 14px 12px;
  width: 100%;
  box-sizing: border-box;
  color: var(--if-ink);
  background: var(--if-surface);
  border: 1px solid var(--if-border-strong);
  border-radius: var(--if-r);
  font-variant-numeric: tabular-nums;
  transition: border-color var(--if-t) var(--if-ease), box-shadow var(--if-t) var(--if-ease);
}
.if-otp::placeholder { color: var(--if-border-strong); }
.if-otp:hover:not(:disabled) { border-color: var(--if-ink-faint); }
.if-otp:focus-visible {
  outline: none;
  border-color: var(--if-accent);
  box-shadow: 0 0 0 3px var(--if-accent-soft);
}
.if-otp[aria-invalid="true"] { border-color: var(--if-fail); }
/* A recovery code is 11 characters with a hyphen, so it cannot carry the
   6-digit field's tracking without wrapping. Same field, looser fit. */
.if-otp.is-recovery { font-size: 1.25rem; letter-spacing: .16em; text-indent: .16em; text-transform: uppercase; }

/* --- Lockout ------------------------------------------------------------- */
/*
  The old page rendered "try again in 5 minutes" — a string that is wrong one
  second after it is painted and offers the user no way to know when it stopped
  being true. This states the deadline and counts to it, then re-enables the form
  itself, so nobody sits refreshing a page to find out whether they may type yet.
*/
.if-lockout {
  display: flex; gap: 11px; align-items: flex-start;
  padding: 11px 13px; border-radius: var(--if-r);
  background: var(--if-watch-soft);
  border: 1px solid color-mix(in srgb, var(--if-watch) 22%, transparent);
  color: var(--if-watch);
  font-size: .8125rem; line-height: 1.5;
}
.if-lockout svg { flex: none; margin-top: 1px; }
.if-lockout b { font-variant-numeric: tabular-nums; font-weight: 700; }

/* Cleared state: the same box, so the eye does not have to re-find it, but the
   clinical scale's "pass" tone and a word rather than a colour swap. */
.if-lockout.is-clear {
  background: var(--if-pass-soft);
  border-color: color-mix(in srgb, var(--if-pass) 22%, transparent);
  color: var(--if-pass);
}

/* --- Password reveal ------------------------------------------------------ */
/*
  The control sits INSIDE the field's box rather than beside it, so the field
  keeps its full measure and the row does not grow a second column that the
  username field above it does not have.
*/
.if-pwwrap { position: relative; display: flex; }
.if-pwwrap input { padding-right: 62px; }
.if-pwtoggle {
  position: absolute; right: 5px; top: 50%; transform: translateY(-50%);
  border: 0; background: none; cursor: pointer; padding: 4px 7px;
  border-radius: var(--if-r-sm);
  font: inherit; font-size: .75rem; font-weight: 650;
  color: var(--if-ink-faint);
  transition: color var(--if-t) var(--if-ease), background var(--if-t) var(--if-ease);
}
.if-pwtoggle:hover { color: var(--if-ink); background: var(--if-surface-sunk); }
.if-pwtoggle:focus-visible { outline: 2px solid var(--if-accent); outline-offset: 1px; color: var(--if-ink); }
.if-pwtoggle:disabled { opacity: .45; cursor: not-allowed; }

/* --- Recovery codes ------------------------------------------------------- */
.if-codes {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: 7px 12px;
  padding: 14px; border-radius: var(--if-r);
  background: var(--if-surface-sunk); border: 1px solid var(--if-border);
  font-family: var(--if-mono); font-size: .8125rem; font-weight: 600;
  color: var(--if-ink); font-variant-numeric: tabular-nums;
}
.if-codes span { display: flex; align-items: center; gap: 8px; }
/* The index is the point of a recovery code list — "I have used four" is the
   only question anyone asks of it. */
.if-codes i {
  font-style: normal; color: var(--if-ink-faint); font-size: .6875rem;
  font-weight: 600; min-width: 1.2em; text-align: right;
}

/* --- Enrolment: QR, key, code actions ------------------------------------- */
/*
  The QR sits on white regardless of the page ground: a scanner needs the quiet
  zone and the contrast, and tinting it to match the surface is the decoration
  that breaks the thing it decorates.
*/
.if-qr {
  background: #fff;
  border: 1px solid var(--if-border);
  border-radius: var(--if-r);
  padding: 16px;
  width: 196px; height: 196px;
  margin: 0 auto 16px;
  display: grid; place-items: center;
}
.if-qr svg { width: 100%; height: 100%; display: block; shape-rendering: crispEdges; }

.if-qr-manual { margin-bottom: 18px; }
.if-qr-manual .hint { margin: 8px 0 10px; }

/* The setup key is read aloud off one screen and typed into another, so it gets
   more air between runs than a recovery-code list needs. */
.if-codes-key { grid-template-columns: repeat(4, 1fr); gap: 6px 10px; letter-spacing: .02em; }

.if-codeacts { display: flex; gap: 8px; margin: 14px 0 4px; }
.if-codeacts .if-btn { flex: 1; justify-content: center; }
.if-codeacts .if-btn.is-ok { color: var(--if-pass); border-color: var(--if-pass); }

.if-codenote { margin: 10px 0 20px; font-size: .75rem; color: var(--if-ink-faint); line-height: 1.5; }

/* --- Steps --------------------------------------------------------------- */
/* Shown only when a second factor genuinely follows, so a password-only sign-in
   is never dressed with a one-of-one progress indicator. */
.if-steps { display: flex; align-items: center; gap: 9px; margin-bottom: 18px; }
.if-steps span {
  height: 3px; flex: 1; border-radius: 2px; background: var(--if-border-strong);
  transition: background var(--if-t) var(--if-ease);
}
.if-steps span.on { background: var(--if-accent); }
.if-steps small {
  flex: none; font-size: .6875rem; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--if-ink-faint);
}

/* --- Responsive ----------------------------------------------------------- */
/*
  The rail does not disappear below the fold-out breakpoint — it becomes a band.
  Hiding it would mean the phone sign-in is the one place the product never says
  its own name, and phones are where a clinician actually checks between sessions.
*/
@media (max-width: 860px) {
  .if-auth { grid-template-columns: 1fr; min-height: 100dvh; grid-template-rows: auto 1fr; }
  .if-auth-rail {
    flex-direction: row; align-items: center; justify-content: space-between;
    gap: 18px; padding: 16px 20px;
  }
  .if-auth-say { margin: 0; max-width: 34ch; }
  .if-auth-say p { font-size: .8125rem; line-height: 1.45; font-weight: 500; }
  .if-auth-say .who, .if-auth-foot { display: none; }
  .if-auth-main { padding: 32px 20px 40px; align-items: start; }
}

@media (max-width: 560px) {
  .if-auth-say { display: none; }
  .if-auth-rail { justify-content: flex-start; }
  .if-auth-head h1 { font-size: 1.35rem; }
  .if-otp { font-size: 1.4rem; letter-spacing: .42em; text-indent: .42em; }
  .if-codes { grid-template-columns: 1fr; }
}

/* --- Responsive: structural, not fluid type ------------------------------ */
@media (max-width: 1024px) {
  .if-shell { grid-template-columns: 1fr; }
  .if-rail {
    position: fixed; inset: 0 auto 0 0; width: var(--if-rail-w); z-index: 60;
    transform: translateX(-100%); transition: transform 240ms var(--if-ease);
  }
  body.if-rail-open .if-rail { transform: none; box-shadow: var(--if-shadow-lift); }
  .if-scrim { display: block; }
  body.if-rail-open .if-scrim { opacity: 1; pointer-events: auto; }
  .if-rail-toggle {
    display: grid; place-items: center; border: 1px solid var(--if-border-strong);
    background: var(--if-surface); border-radius: var(--if-r-sm); padding: 6px 8px;
    cursor: pointer; color: var(--if-ink-soft);
  }
}

@media (max-width: 900px) {
  /* Below 900px a table becomes a card list. It never scrolls sideways — a date
     cell wrapping to five lines on a phone is the failure this rule prevents.
     Mark the identity cell `data-primary` and give every other cell a `data-col`
     label.

     The DEFAULT is deliberately one column. A two-column default cannot know
     which of a given table's columns belong side by side, so it produces a
     zigzag — score on the left under one label, status on the right under
     another — which reads worse than a plain stack. Each screen supplies its own
     card grid (see the notes and roster layouts), and this is the predictable
     fallback until it does. */
  .if-twrap thead { display: none; }
  .if-twrap .if-table, .if-twrap tbody { display: block; width: 100%; }
  .if-twrap tbody tr {
    display: grid; grid-template-columns: 1fr; gap: 7px;
    padding: 13px 14px; width: 100%;
  }
  .if-twrap tbody td,
  .if-twrap tbody td.num {
    display: block; padding: 0; border: 0; text-align: left; min-width: 0;
  }
  .if-twrap tbody td[data-col]:not([data-primary])::before {
    content: attr(data-col) " "; display: block;
    font-size: .625rem; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
    color: var(--if-ink-faint); margin-bottom: 2px;
  }
  .if-twrap tbody td[data-col=""]::before { display: none; }
  /* An empty cell contributes nothing but a gap once stacked. */
  .if-twrap tbody td:empty { display: none; }

  .if-content { padding: 18px 14px 48px; }
  .if-page-head { flex-direction: column; }
  /* wrap, because `.actions` is not always two buttons: validation_rules puts an
     organization <select> in there too, and three items in a nowrap row pushed the
     page 140px sideways at 390px. */
  .if-page-head .actions { margin-left: 0; width: 100%; flex-wrap: wrap; }
  .if-page-head .actions > * { flex: 0 1 auto; min-width: 0; }
}

@media (max-width: 560px) {
  .if-display { font-size: 1.4rem; }
  .if-views { width: 100%; }
  .if-views button { flex: 1; justify-content: center; }
}

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

@media print {
  .if-rail, .if-topbar, .if-scrim { display: none !important; }
  .if-shell { grid-template-columns: 1fr; }
  body { background: #fff; }
}
