:root {
  --bg: #f6f7f9;
  --panel: #ffffff;
  --ink: #16202c;
  --muted: #64748b;
  --line: #e2e8f0;
  --accent: #1f3a5f;
  --accent-ink: #ffffff;
  --ok-bg: #e7f6ed; --ok-ink: #14532d; --ok-line: #86d3a5;
  --bad-bg: #fdeaea; --bad-ink: #7f1d1d; --bad-line: #f0a8a8;
  --warn-bg: #fff6e5; --warn-ink: #7c4a03; --warn-line: #f0c67a;
  --radius: 10px;
}
/* Dark palette. Applied by OS preference unless the viewer has explicitly chosen
   light, and by an explicit dark choice regardless of the OS. The toggle stamps
   data-theme on <html>, so an explicit choice always wins. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #0f1520; --panel: #161e2b; --ink: #e6edf5; --muted: #93a4bb;
    --line: #26303f; --accent: #4b7fc4; --accent-ink: #0b1017;
    --ok-bg: #12301f; --ok-ink: #9ae6b4; --ok-line: #2f6b45;
    --bad-bg: #3a1618; --bad-ink: #fca5a5; --bad-line: #7f3336;
    --warn-bg: #3a2c10; --warn-ink: #fcd48c; --warn-line: #7a5a1d;
  }
}
:root[data-theme="dark"] {
  --bg: #0f1520; --panel: #161e2b; --ink: #e6edf5; --muted: #93a4bb;
  --line: #26303f; --accent: #4b7fc4; --accent-ink: #0b1017;
  --ok-bg: #12301f; --ok-ink: #9ae6b4; --ok-line: #2f6b45;
  --bad-bg: #3a1618; --bad-ink: #fca5a5; --bad-line: #7f3336;
  --warn-bg: #3a2c10; --warn-ink: #fcd48c; --warn-line: #7a5a1d;
}
:root[data-theme="dark"] a { color: #7fb0ee; }

* { box-sizing: border-box; }
body {
  margin: 0; background: var(--bg); color: var(--ink);
  font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
a { color: var(--accent); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) a { color: #7fb0ee; }
}

/* --- impersonation banner ---
   Loud on purpose and above everything, including the sticky top bar: an admin must
   never be a click away from forgetting whose screen they are looking at. */
.impersonation-banner {
  position: sticky; top: 0; z-index: 30;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 8px 20px;
  background: var(--warn-bg); color: var(--warn-ink);
  border-bottom: 2px solid var(--warn-line);
  font-size: 13px;
}
.impersonation-banner .impersonation-mark { font-weight: 700; letter-spacing: 0.2px; }
.impersonation-banner form { margin: 0 0 0 auto; }
.impersonation-banner .muted { color: var(--warn-ink); opacity: 0.75; }
/* Two things cannot both be stuck to the top of the viewport, and between the nav and
   "you are looking at someone else's screen" the warning wins. The banner immediately
   precedes the header in the DOM, so this only applies while one is on screen. */
.impersonation-banner + .topbar { position: static; }

/* --- context bar --- */
.contextbar {
  display: flex; align-items: flex-end; gap: 18px; flex-wrap: wrap;
  padding: 8px 20px; background: var(--panel);
  border-bottom: 1px solid var(--line); font-size: 13px;
}
.contextbar .ctx-label { color: var(--muted); }
.contextbar .pill {
  background: var(--bg); border: 1px solid var(--line);
  padding: 1px 6px; border-radius: 999px; color: var(--muted);
}
.contextbar form { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; margin: 0; }
.contextbar label { display: flex; flex-direction: column; gap: 2px; color: var(--muted); }
.contextbar select { min-width: 150px; }
.contextbar .ctx-actions { display: flex; gap: 6px; }
.contextbar .ctx-active {
  flex-basis: 100%; color: var(--warn-ink); background: var(--warn-bg);
  border: 1px solid var(--warn-line); border-radius: var(--radius);
  padding: 4px 8px;
}

/* --- top bar --- */
.topbar {
  display: flex; align-items: center; gap: 20px; flex-wrap: wrap;
  padding: 10px 20px; background: var(--panel); border-bottom: 1px solid var(--line);
  position: sticky; top: 0; z-index: 10;
}
.brand { font-weight: 700; text-decoration: none; color: var(--ink); letter-spacing: -0.2px; }
.topbar nav { display: flex; gap: 4px; flex-wrap: wrap; flex: 1; }
.topbar nav a {
  padding: 6px 10px; border-radius: 7px; text-decoration: none;
  color: var(--muted); font-size: 14px;
}
.topbar nav a:hover { background: var(--bg); color: var(--ink); }
.topbar nav a.on { background: var(--accent); color: var(--accent-ink); }
.who { display: flex; align-items: center; gap: 12px; font-size: 13px; color: var(--muted); }
.who em { font-style: normal; font-size: 11px; background: var(--bg); padding: 1px 5px; border-radius: 4px; }

/* --- layout ---
   The company shortcut bar that used to sit inside .shell is gone, and with it the
   flex row, the sticky 236px column, the company list and the narrow-screen rules that
   folded the whole thing above the page. .shell is a plain wrapper around one <main>
   now; it is kept so that the next thing that wants to sit beside the page has an
   element to attach to rather than a template edit. */
.shell { display: block; }

.wrap { max-width: 1280px; margin: 0 auto; padding: 22px 20px 60px; }
.narrow { max-width: 420px; margin: 8vh auto; padding: 0 20px; }
h1 { font-size: 22px; margin: 0 0 4px; letter-spacing: -0.3px; }
h2 { font-size: 16px; margin: 26px 0 10px; }
.sub { color: var(--muted); font-size: 14px; margin: 0 0 18px; }
.page-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; flex-wrap: wrap; }

.card {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 16px; margin-bottom: 16px;
}
.grid { display: grid; gap: 14px; }
.grid.cols-4 { grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }
.grid.cols-2 { grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); }

/* --- stat tiles --- */
.stat { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 14px 16px; }
.stat .n { font-size: 26px; font-weight: 700; letter-spacing: -0.5px; }
.stat .k { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .5px; }
.stat.warn { border-color: var(--warn-line); background: var(--warn-bg); }
.stat.warn .n, .stat.warn .k { color: var(--warn-ink); }

/* --- tables --- */
.tablewrap { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel); }
table { border-collapse: collapse; width: 100%; font-size: 14px; }
th, td { text-align: left; padding: 9px 12px; border-bottom: 1px solid var(--line); vertical-align: top; }
th { background: var(--bg); font-size: 12px; text-transform: uppercase; letter-spacing: .4px; color: var(--muted); white-space: nowrap; }
tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover { background: var(--bg); }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
.empty { padding: 28px; text-align: center; color: var(--muted); }

/* --- bits --- */
.tag {
  display: inline-block; font-size: 11px; padding: 2px 7px; border-radius: 20px;
  border: 1px solid var(--line); background: var(--bg); color: var(--muted); white-space: nowrap;
}
.tag.src-sheet   { background: var(--ok-bg); color: var(--ok-ink); border-color: var(--ok-line); }
.tag.src-override{ background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
.tag.src-hubspot { background: var(--warn-bg); color: var(--warn-ink); border-color: var(--warn-line); }
/* Deliberately the quietest of the four: a geographic rule is the weakest claim. */
.tag.src-territory_rule { background: var(--bg); color: var(--muted); border-style: dashed; }
.tag.bad { background: var(--bad-bg); color: var(--bad-ink); border-color: var(--bad-line); }
.tag.enrich-ready   { background: var(--ok-bg);   color: var(--ok-ink);   border-color: var(--ok-line); }
.tag.enrich-pending { background: var(--warn-bg); color: var(--warn-ink); border-color: var(--warn-line); }
.tag.enrich-failed  { background: var(--bad-bg);  color: var(--bad-ink);  border-color: var(--bad-line); }

/* Research JARVIS wrote: prose with its own line breaks, shown verbatim and capped so
   a long profile doesn't push the rest of the page off screen. */
pre.enrichment {
  white-space: pre-wrap; overflow-wrap: anywhere; margin: 10px 0 0;
  max-height: 420px; overflow-y: auto; font: inherit; font-size: 13px;
  line-height: 1.55; color: var(--ink);
}
.muted { color: var(--muted); }
.tiny { font-size: 12px; }
.nowrap { white-space: nowrap; }

.flash { padding: 10px 14px; border-radius: 8px; margin-bottom: 16px; font-size: 14px; }
.flash.ok  { background: var(--ok-bg); color: var(--ok-ink); border: 1px solid var(--ok-line); }
.flash.bad { background: var(--bad-bg); color: var(--bad-ink); border: 1px solid var(--bad-line); }
/* Six templates were already asking for this class and getting nothing: bare `.flash`
   sets padding, radius and a size but no colour, so on /data and /rooms/{id}/data the
   ERROR banner rendered identically to the success banner above it — same box, same
   ink, opposite meaning. The amber tokens are the ones the impersonation banner already
   uses, declared for light, system-dark and an explicit dark choice alike, so this is a
   third colour rather than a lighter .bad. */
.flash.warn { background: var(--warn-bg); color: var(--warn-ink); border: 1px solid var(--warn-line); }

/* --- forms --- */
/* `.fieldname` is a field caption that is NOT a <label>, and it earns its place twice.
   Above a drop zone, the zone itself is already `<label for="file">` — a caption that
   also carried `for` made two labels for one input, which a screen reader reads as one
   run-on name ("The file, Drag your file here or browse for it, button… file") and a
   sighted person finds is a second thing that opens the picker. Above a radio group,
   the caption labels nothing at all: no `for`, wrapping no control. So the caption is
   a plain span (pointed at by `aria-labelledby`) or a <legend>, and this rule is what
   keeps it looking like the label it used to be. */
label, .fieldname { display: block; font-size: 13px; color: var(--muted); margin-bottom: 4px; }
input[type=text], input[type=email], input[type=password], input[type=file], select, textarea {
  width: 100%; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px;
  background: var(--bg); color: var(--ink); font: inherit; font-size: 14px;
}
.field { margin-bottom: 12px; }
/* A radio group is a group. Headed by a <label>, the question ("What is it?") reaches
   nobody using a screen reader — each radio announces its own text and never what it is
   answering — so the two upload forms use fieldset/legend instead. The browser's own
   border, padding and min-width are removed so the group still looks like the .field it
   replaced; `min-inline-size: 0` is the fieldset-specific one, without which a fieldset
   refuses to shrink below its content inside a flex or grid parent. */
fieldset.field { border: 0; margin: 0 0 12px; padding: 0; min-inline-size: 0; }
fieldset.field > legend { padding: 0; }
.row { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; }
.row > * { flex: 1; min-width: 150px; }
.row > .shrink { flex: 0 0 auto; min-width: 0; }

button, .btn {
  display: inline-block; padding: 8px 14px; border-radius: 8px; border: 1px solid var(--accent);
  background: var(--accent); color: var(--accent-ink); font: inherit; font-size: 14px;
  cursor: pointer; text-decoration: none; white-space: nowrap;
}
button:hover, .btn:hover { filter: brightness(1.1); }
.btn.ghost, button.ghost {
  background: var(--panel); color: var(--ink); border-color: var(--line);
}
button.sm, .btn.sm { padding: 4px 9px; font-size: 12.5px; }
button.danger, .btn.danger {
  background: var(--bad-bg); color: var(--bad-ink); border-color: var(--bad-line);
}
/* Sign-in page: the email-link form, and the rule that separates it from the
   OAuth buttons so the two are read as alternatives rather than steps. */
.stack { display: flex; flex-direction: column; gap: 6px; }
.or-rule {
  display: flex; align-items: center; gap: 8px;
  margin: 14px 0; color: var(--muted); font-size: 12px;
}
.or-rule::before, .or-rule::after {
  content: ""; flex: 1; height: 1px; background: var(--line);
}
button.linkish {
  background: none; border: none; color: var(--muted); padding: 0;
  text-decoration: underline; cursor: pointer; font-size: 13px;
}
button.approve { background: #1a7f4b; border-color: #1a7f4b; color: #fff; }
button.reject  { background: var(--panel); border-color: var(--bad-line); color: var(--bad-ink); }

.inline { display: inline; }
/* Search results mid-fetch: dimmed rather than emptied, so the table doesn't jump. */
#territory-results.loading { opacity: .55; transition: opacity .12s ease-in; }
details.assign { margin: 0; }
details.assign summary { cursor: pointer; color: var(--muted); font-size: 12px; list-style: none; }
details.assign summary::-webkit-details-marker { display: none; }
details.assign[open] summary { margin-bottom: 8px; }

/* --- review queue rows --- */
.review-row { padding: 12px 14px; margin-bottom: 10px; }
.review-row.flagged { border-color: var(--warn-line); }
.review-grid {
  display: grid; align-items: start; gap: 14px;
  grid-template-columns: 24px 1fr 90px 1fr 130px;
}
.review-grid .bigname { font-size: 14.5px; font-weight: 600; line-height: 1.3; margin: 2px 0; }
.review-grid .side { min-width: 0; }
.review-grid .side > div { overflow-wrap: anywhere; }
.review-grid .middle { text-align: center; padding-top: 14px; }
.review-grid .middle .score { justify-content: center; }
.review-grid .decide { display: flex; flex-direction: column; gap: 6px; }
.review-grid .decide button { width: 100%; }
.review-row code {
  background: var(--bg); padding: 1px 4px; border-radius: 4px; font-size: 11.5px;
}
.flagbox {
  margin-top: 10px; padding: 8px 10px; border-radius: 8px; font-size: 12.5px;
  background: var(--warn-bg); color: var(--warn-ink); border: 1px solid var(--warn-line);
}
details.why { margin-top: 8px; }
details.why summary { cursor: pointer; color: var(--muted); font-size: 12.5px; }
.whybody { padding: 8px 0 2px; display: flex; flex-direction: column; gap: 5px; }
.runner {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 4px 0; border-top: 1px solid var(--line);
}

@media (max-width: 1000px) {
  .review-grid { grid-template-columns: 24px 1fr; }
  .review-grid .middle { text-align: left; padding-top: 0; }
  .review-grid .middle .score { justify-content: flex-start; }
  .review-grid .decide { flex-direction: row; }
}

/* --- score bar --- */
.score { display: flex; align-items: center; gap: 6px; }
.score .bar { width: 46px; height: 5px; border-radius: 3px; background: var(--line); overflow: hidden; }
.score .bar i { display: block; height: 100%; background: var(--accent); }

/* ===================================================================================
   Whitespace grid + technology heat map
   -----------------------------------------------------------------------------------
   Colour is never the only channel here. Every square also carries a glyph, a number
   or a word, because a rep with a colour-vision deficiency has to read the same page
   as everyone else — and because these pages get printed. The ramps below are declared
   three times over, matching the pattern the rest of this file already uses: :root for
   light, the prefers-color-scheme query for a system-dark reader, and :root[data-theme]
   so the header's own toggle wins over both.
   =================================================================================== */

:root {
  /* Sequential ramp for the two intensity heat modes. Five steps, because five is as
     many as anyone can tell apart at a glance — and every square prints its own number
     anyway, so the ramp only has to suggest the shape. */
  --heat-0: #eef2f7; --heat-0-ink: #64748b;
  --heat-1: #dce9fa; --heat-1-ink: #1c3a5e;
  --heat-2: #9cc6f0; --heat-2-ink: #10243d;
  --heat-3: #4c8fd8; --heat-3-ink: #ffffff;
  --heat-4: #1d4ed8; --heat-4-ink: #ffffff;
  --heat-na: #f3f4f6; --heat-na-ink: #94a3b8;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --heat-0: #18212f; --heat-0-ink: #7f8ea4;
    --heat-1: #1d3552; --heat-1-ink: #c7dcf7;
    --heat-2: #2c5b93; --heat-2-ink: #e8f1fc;
    --heat-3: #4886c9; --heat-3-ink: #06101c;
    --heat-4: #79b0ea; --heat-4-ink: #06101c;
    --heat-na: #1b2230; --heat-na-ink: #67758a;
  }
}
:root[data-theme="dark"] {
  --heat-0: #18212f; --heat-0-ink: #7f8ea4;
  --heat-1: #1d3552; --heat-1-ink: #c7dcf7;
  --heat-2: #2c5b93; --heat-2-ink: #e8f1fc;
  --heat-3: #4886c9; --heat-3-ink: #06101c;
  --heat-4: #79b0ea; --heat-4-ink: #06101c;
  --heat-na: #1b2230; --heat-na-ink: #67758a;
}

/* Read out by a screen reader, invisible on screen. Used wherever a square's meaning
   is carried by a glyph that would otherwise be announced as punctuation. */
.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;
}

.ws-legend .row > * { min-width: 240px; }
.ws-legend .tag { margin: 0 4px 4px 0; }
.ws-key { font-size: 11.5px; }

/* --- the grid --- */
.ws-grid { font-size: 12.5px; }
.ws-grid th, .ws-grid td { padding: 4px 6px; vertical-align: top; }
.ws-grid thead th { position: sticky; top: 0; z-index: 2; }
.ws-grid th.ws-account {
  position: sticky; left: 0; z-index: 3; background: var(--panel);
  text-align: left; font-size: 12.5px; text-transform: none; letter-spacing: 0;
  color: var(--ink); min-width: 190px; max-width: 240px;
}
.ws-grid thead th.ws-account { z-index: 4; background: var(--bg); }
.ws-grid td.ws-cell { min-width: 110px; max-width: 190px; border-left: 1px solid var(--line); }
.ws-cell details { margin: 0; }
.ws-cell summary { cursor: pointer; list-style: none; }
.ws-cell summary::-webkit-details-marker { display: none; }
.ws-cell .ws-mark { font-weight: 700; margin-right: 3px; }
.ws-cell .ws-names { font-weight: 600; }
.ws-cell .ws-meta { display: block; font-size: 10.5px; opacity: .85; }
.ws-cell .ws-status-word { text-transform: lowercase; }
.ws-cell .ws-band { margin-left: 5px; font-style: italic; }
.ws-cell .ws-flag { margin-left: 5px; font-weight: 700; }
.ws-cell.ws-empty { color: var(--muted); text-align: center; }

/* Status. Each pairs a colour with the glyph the template already prints, so removing
   the colour loses decoration rather than information. */
.ws-we_sold    { background: var(--ok-bg);   color: var(--ok-ink); }
.ws-competitor { background: var(--bad-bg);  color: var(--bad-ink); }
.ws-evaluating { background: var(--warn-bg); color: var(--warn-ink); }
.ws-incumbent  { background: var(--panel);   color: var(--ink); }
.ws-unknown    { background: var(--bg);      color: var(--muted); }
.tag.ws-we_sold    { border-color: var(--ok-line); }
.tag.ws-competitor { border-color: var(--bad-line); }
.tag.ws-evaluating { border-color: var(--warn-line); }

/* How well attested. An edge treatment and an italic word, never a change of hue and
   never an opacity ramp: "faded" is unreadable without a neighbour to compare against,
   and this is the distinction the product owner cares most about — an inference must
   not be able to look like a fact. */
.ws-cell.band-verified  { box-shadow: inset 3px 0 0 var(--accent); }
.ws-cell.band-reported  { border-top: 1px dashed var(--muted); }
.ws-cell.band-overheard { border-top: 2px dotted var(--muted); font-style: italic; }
.tag.band-reported  { border-style: dashed; }
.tag.band-overheard { border-style: dotted; font-style: italic; }

.ws-cell.contested { outline: 1px solid var(--warn-line); outline-offset: -1px; }
.ws-cell.renewal-soon { box-shadow: inset 0 -3px 0 var(--warn-line); }
.ws-cell.renewal-passed { box-shadow: inset 0 -3px 0 var(--line); }
.tag.contested,
.tag.renewal-soon {
  background: var(--warn-bg); color: var(--warn-ink); border-color: var(--warn-line);
}

.ws-drill {
  padding: 6px 2px 2px; font-size: 12px; border-top: 1px solid var(--line);
  margin-top: 4px; white-space: normal;
}
.ws-claim + .ws-claim { margin-top: 8px; border-top: 1px dotted var(--line); padding-top: 6px; }
.ws-drill blockquote { margin: 4px 0; padding-left: 8px; border-left: 2px solid var(--line); color: var(--muted); }
.ws-others { margin: 2px 0 0 14px; padding: 0; }
.ws-others .disagrees { color: var(--warn-ink); font-weight: 600; }

/* --- mode tabs --- */
.modebar { display: flex; gap: 4px; flex-wrap: wrap; margin: 0 0 6px; }
.modetab {
  padding: 6px 12px; border: 1px solid var(--line); border-radius: 999px;
  text-decoration: none; color: var(--muted); font-size: 13.5px; background: var(--panel);
}
.modetab.on { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }

/* --- the heat map --- */
.heatmap { font-size: 12.5px; }
.heatmap th, .heatmap td { padding: 5px 8px; }
.heatmap th.ws-account {
  position: sticky; left: 0; z-index: 3; background: var(--panel);
  text-align: left; text-transform: none; letter-spacing: 0; color: var(--ink);
  font-size: 12.5px; min-width: 190px;
}
.heatmap thead th { position: sticky; top: 0; z-index: 2; }
.heatmap thead th.ws-account { z-index: 4; background: var(--bg); }
.heatmap td.heat {
  text-align: center; font-variant-numeric: tabular-nums; font-weight: 700;
  border-left: 1px solid var(--line); min-width: 64px;
}
.heat-text { display: inline-block; min-width: 1.2em; }

.heat-i0 { background: var(--heat-0); color: var(--heat-0-ink); }
.heat-i1 { background: var(--heat-1); color: var(--heat-1-ink); }
.heat-i2 { background: var(--heat-2); color: var(--heat-2-ink); }
/* The top two steps carry a hatch as well, so the ramp has a second channel besides
   lightness for anyone reading it in greyscale or with reduced colour discrimination. */
.heat-i3 {
  background: var(--heat-3); color: var(--heat-3-ink);
  background-image: repeating-linear-gradient(45deg, rgba(255,255,255,.16) 0 3px, transparent 3px 7px);
}
.heat-i4 {
  background: var(--heat-4); color: var(--heat-4-ink);
  background-image: repeating-linear-gradient(45deg, rgba(255,255,255,.28) 0 3px, transparent 3px 6px);
}
.heat-na { background: var(--heat-na); color: var(--heat-na-ink); font-weight: 400; }

.heat-we_sold    { background: var(--ok-bg);   color: var(--ok-ink); }
.heat-competitor {
  background: var(--bad-bg); color: var(--bad-ink);
  background-image: repeating-linear-gradient(-45deg, rgba(127,29,29,.14) 0 3px, transparent 3px 7px);
}
.heat-evaluating { background: var(--warn-bg); color: var(--warn-ink); }
.heat-incumbent  { background: var(--panel);   color: var(--ink); }
.heat-unknown    { background: var(--bg);      color: var(--muted); }
.heat-whitespace { background: transparent;    color: var(--muted); font-weight: 400; }
.heat-key { margin: 0 4px 4px 0; }

/* --- the partner coverage matrix ---------------------------------------------------
   Accounts down, partners across, and a binary cell: a rep from that partner covers
   this account, or we cannot say that they do. Three states are drawn, and each one
   carries a glyph as well as a colour — a matrix printed in black and white, or read by
   someone who cannot separate the greens from the greys, still says the same thing. The
   third state (a row on an aged list) exists because dropping the column would answer
   "why did our coverage drop?" by deleting the evidence. */
.cov-grid { font-size: 12.5px; }
.cov-grid th, .cov-grid td { padding: 5px 8px; }
.cov-grid thead th { position: sticky; top: 0; z-index: 2; }
.cov-grid th.cov-account {
  position: sticky; left: 0; z-index: 3; background: var(--panel);
  text-align: left; text-transform: none; letter-spacing: 0; color: var(--ink);
  font-size: 12.5px; font-weight: 600; min-width: 200px; max-width: 260px;
}
.cov-grid thead th.cov-account { z-index: 4; background: var(--bg); }
.cov-grid th.cov-col { min-width: 84px; }
.cov-grid th.cov-aged { font-style: italic; }
.cov-grid td.cov-cell {
  text-align: center; border-left: 1px solid var(--line); font-weight: 700; min-width: 64px;
}
.cov-cell.cov-yes { background: var(--ok-bg); color: var(--ok-ink); }
/* Hatched as well as tinted: "used to count" must not read as a paler "counts". */
.cov-cell.cov-was {
  background: var(--bg); color: var(--muted); font-weight: 400; font-style: italic;
  background-image: repeating-linear-gradient(45deg, rgba(127,127,127,.18) 0 3px, transparent 3px 7px);
}
.cov-cell.cov-no { color: var(--muted); font-weight: 400; }
.cov-legend .tag { margin: 0 4px 4px 0; }
.cov-key { font-size: 11.5px; }
.tag.cov-aged { border-style: dashed; font-style: italic; color: var(--muted); }

/* The /admin index: one list of links per card, each with a line saying what it is. */
.admin-links { list-style: none; margin: 0; padding: 0; }
.admin-links li { padding: 7px 0; border-top: 1px solid var(--line); font-size: 13px; color: var(--muted); }
.admin-links li:first-child { border-top: 0; }
.admin-links a { font-weight: 600; }


/* --- first run -----------------------------------------------------------------
   The landing page with nothing on it yet. The two setup actions were a small ghost
   button and a grey footnote, which is discoverable only to somebody who already
   knows they exist. */
.firstrun { max-width: 720px; }
.firstrun h2 { margin: 0 0 4px; }
.startsteps { list-style: none; counter-reset: s; margin: 18px 0 12px; padding: 0;
              display: flex; flex-direction: column; gap: 14px; }
.startsteps li { counter-increment: s; display: grid;
                 grid-template-columns: 30px minmax(0, 1fr) auto;
                 gap: 14px; align-items: center; }
.startsteps li::before { content: counter(s); font-variant-numeric: tabular-nums;
                         font-size: 1.15rem; opacity: .55; }
.startsteps li > div { font-size: .94rem; line-height: 1.5; }
@media (max-width: 620px) {
  .startsteps li { grid-template-columns: 24px minmax(0, 1fr); }
  .startsteps li .btn { grid-column: 2; justify-self: start; }
}

/* --- landing tiles ---------------------------------------------------------------
   Three, in the order the work happens: get your data in, look at your rooms, open a
   new one. Each carries a live number, because a tile that only names a destination is
   a link wearing a box. */
.tiles { display: grid; gap: 14px; margin: 4px 0 8px;
         grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.tile { display: flex; flex-direction: column; gap: 6px; padding: 20px 22px;
        border: 1px solid var(--line); border-radius: 4px;
        background: var(--panel, #fff); text-decoration: none; color: inherit;
        transition: border-color .12s ease, transform .12s ease; }
.tile:hover { border-color: currentColor; transform: translateY(-1px); }
.tile:focus-visible { outline: 2px solid; outline-offset: 3px; }
.tile-k { font-size: .74rem; letter-spacing: .1em; text-transform: uppercase;
          opacity: .62; }
.tile-v { font-size: 2.1rem; font-weight: 650; line-height: 1;
          font-variant-numeric: tabular-nums; }
.tile-l { font-size: .88rem; opacity: .78; line-height: 1.45; }
.tile-go { margin-top: auto; padding-top: 10px; font-size: .84rem; font-weight: 600; }
.tile.accent { border-width: 2px; }
/* A tile that is not a link. /data lists the lists you have loaded in the same box the
   admin index uses for its destinations, and `.tile:hover`'s lift and border highlight
   promised a click that does nothing — the only controls on those tiles are the buttons
   inside them. Same box, no affordance. */
.tile.static { cursor: default; }
.tile.static:hover { border-color: var(--line); transform: none; }
.mt { margin-top: 26px; }

/* --- drop zone and per-file type rows ---------------------------------------------
   The zone is a <label> wrapping a real file input: click, tab and space all work
   because the input is still there, not because script re-implements them. The input is
   clipped rather than display:none so it stays focusable and reachable. */
.dropzone { position: relative; display: flex; flex-direction: column; gap: 4px;
            align-items: center; justify-content: center; text-align: center;
            padding: 30px 20px; margin: 8px 0 12px; cursor: pointer;
            border: 2px dashed var(--line); border-radius: 6px;
            background: var(--panel, #fff);
            transition: border-color .12s ease, background .12s ease; }
.dropzone:hover { border-color: currentColor; }
.dropzone.is-over { border-style: solid; border-color: currentColor;
                    background: color-mix(in srgb, currentColor 6%, transparent); }
.dropzone:focus-within { outline: 2px solid; outline-offset: 3px; }
.dropzone-main { font-weight: 600; }
.dropzone-sub { font-size: .86rem; opacity: .72; }
.dropzone .linkish { text-decoration: underline; }
/* Clipped, not hidden — display:none would take it out of the tab order.
   Clipped only under [data-js], and that is the fix for a confirmation this zone
   removed and never put back: the clip hides the browser's own "Connections.csv" as
   well, so with script off a person chose a file and the box still read "Drag your file
   here". The attribute is set in base.html's <head>, before first paint, so nobody sees
   the input flash into place; without the script that writes the filename into the
   prompt, the native control stays visible and says it itself. */
:root[data-js] .dropzone-input { position: absolute; width: 1px; height: 1px;
                  padding: 0; margin: -1px;
                  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
                  border: 0; }

.filelist { list-style: none; margin: 0 0 12px; padding: 0;
            display: flex; flex-direction: column; gap: 6px; }
/* A standalone warning line (the drop zone's "that is a folder" message, the noscript
   note). `.warn` on its own was only ever a modifier for .stat, so it had no colour of
   its own -- these read as ordinary grey text without it. */
p.warn { color: var(--warn-ink); margin: 0 0 10px; }

.filelist[hidden] { display: none; }
.filerow { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
           padding: 8px 10px; border: 1px solid var(--line);
           border-radius: 4px; }
.filerow-name { flex: 1 1 12rem; min-width: 0; overflow-wrap: anywhere;
                font-size: .9rem; }
.filerow-size { flex: 0 0 auto; font-variant-numeric: tabular-nums; }
.filerow-kind { flex: 0 0 auto; }
.filerow-remove { flex: 0 0 auto; }

/* --- plan tabs ---------------------------------------------------------------------
   Three slices of one list, not three pages. The count rides on each tab so the two you
   are not looking at still say how big they are — a filter that hides its own existence
   is how somebody concludes their upload failed. */
.tabs { display: flex; flex-wrap: wrap; gap: 4px; margin: 10px 0 6px;
        border-bottom: 1px solid var(--line); }
.tab { display: inline-flex; align-items: center; gap: 7px; padding: 7px 13px;
       text-decoration: none; color: inherit; font-size: .9rem;
       border: 1px solid transparent; border-bottom: none; border-radius: 4px 4px 0 0;
       margin-bottom: -1px; opacity: .72; }
.tab:hover { opacity: 1; }
.tab.on { opacity: 1; font-weight: 600; border-color: var(--line);
          background: var(--panel); }
.tab-n { font-size: .78rem; padding: 1px 7px; border-radius: 999px;
         background: color-mix(in srgb, currentColor 12%, transparent);
         font-variant-numeric: tabular-nums; }
.tab:focus-visible { outline: 2px solid; outline-offset: 2px; }

/* --- theme switch ------------------------------------------------------------------
   Named, not a lone glyph. The control it replaced was a single 🌗 whose only label was
   a title attribute — invisible in a busy header, silent to a screen reader, and with no
   tooltip at all on a touch screen. */
.themer { display: inline-flex; align-items: center; gap: 2px; padding: 2px;
          border: 1px solid var(--line); border-radius: 999px; }
.themer-label { font-size: .7rem; letter-spacing: .08em; text-transform: uppercase;
                opacity: .55; padding: 0 6px 0 8px; }
.themer-opt { appearance: none; cursor: pointer; font: inherit; font-size: .8rem;
              padding: 3px 10px; border: 0; border-radius: 999px;
              background: transparent; color: inherit; opacity: .7;
              white-space: nowrap; }
.themer-opt:hover { opacity: 1; }
.themer-opt.on { opacity: 1; font-weight: 600; background: var(--accent);
                 color: var(--accent-ink); }
.themer-opt:focus-visible { outline: 2px solid; outline-offset: 2px; }

/* Narrow screens keep the icons and drop the words — the header is the one place that
   genuinely runs out of room, and by then the segments have been seen at least once. */
@media (max-width: 720px) {
  .themer-label { display: none; }
  .themer-opt { font-size: 0; padding: 4px 8px; }
  .themer-opt span { font-size: .95rem; }
}

/* ===================================================================================
   Glossary tooltips
   -----------------------------------------------------------------------------------
   The mechanism these replace is the one criticised in the rule above, at the theme
   switch: a native `title` is invisible until hover, silent to a screen reader, and
   has NO tooltip at all on a touch screen. Every explanation that used to live in a
   `title` lives here instead, and those three failures are answered separately:

   * the trigger is a real <button>, so it is in the tab order and a keyboard reaches
     it — hover is no longer the only way in;
   * the words themselves sit in a clipped `.sr-only` span that `aria-describedby`
     points at, so a screen reader reads the definition on focus whether or not the
     bubble is on screen, and whether or not the script in base.html ever ran;
   * a tap opens the bubble and a tap anywhere else closes it, which is the failure
     that started this — a phone has no hover state to enter.

   The bubble is `position: fixed` AND appended to <body> by the script rather than
   left where the trigger sits. Either alone is not enough. The app's widest surfaces
   are `.tablewrap` and `.ws-grid`, both `overflow: auto`, and a bubble parented inside
   one is clipped at its edge; meanwhile `button:hover { filter: brightness(1.1) }`
   above would make any hovered button a containing block for a fixed descendant, so
   "fixed" on its own is not a promise either. Reparenting to <body> means no ancestor
   can clip it, whatever the page around it does.
   =================================================================================== */

:root {
  /* Declared three times over, matching the rest of this file: :root for light, the
     media query for a system-dark reader, and :root[data-theme] so the header's own
     switch beats both. A colour defined only inside the media query would be a colour
     the explicit Dark button can never reach. */
  --tip-bg: #16202c; --tip-ink: #f2f6fb; --tip-line: #2b3a4d;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --tip-bg: #26303f; --tip-ink: #e6edf5; --tip-line: #3d4b5f;
  }
}
:root[data-theme="dark"] {
  --tip-bg: #26303f; --tip-ink: #e6edf5; --tip-line: #3d4b5f;
}

/* The trigger reads as the word it explains, with a dotted underline and a help
   cursor. `text-decoration` rather than `border-bottom`, so the marking follows the
   word when a narrow table cell wraps it across two lines. */
.gloss {
  appearance: none; -webkit-appearance: none;
  font: inherit; color: inherit; background: none; border: 0;
  padding: 0; margin: 0; text-align: inherit; line-height: inherit;
  letter-spacing: inherit; text-transform: inherit; white-space: inherit;
  cursor: help;
  text-decoration: underline dotted; text-underline-offset: 3px;
  text-decoration-thickness: 1px;
}
.gloss:hover, .gloss[data-tip-open="true"] { text-decoration-style: solid; }
.gloss:focus-visible { outline: 2px solid; outline-offset: 2px; border-radius: 3px; }

/* The standalone form, for controls that cannot host a trigger themselves — see
   `templating.hint`. Deliberately quiet: it sits beside a button somebody is about to
   press, and an affordance that competes with that button for attention has picked the
   wrong fight. */
.gloss.mark {
  text-decoration: none; cursor: help;
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.25em; height: 1.25em; margin-left: 3px;
  border: 1px solid currentColor; border-radius: 50%;
  font-size: .8em; line-height: 1; opacity: .6;
  vertical-align: baseline;
}
.gloss.mark:hover, .gloss.mark[data-tip-open="true"] { opacity: 1; }

.tip-bubble {
  /* Above the impersonation banner (z-index 30) and the sticky top bar (10). A
     definition that renders behind the thing it explains is not a definition. */
  position: fixed; z-index: 60; top: 0; left: 0;
  max-width: min(320px, calc(100vw - 24px));
  padding: 7px 10px; border-radius: 8px;
  background: var(--tip-bg); color: var(--tip-ink);
  border: 1px solid var(--tip-line);
  box-shadow: 0 4px 14px rgba(0, 0, 0, .22);
  font-size: 12.5px; line-height: 1.45; font-weight: 400;
  text-align: left; text-transform: none; letter-spacing: normal;
  white-space: normal;
  /* The bubble is scenery. Pointer events on it would let the mouse "enter" the
     tooltip and so leave the word, which reads as flicker. */
  pointer-events: none;
  opacity: 0; transform: translateY(2px);
  transition: opacity .12s ease-out, transform .12s ease-out;
}
.tip-bubble.on { opacity: 1; transform: none; }

/* The arrow's offset is set by the script, because a bubble clamped against the
   viewport edge no longer has its point in the middle — and an arrow aimed at the
   wrong word is worse than no arrow. */
.tip-bubble::after {
  content: ""; position: absolute; width: 8px; height: 8px;
  left: var(--tip-arrow, 50%); margin-left: -4px;
  background: var(--tip-bg); border: 1px solid var(--tip-line);
  transform: rotate(45deg);
}
.tip-bubble.above::after { bottom: -5px; border-top: 0; border-left: 0; }
.tip-bubble.below::after { top: -5px; border-bottom: 0; border-right: 0; }

/* Motion is the whole of the effect, so removing it leaves the tooltip working. The
   `.on` rule is restated because it sets `transform` itself, and a movement suppressed
   in one rule and reintroduced by the next is not suppressed. */
@media (prefers-reduced-motion: reduce) {
  .tip-bubble, .tip-bubble.on { transition: none; transform: none; }
}

/* =====================================================================================
   The account card — Saved Actions, then four panels
   -------------------------------------------------------------------------------------
   Drawn by `app/templates/_account_card.html`, which the rep's room and the partner's
   own half of it both render. One stylesheet for both, for the same reason there is one
   macro file: two cards styled in two places is the defect the August 2026 audit found
   five times over, with a design brief attached.

   **Not one new colour token in here, and that is deliberate.** Every colour below is one
   of the eight already declared on the bare `:root` and redeclared under both
   `prefers-color-scheme: dark` and `:root[data-theme="dark"]`, so the card is correct in
   light, in system dark and under an explicit Dark choice without a ninth palette to keep
   in step. This repo has shipped a rule that asked for a token named `--rule` when the
   declaration said `--line` — a colour that resolves to nothing is not a colour, it is
   the property left at its initial value. `tests/test_dropzones.py` fails on it now, and
   it reads this file as CSS rather than as prose: naming the mistake the way the mistake
   was written would make this comment fail that test, which is why it is described here
   rather than quoted.
   ===================================================================================== */

/* --- the promises --- */
.acct-actions { margin: 12px 0; }

/* Full width, one per line. A promise is a sentence somebody has to be able to read at a
   glance across a table from the person who made it; two of them side by side turns the
   card back into the spreadsheet of ids this redesign was undoing. */
.action-band {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px;
  padding: 8px 10px; margin-bottom: 6px;
  border: 1px solid var(--line); border-left-width: 3px;
  border-radius: 8px; background: var(--bg);
}
.action-band .action-what { flex: 1 1 260px; min-width: 0; }
.action-band .action-text { overflow-wrap: anywhere; }
.action-band .action-meta { display: block; }
.action-band .action-do, .action-band .action-edit { flex: 0 0 auto; }
.action-band .action-edit summary { cursor: pointer; color: var(--muted); font-size: 12px; }
/* The edit form is a `.row` of inputs and needs the whole width once it is open, which a
   flex item sized to its summary will not give it. */
.action-band .action-edit[open] { flex: 1 1 100%; }

/* Late, and said in the border as well as in the tag: a tag alone is one small word in a
   list of small words, and the band it is on is what somebody is scanning. `--bad-line`
   rather than a red of its own, so it is the same "this is wrong" as every refusal in the
   app and it follows the theme. */
.action-band.is-overdue { border-color: var(--bad-line); background: var(--bad-bg); }
.action-band.is-overdue .action-text { color: var(--bad-ink); }

/* Done, and still on the card, because "we did it" is the half of the record a QBR is
   counted against. Quietened rather than removed — and quietened with `--muted`, not with
   opacity, which would take the contrast of the text below any readable ratio. */
.action-band.is-done { border-left-color: var(--ok-line); }
.action-band.is-done .action-text { color: var(--muted); }

.action-add { margin-top: 8px; }

/* --- the four panels, as a tab strip --- */
/* They were four cards side by side. Four columns on a wide screen is four *narrow*
   columns, and a plan repeats them down the page once per account — so the thing a rep
   opened the room for ended up a scroll away, four times over. One panel and four labels
   is the same information at a quarter of the height.

   **The switch is a radio and a `:checked` rule. No script.** Not a preference: this repo
   ships a `data-js` boot flag on the dropzones precisely so that a control which needs a
   bundle degrades honestly, and a tab strip that goes dead when the bundle is clipped
   would hide three quarters of every card with no way back. The markup is written in
   `_account_card.html` — radio, label, panel, adjacent and in that order — and the reason
   the radio group is named after the row rather than the panel is written there too. */

/* The strip and the panel well are one flex container, because the selector below is an
   *adjacent* sibling chain and a chain cannot cross a wrapper. `order: -1` on the labels
   is what separates them visually: every label sorts above every panel, so the four sit
   on one line with the open panel underneath, while the source order stays
   radio → label → panel.

   Source order is worth the trick. It puts the checked radio immediately before the panel
   it opens, so Tab out of the strip lands in the panel the reader just opened — with a
   labels-then-panels layout it would land in whichever panel happened to be written
   first. The cost is that the last label follows the open panel in the DOM while sitting
   before it on screen; arrow keys still walk the four in the order they are drawn,
   because the radios are in source order regardless. */
.acct-cards {
  position: relative;
  display: flex; flex-wrap: wrap; gap: 4px; align-items: flex-start; margin: 12px 0;
}

/* Clipped, and specifically *not* `display: none` or `visibility: hidden`: both of those
   take a control out of the focus order, and the radio is the control — the whole
   keyboard story here is that four radios in a group already answer to the arrow keys.
   Same clip as `.sr-only`, for the same reason. */
.acct-tab-radio {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); border: 0; white-space: nowrap;
}

/* Deliberately the same shape as `.tab` above it. The plan's own three-way filter is the
   only other tab strip in this app and a second visual language for "one of these is
   open" would make the page argue with itself. */
.acct-tab {
  order: -1;
  display: inline-flex; align-items: center; padding: 6px 11px;
  font-size: .85rem; line-height: 1.3; cursor: pointer;
  border: 1px solid transparent; border-radius: 6px;
  opacity: .72;
}
.acct-tab:hover { opacity: 1; }

/* Open, and said in the weight and the border as well as the fill — the same rule the
   overdue band follows. A tab picked out by background colour alone says nothing to a
   reader who cannot see the difference, and nothing at all on paper. */
.acct-tab-radio:checked + .acct-tab {
  opacity: 1; font-weight: 600;
  border-color: var(--line); background: var(--panel);
}

/* The ring goes on the thing you can see. The radio is clipped to a single pixel, so its
   own focus ring would be a dot in the corner of the card — which reads as no focus ring
   at all, and a keyboard user then has no cursor anywhere on the strip. */
.acct-tab-radio:focus-visible + .acct-tab { outline: 2px solid; outline-offset: 2px; }

/* Hidden, never dropped. All four panels are rendered on every card and three are turned
   off here, so switching costs no request and loses no state — a half-typed company field
   is still there when you come back to it.

   The honest cost is that `display: none` is invisible to find-in-page and to print, so
   the print rule below turns the strip off and every panel on. Somebody printing a card
   for a QBR wants all four and has no way to click. */
.acct-panel { display: none; flex: 1 1 100%; }
.acct-tab-radio:checked + .acct-tab + .acct-panel { display: block; }
@media print {
  .acct-tab { display: none; }
  .acct-panel { display: block; }
  /* The heading is clipped by `.sr-only` on screen. On paper it is the only thing left
     saying which panel this is, so every part of that clip is handed back — a `position`
     reset alone leaves a one-pixel box. */
  .acct-panel-title {
    position: static; width: auto; height: auto;
    overflow: visible; clip: auto; white-space: normal;
  }
}

/* Without this a flex item refuses to shrink below its content, and one long product
   string in the stack table pushes the whole page sideways. The table scrolls inside its
   own `.tablewrap` instead; the body never scrolls horizontally. */
.acct-panel { min-width: 0; }
.acct-panel {
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--bg); padding: 10px 12px;
}
/* The panel's heading is `.sr-only` now — the tab label is what a sighted reader reads,
   and printing the same four words again directly under the strip that just said them is
   the "same fact twice" this card has spent two changes removing. The level stays in the
   markup because the outline is the only map of a page this long; only its pixels are
   gone, which is why the print rule above hands them back. */
.acct-panel-title { font-size: 13px; margin: 0 0 8px; }
.acct-panel .tablewrap { background: var(--panel); }
.acct-panel table { font-size: 12.5px; }
.acct-panel th, .acct-panel td { padding: 5px 7px; }

.acct-field { margin-bottom: 8px; }
.acct-value { display: block; font-size: 13px; overflow-wrap: anywhere; }

/* One box, one form, one Save. The button sits on the same line as the input rather than
   under it: five stacked pairs in a 220px column is a wall, and the pairing is what says
   which Save belongs to which value. */
.acct-edit { margin-bottom: 8px; }
.acct-edit-row { display: flex; gap: 6px; align-items: center; }
.acct-edit-row input { min-width: 0; }

/* "A person set this, so the sync must not." Marked in the field's own label, where the
   value is, rather than in a legend somewhere else on the page. */
.tag.acct-corrected { border-color: var(--accent); color: var(--accent); }

.acct-signals { margin-top: 10px; border-top: 1px solid var(--line); padding-top: 8px; }
.acct-signals pre.enrichment { max-height: 220px; }
.acct-research { margin-top: 8px; display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }

/* Reserved, and it has to look *chosen*. An empty fourth column with a solid border is
   read as a panel that failed to load — somebody files it as a bug, or quietly assumes
   the other three are half-loaded too. Dashed and muted is the visual vocabulary this app
   already uses for "deliberately nothing here" (`.tag.src-territory_rule`). */
.acct-panel.acct-reserved {
  border-style: dashed; background: transparent; color: var(--muted);
}
/* The same dashes on its *label*, and this is the part the tab format made necessary. As
   a card the emptiness was on screen and only had to look deliberate; as a tab it is
   behind a click, and a reader who has to click to find out there is nothing there has
   been charged for the answer. So the label is drawn as a placeholder in the strip —
   dashed and muted whether it is open or not, unlike the other three — and it says
   "Reserved for Future Use", which is a sentence admitting it is not in use yet. */
.acct-tab.acct-reserved {
  border-style: dashed; border-color: var(--line); color: var(--muted);
}
