/* DC.Campaigns -- hand-written, no framework, no build step.
   Neutral grays carry the UI; one blue accent. Compact density: the operator lives in this tool
   during a send. Every interactive element declares default/hover/focus/active/disabled. */

/* ---------------------------------------------------------------------------
   Design tokens

   Two raw ramps -- one neutral, one blue -- and everything else is a NAME for a
   step on one of them. A rule that wants "the page background" says so; nothing
   below this block repeats a hex value. Semantic aliases are what views use, so
   a ramp can be retuned in one place without touching a component.
   --------------------------------------------------------------------------- */

:root {
  --c-white:         #ffffff;

  /* Neutral ramp. Carries the whole UI: chrome, text, dividers, disabled states. */
  --c-gray-50:       #f8fafc;
  --c-gray-100:      #f1f5f9;
  --c-gray-200:      #e2e8f0;
  --c-gray-300:      #cbd5e1;
  --c-gray-400:      #94a3b8;
  --c-gray-500:      #64748b;
  --c-gray-600:      #475569;
  --c-gray-700:      #334155;
  --c-gray-800:      #1e293b;
  --c-gray-900:      #0f172a;

  /* The ONE accent family. Nothing in this app is a second brand colour --
     green, amber and red below are status, not decoration. */
  --c-blue-50:       #eff6ff;
  --c-blue-100:      #dbeafe;
  --c-blue-200:      #bfdbfe;
  --c-blue-500:      #3b82f6;
  --c-blue-600:      #2563eb;
  --c-blue-700:      #1d4ed8;
  --c-blue-800:      #1e40af;

  /* ---- surfaces and text ---- */

  --c-bg:            var(--c-gray-50);
  --c-surface:       var(--c-white);
  --c-surface-alt:   var(--c-gray-100);
  --c-border:        var(--c-gray-200);
  --c-border-strong: var(--c-gray-300);

  --c-text:          var(--c-gray-900);
  --c-text-muted:    var(--c-gray-600);
  --c-text-subtle:   var(--c-gray-400);
  --c-on-accent:     var(--c-white);

  /* ---- accent ---- */

  --c-accent:        var(--c-blue-600);
  --c-accent-hover:  var(--c-blue-700);
  --c-accent-active: var(--c-blue-800);
  --c-accent-light:  var(--c-blue-50);
  --c-accent-border: var(--c-blue-200);

  /* ---- status: danger / warn / ok ---- */

  --c-danger:        #b91c1c;
  --c-danger-hover:  #991b1b;
  --c-danger-active: #7f1d1d;
  --c-danger-light:  #fef2f2;
  --c-danger-border: #fecaca;

  --c-warn:          #92400e;
  --c-warn-light:    #fffbeb;
  --c-warn-border:   #fde68a;

  /* Added in gate 7 for the health widgets. Same values the pass badge already used inline. */
  --c-ok:            #047857;
  --c-ok-light:      #ecfdf5;
  --c-ok-border:     #a7f3d0;

  /* ---- type ---- */

  --c-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --c-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  --c-font-xs: 11px;    /* micro-labels and badges only */
  --c-font-sm: 12px;
  --c-font-md: 13px;    /* dense table and metric rows */
  --c-font-base: 14px;
  --c-font-lg: 16px;
  --c-font-xl: 20px;
  --c-font-2xl: 24px;

  /* ---- space, on a 4px base ---- */

  --c-space-1: 4px;
  --c-space-2: 8px;
  --c-space-3: 12px;
  --c-space-4: 16px;
  --c-space-6: 24px;
  --c-space-8: 32px;

  /* ---- shape, depth, motion ---- */

  --c-radius: 6px;
  --c-radius-sm: 4px;
  --c-radius-pill: 999px;

  /* Depth is used sparingly: a 1px divider says "different thing" and a shadow
     says "on top of the page". Only the modal is on top of the page. */
  --c-shadow-modal: 0 12px 32px rgba(15, 23, 42, 0.18);
  --c-overlay: rgba(15, 23, 42, 0.4);

  --c-transition: 150ms ease;

  --c-nav-width: 208px;
}

* { box-sizing: border-box; }

html, body { height: 100%; }

body {
  margin: 0;
  font-family: var(--c-font);
  font-size: var(--c-font-base);
  line-height: 1.5;
  color: var(--c-text);
  background: var(--c-bg);
  -webkit-font-smoothing: antialiased;
}

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

/* Keyboard focus is visible everywhere. Never remove this without a replacement ring. */
:focus-visible {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
  border-radius: var(--c-radius-sm);
}

/* ---- shell ---- */

.app { display: flex; min-height: 100vh; }

.nav {
  width: var(--c-nav-width);
  flex: 0 0 var(--c-nav-width);
  background: var(--c-surface);
  border-right: 1px solid var(--c-border);
  padding: var(--c-space-4) 0;
  display: flex;
  flex-direction: column;
}

/* The wordmark: a mark and a name, the same pair the favicon draws, so the tab and the
   sidebar are recognisably one product. "internal" is a qualifier, not part of the name. */
.nav-brand {
  display: flex;
  align-items: center;
  gap: var(--c-space-2);
  font-size: var(--c-font-base);
  font-weight: 600;
  letter-spacing: -0.01em;
  padding: 0 var(--c-space-2) var(--c-space-4);
  color: var(--c-text);
}

/* Shared by the sidebar and the sign-in card. The qualifier rules below exclude it by
   name rather than out-specifying it, so the mark keeps a single flat rule. */
.brand-mark {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: var(--c-radius-sm);
  background: var(--c-accent);
  color: var(--c-on-accent);
  font-size: var(--c-font-xs);
  font-weight: 700;
  letter-spacing: 0.02em;
}

.nav-brand span:not(.brand-mark) { color: var(--c-text-subtle); font-weight: 400; }

.nav-items { display: flex; flex-direction: column; gap: 1px; flex: 1; padding: 0 var(--c-space-2); }

/* An inset pill rather than a full-bleed row: the gutter is what makes the active item read
   as a selection instead of as a section header. */
.nav-item {
  display: block;
  padding: 6px var(--c-space-2) 6px var(--c-space-3);
  border-radius: var(--c-radius-sm);
  font-size: var(--c-font-base);
  color: var(--c-text-muted);
  transition: background var(--c-transition), color var(--c-transition);
}

.nav-item:hover { background: var(--c-surface-alt); color: var(--c-text); text-decoration: none; }
.nav-item:active { background: var(--c-border); }
.nav-item:focus-visible { outline: 2px solid var(--c-accent); outline-offset: -1px; }

.nav-item.is-active {
  background: var(--c-accent-light);
  color: var(--c-accent-active);
  font-weight: 500;
}

.nav-item.is-disabled {
  color: var(--c-text-subtle);
  pointer-events: none;
  cursor: default;
}

.nav-item.is-disabled::after {
  content: "soon";
  float: right;
  font-size: var(--c-font-xs);
  color: var(--c-text-subtle);
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius-sm);
  padding: 0 var(--c-space-1);
}

.nav-foot { margin: 0 var(--c-space-2); padding: var(--c-space-3) 0 0; border-top: 1px solid var(--c-border); }
.nav-user { font-size: var(--c-font-sm); color: var(--c-text-subtle); margin-bottom: var(--c-space-2); }

.main { flex: 1; min-width: 0; padding: var(--c-space-6); }

.page-head { margin-bottom: var(--c-space-6); }
.page-title { font-size: var(--c-font-2xl); font-weight: 600; letter-spacing: -0.02em; margin: 0; }
.page-sub { color: var(--c-text-muted); font-size: var(--c-font-base); margin: var(--c-space-1) 0 0; }

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

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--c-space-4);
}

.card {
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius);
  padding: var(--c-space-4);
}

/* Same micro-label the stat tiles and count columns use, so every label in the app that
   sits over a number is the same label. */
.card-label {
  font-size: var(--c-font-xs);
  font-weight: 600;
  color: var(--c-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin: 0 0 var(--c-space-2);
}

/* The number is the tile. Tabular figures so a column of tiles does not jitter when the
   dashboard reloads mid-send. */
.card-value {
  font-size: var(--c-font-2xl);
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}

.card-hint { font-size: var(--c-font-sm); color: var(--c-text-subtle); margin-top: var(--c-space-1); }

/* Empty state: says what is missing and what to do about it. Never a fake zero. */
.card-empty { font-size: var(--c-font-sm); color: var(--c-text-subtle); }
.card-empty strong { display: block; color: var(--c-text-muted); font-weight: 500; font-size: var(--c-font-base); }

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

.form-narrow { max-width: 360px; }

.field { margin-bottom: var(--c-space-4); }

/* Labels sit ABOVE inputs. A placeholder is not a label. */
.field label {
  display: block;
  font-size: var(--c-font-sm);
  font-weight: 500;
  color: var(--c-text-muted);
  margin-bottom: var(--c-space-1);
}

.field .req { color: var(--c-danger); }

.input {
  width: 100%;
  height: 32px;
  padding: 0 var(--c-space-3);
  font-family: inherit;
  font-size: var(--c-font-base);
  color: var(--c-text);
  background: var(--c-surface);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--c-radius-sm);
  transition: border-color var(--c-transition), box-shadow var(--c-transition);
}

.input:hover { border-color: var(--c-text-subtle); }
.input:focus { outline: none; border-color: var(--c-accent); box-shadow: 0 0 0 3px var(--c-accent-light); }
.input:disabled { background: var(--c-surface-alt); color: var(--c-text-subtle); cursor: not-allowed; }
.input.has-error { border-color: var(--c-danger); }

.field-error { display: block; margin-top: var(--c-space-1); font-size: var(--c-font-sm); color: var(--c-danger); }

/* The consent checkbox on /sms-optin. The label WRAPS the box so the whole disclosure is a click
   target -- the wording is long by law, and a 13px tickbox as the only target is how people miss it.
   align-items:flex-start keeps the box on the first line rather than centred against a paragraph. */
.checkbox-label {
  display: flex;
  gap: var(--c-space-2);
  align-items: flex-start;
  font-weight: 400;
  font-size: var(--c-font-sm);
  line-height: 1.5;
  color: var(--c-text-subtle);
  cursor: pointer;
}

.checkbox-label input[type="checkbox"] { flex: 0 0 auto; margin-top: 2px; width: 16px; height: 16px; cursor: pointer; }
.checkbox-label input[type="checkbox"]:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 2px; }

/* ---- buttons ---- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--c-space-2);
  height: 32px;
  padding: 0 var(--c-space-4);
  font-family: inherit;
  font-size: var(--c-font-base);
  font-weight: 500;
  border: 1px solid transparent;
  border-radius: var(--c-radius-sm);
  cursor: pointer;
  transition: background var(--c-transition), border-color var(--c-transition), color var(--c-transition);
}

/*
  Four tiers and no more: primary (the one thing this screen is for), secondary (a real
  alternative), tertiary (a way out -- cancel, clear, page), destructive (.btn-danger,
  declared with the segment rules). Every tier states default/hover/focus/active/disabled.
*/
.btn-primary { background: var(--c-accent); color: var(--c-on-accent); }
.btn-primary:hover { background: var(--c-accent-hover); }
.btn-primary:active { background: var(--c-accent-active); }

.btn-secondary { background: var(--c-surface); color: var(--c-text); border-color: var(--c-border-strong); }
.btn-secondary:hover { background: var(--c-surface-alt); }
.btn-secondary:active { background: var(--c-border); }

.btn-tertiary { background: transparent; color: var(--c-text-muted); }
.btn-tertiary:hover { background: var(--c-surface-alt); color: var(--c-text); }
.btn-tertiary:active { background: var(--c-border); }

/* A .btn is often an <a>, and the global anchor underline on hover makes it look like
   prose. The chrome IS the affordance here. */
.btn:hover { text-decoration: none; }

/* Drawn per tier so the ring stays visible against the fill it sits on. Never removed. */
.btn:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 2px; }

.btn:disabled, .btn[aria-disabled="true"] {
  opacity: 0.55;
  cursor: not-allowed;
  pointer-events: none;
}

.btn-block { width: 100%; }

/* Loading state: the spinner replaces the label so the button cannot be double-submitted blind. */
.btn.is-loading { position: relative; color: transparent; pointer-events: none; }
.btn.is-loading::after {
  content: "";
  position: absolute;
  width: 14px;
  height: 14px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  color: var(--c-on-accent);
  animation: spin 600ms linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }

/* ---- banners ---- */

.banner {
  padding: var(--c-space-3) var(--c-space-4);
  border-radius: var(--c-radius-sm);
  font-size: var(--c-font-base);
  margin-bottom: var(--c-space-4);
}

.banner-error {
  background: var(--c-danger-light);
  border: 1px solid var(--c-danger-border);
  color: var(--c-danger);
}

.banner-info {
  background: var(--c-accent-light);
  border: 1px solid var(--c-accent-border);
  color: var(--c-accent-active);
}

/* ---- panels ---- */

.panel {
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius);
  margin-bottom: var(--c-space-4);
}

.panel.is-hidden { display: none; }

.panel-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--c-space-4);
  padding: var(--c-space-3) var(--c-space-4);
  border-bottom: 1px solid var(--c-border);
}

.panel-title {
  font-size: var(--c-font-lg);
  font-weight: 600;
  letter-spacing: -0.01em;
  margin: 0;
}

.panel-note { font-size: var(--c-font-sm); color: var(--c-text-subtle); margin: 0; }

.panel-body { padding: var(--c-space-4); }
.panel-body .field:last-of-type { margin-bottom: var(--c-space-4); }

.file-path {
  margin: 0;
  padding: var(--c-space-2) var(--c-space-3);
  background: var(--c-surface-alt);
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius-sm);
  word-break: break-all;
}

.btn-hint { margin-left: var(--c-space-3); font-size: var(--c-font-sm); color: var(--c-text-subtle); }

.field-check { margin-bottom: var(--c-space-4); }

.check { display: flex; align-items: flex-start; gap: var(--c-space-2); font-weight: 400; color: var(--c-text); }
.check input { margin: 3px 0 0; width: 14px; height: 14px; accent-color: var(--c-accent); cursor: pointer; }
.check span { font-size: var(--c-font-base); }

/* The one moving element on the page: it means "work is happening", so it stays until it isn't. */
.pulse {
  display: inline-block;
  width: 8px;
  height: 8px;
  margin-right: var(--c-space-2);
  border-radius: 50%;
  background: var(--c-accent);
  animation: pulse 1.4s ease-in-out infinite;
}

@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }

.progress-line { margin: 0 0 var(--c-space-2); font-size: var(--c-font-base); }

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

/* Every table lives in its own scroller, so a wide table scrolls itself rather than the page. */
.table-scroll { overflow-x: auto; }

/* Opt-in for a table long enough to be worth pinning its header to. */
.table-scroll-tall { max-height: 60vh; overflow-y: auto; }

.table { width: 100%; border-collapse: collapse; font-size: var(--c-font-base); }

.table th {
  text-align: left;
  font-size: var(--c-font-sm);
  font-weight: 500;
  color: var(--c-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: var(--c-space-2) var(--c-space-3);
  border-bottom: 1px solid var(--c-border);
  white-space: nowrap;
}

/*
  A pinned header. border-collapse drops borders on a sticky cell, so the rule under the
  header is drawn as an inset shadow instead -- otherwise the header floats over the first
  row with nothing separating them. Inert until the container actually scrolls vertically.
*/
.table-scroll .table thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--c-surface);
  box-shadow: inset 0 -1px 0 var(--c-border);
}

.table td {
  padding: var(--c-space-2) var(--c-space-3);
  border-bottom: 1px solid var(--c-border);
  vertical-align: top;
  line-height: 1.45;
}

/* Hover is the row-tracking aid. Deliberately no zebra: 1px lines and a hover tint read
   cleaner than banding, and banding fights every row state this app already has. */
.table tbody tr:hover td { background: var(--c-surface-alt); }

.table tbody tr:last-child td { border-bottom: none; }
.table .num { text-align: right; font-variant-numeric: tabular-nums; }
.table .nowrap { white-space: nowrap; }
.table .file-cell { max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.detail-row td { padding-top: 0; border-bottom: 1px solid var(--c-border); }

.assertion-detail {
  margin: 0;
  padding: var(--c-space-2) var(--c-space-3);
  background: var(--c-surface-alt);
  border-radius: var(--c-radius-sm);
  font-family: var(--c-mono);
  font-size: var(--c-font-sm);
  color: var(--c-text-muted);
  white-space: pre-wrap;
  overflow-x: auto;
}

/* ---- badges ---- */

.badge {
  display: inline-block;
  padding: 1px var(--c-space-2);
  border-radius: var(--c-radius-sm);
  font-size: var(--c-font-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  border: 1px solid transparent;
}

.badge-pass { background: var(--c-ok-light); border-color: var(--c-ok-border); color: var(--c-ok); }
.badge-fail { background: var(--c-danger-light); border-color: var(--c-danger-border); color: var(--c-danger); }
.badge-none { background: var(--c-surface-alt); border-color: var(--c-border); color: var(--c-text-subtle); }

/* ---- empty state ---- */

.empty-state { padding: var(--c-space-8) var(--c-space-4); text-align: center; color: var(--c-text-subtle); }
.empty-icon { font-size: var(--c-font-2xl); color: var(--c-border-strong); line-height: 1; }
.empty-state strong { display: block; margin-top: var(--c-space-2); color: var(--c-text); font-size: var(--c-font-lg); font-weight: 600; }
.empty-state p { max-width: 460px; margin: var(--c-space-1) auto 0; font-size: var(--c-font-sm); }

/*
  The icon as a line drawing rather than a text glyph. Glyph icons (still used by the pages
  this pass did not touch, hence the font-size above) render at whatever weight the system
  font decides; an inline SVG inherits colour and stroke weight from here, so every empty
  state on a redesigned page draws at the same weight.
*/
.empty-icon svg {
  width: 28px;
  height: 28px;
  fill: none;
  /* One step darker than the glyph colour above: a 1.5px stroke needs more contrast than a
     solid glyph of the same size to read at all. */
  color: var(--c-text-subtle);
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ---- login ---- */

.login-shell {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--c-space-4);
}

.login-card {
  width: 100%;
  max-width: 340px;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius);
  padding: var(--c-space-6);
}

/* The same wordmark the sidebar carries. The sign-in card is the one screen with no nav,
   so it is the one screen that has to say what it is the sign-in FOR. */
.login-brand {
  display: flex;
  align-items: center;
  gap: var(--c-space-2);
  margin-bottom: var(--c-space-6);
  font-size: var(--c-font-base);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--c-text);
}

.login-brand span:not(.brand-mark) { color: var(--c-text-subtle); font-weight: 400; }

.login-title { font-size: var(--c-font-xl); font-weight: 600; letter-spacing: -0.02em; margin: 0 0 var(--c-space-1); }
.login-sub { margin: 0 0 var(--c-space-6); font-size: var(--c-font-sm); color: var(--c-text-muted); }

.muted { color: var(--c-text-muted); }
.mono { font-family: var(--c-mono); font-size: var(--c-font-sm); }

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

@media (max-width: 720px) {
  .app { flex-direction: column; }
  .nav { width: 100%; flex: none; border-right: none; border-bottom: 1px solid var(--c-border); }
  .main { padding: var(--c-space-4); }
}

/* ---- segments ---- */

/* Wraps rather than overflows: at 1024 a long campaign name and three buttons do not fit
   on one line, and a page that scrolls sideways is worse than one that stacks. */
.page-head-actions {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--c-space-4);
}

.btn-danger { background: var(--c-danger); color: var(--c-on-accent); }
.btn-danger:hover { background: var(--c-danger-hover); }
.btn-danger:active { background: var(--c-danger-active); }
.btn-danger:focus-visible { outline-color: var(--c-danger); }

.btn-sm { height: 28px; padding: 0 var(--c-space-3); font-size: var(--c-font-sm); }

/* Criteria summary on the list. What a segment SELECTS, not just what it is called. */
.chip-row { display: flex; flex-wrap: wrap; gap: var(--c-space-1); }

.chip {
  display: inline-block;
  padding: 1px var(--c-space-2);
  background: var(--c-surface-alt);
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius-sm);
  font-size: var(--c-font-sm);
  color: var(--c-text-muted);
  white-space: nowrap;
}

.row-note { display: block; font-size: var(--c-font-xs); color: var(--c-text-subtle); }
.row-actions { display: flex; flex-wrap: wrap; gap: var(--c-space-2); align-items: center; }
.actions-col { width: 1%; white-space: nowrap; }

.empty-state .btn { margin-top: var(--c-space-4); }

/* ---- confirm dialog ---- */

.modal {
  width: min(420px, calc(100vw - 32px));
  padding: var(--c-space-6);
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius);
  background: var(--c-surface);
  color: var(--c-text);
  box-shadow: var(--c-shadow-modal);
}

.modal::backdrop { background: var(--c-overlay); }

.modal-title { margin: 0 0 var(--c-space-2); font-size: var(--c-font-lg); font-weight: 600; }
.modal-body { margin: 0 0 var(--c-space-6); font-size: var(--c-font-base); color: var(--c-text-muted); }
.modal-actions { display: flex; justify-content: flex-end; gap: var(--c-space-2); }

/* ---- segment editor ---- */

.edit-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 380px;
  gap: var(--c-space-4);
  align-items: start;
}

.edit-main { min-width: 0; }

.edit-side { position: sticky; top: var(--c-space-6); }

.field-hint { display: block; margin-top: var(--c-space-1); font-size: var(--c-font-sm); color: var(--c-text-subtle); }

.field-warn {
  display: block;
  margin-top: var(--c-space-2);
  padding: var(--c-space-2) var(--c-space-3);
  background: var(--c-warn-light);
  border: 1px solid var(--c-warn-border);
  border-radius: var(--c-radius-sm);
  font-size: var(--c-font-sm);
  color: var(--c-warn);
}

.field-pair {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--c-space-4);
}

.tri-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--c-space-4);
}

.textarea { height: auto; padding: var(--c-space-2) var(--c-space-3); line-height: 1.5; resize: vertical; }

.select { cursor: pointer; }

/* Checkbox grid: 15 states have to be scannable in one glance, not a 15-row column. */
.check-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(76px, 1fr));
  gap: var(--c-space-1);
}

.check-grid-wide { grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); }

.check-tile {
  align-items: center;
  padding: var(--c-space-1) var(--c-space-2);
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius-sm);
  background: var(--c-surface);
  cursor: pointer;
  transition: background var(--c-transition), border-color var(--c-transition);
}

.check-tile:hover { background: var(--c-surface-alt); border-color: var(--c-border-strong); }
.check-tile:active { background: var(--c-border); }
.check-tile input { margin: 0; }
.check-tile span { font-size: var(--c-font-sm); overflow: hidden; text-overflow: ellipsis; }
.check-tile:has(input:checked) { background: var(--c-accent-light); border-color: var(--c-accent); color: var(--c-accent-active); }
.check-tile:has(input:focus-visible) { outline: 2px solid var(--c-accent); outline-offset: 1px; }

.form-actions { display: flex; gap: var(--c-space-2); margin-bottom: var(--c-space-6); }

/* ---- live counts ---- */

.counts-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--c-space-4);
  transition: opacity var(--c-transition);
}

/* Busy means the numbers on screen are STALE, so they are dimmed rather than blanked --
   blanking loses the previous answer while the new one is still in flight. */
.counts-panel.is-busy .counts-grid { opacity: 0.45; }

.count-col { min-width: 0; }

.count-head {
  font-size: var(--c-font-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--c-text-muted);
  padding-bottom: var(--c-space-1);
  border-bottom: 1px solid var(--c-border);
}

.count-hero { padding: var(--c-space-3) 0 var(--c-space-2); }
.count-hero-value { display: block; font-size: var(--c-font-2xl); font-weight: 600; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.count-hero-label { display: block; font-size: var(--c-font-xs); color: var(--c-text-subtle); }

.count-list { margin: 0; }

.count-row { display: flex; justify-content: space-between; gap: var(--c-space-2); padding: 3px 0; }
.count-row dt { font-size: var(--c-font-sm); color: var(--c-text-muted); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.count-row dd { margin: 0; font-size: var(--c-font-sm); font-variant-numeric: tabular-nums; }
.count-row-sub dt, .count-row-sub dd { color: var(--c-text-subtle); }

.count-sub-head {
  margin-top: var(--c-space-3);
  padding-top: var(--c-space-2);
  border-top: 1px solid var(--c-border);
  font-size: var(--c-font-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--c-text-subtle);
}

.counts-foot {
  margin: var(--c-space-4) 0 0;
  padding-top: var(--c-space-3);
  border-top: 1px solid var(--c-border);
  font-size: var(--c-font-xs);
  color: var(--c-text-subtle);
}

#counts-error { display: flex; align-items: center; justify-content: space-between; gap: var(--c-space-2); }
#counts-error.is-hidden { display: none; }

@media (max-width: 1180px) {
  .edit-layout { grid-template-columns: minmax(0, 1fr); }
  .edit-side { position: static; }
}

/* ---- composer + suppressions (gate 4) ---- */

/* Merge palette: a row of insert buttons, not a dropdown -- the operator uses them repeatedly. */
.palette { display: flex; flex-wrap: wrap; gap: var(--c-space-2); }
.palette-btn { font-family: var(--c-mono); }

.code { font-family: var(--c-mono); font-size: var(--c-font-sm); }

/* Key/value list used by the header inspector and the test-send receipt. */
.kv { margin: 0; }
.kv-row { display: grid; grid-template-columns: minmax(90px, 30%) 1fr; gap: var(--c-space-2); padding: 3px 0; }
.kv-row dt { font-size: var(--c-font-sm); color: var(--c-text-muted); }
.kv-row dd { margin: 0; font-size: var(--c-font-sm); min-width: 0; }
.break { overflow-wrap: anywhere; }

/* Compliance verdict. Green means it would send; red lists the machine reasons verbatim, because
   those exact strings are what lands in campaign_recipients.error. */
.verdict {
  display: flex;
  gap: var(--c-space-3);
  padding: var(--c-space-3);
  margin-bottom: var(--c-space-4);
  border: 1px solid var(--c-ok-border);
  background: var(--c-ok-light);
  border-radius: var(--c-radius-sm);
}

.verdict.is-hidden { display: none; }
.verdict strong { display: block; font-size: var(--c-font-base); color: var(--c-ok); }
.verdict p { margin: var(--c-space-1) 0 0; font-size: var(--c-font-sm); color: var(--c-text-muted); }

.verdict-fail { border-color: var(--c-danger-border); background: var(--c-danger-light); }
.verdict-fail strong { color: var(--c-danger); }

.verdict-dot { width: 8px; height: 8px; margin-top: 6px; border-radius: 50%; flex: 0 0 8px; }
.verdict-dot-pass { background: var(--c-ok); }
.verdict-dot-fail { background: var(--c-danger); }

.verdict-list { margin: var(--c-space-1) 0 0; padding-left: var(--c-space-4); font-size: var(--c-font-sm); }
.verdict-list li { color: var(--c-danger); }

.audience-note {
  padding: var(--c-space-2) var(--c-space-3);
  margin-bottom: var(--c-space-4);
  background: var(--c-surface-alt);
  border-radius: var(--c-radius-sm);
  font-size: var(--c-font-sm);
  color: var(--c-text-muted);
}

/* Preview tabs: the same message seen three ways, because all three have to be right. */
.preview-tabs { display: flex; gap: var(--c-space-1); border-bottom: 1px solid var(--c-border); margin-bottom: var(--c-space-3); }

.preview-tab {
  padding: var(--c-space-2) var(--c-space-3);
  font-family: inherit;
  font-size: var(--c-font-sm);
  font-weight: 500;
  color: var(--c-text-muted);
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  cursor: pointer;
  transition: color var(--c-transition), border-color var(--c-transition);
}

.preview-tab:hover { color: var(--c-text); background: var(--c-surface-alt); }
.preview-tab:active { background: var(--c-border); }
.preview-tab.is-active { color: var(--c-accent); border-bottom-color: var(--c-accent); }

.preview-pane.is-hidden { display: none; }

#preview-frame {
  width: 100%;
  height: 340px;
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius-sm);
  background: var(--c-white);
}

.preview-text {
  margin: 0;
  max-height: 340px;
  overflow: auto;
  padding: var(--c-space-3);
  background: var(--c-surface-alt);
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius-sm);
  font-family: var(--c-mono);
  font-size: var(--c-font-sm);
  color: var(--c-text-muted);
  white-space: pre-wrap;
}

/* Stale numbers are dimmed, never blanked -- blanking loses the previous answer mid-request. */
#preview-panel.is-busy .preview-pane,
#preview-panel.is-busy .verdict,
#preview-panel.is-busy .audience-note { opacity: 0.45; }

#preview-error { display: flex; align-items: center; justify-content: space-between; gap: var(--c-space-2); }
#preview-error.is-hidden { display: none; }

/* One-line forms (suppression add + search). Fields keep labels above inputs. */
.inline-form { display: flex; flex-wrap: wrap; gap: var(--c-space-3); align-items: flex-end; }
.inline-form .field { margin-bottom: 0; }
.inline-form .field-grow { flex: 1 1 220px; min-width: 0; }

/* ---- public pages (unsubscribe) ---- */

.public-card { max-width: 420px; text-align: left; }
.public-lead { margin: 0 0 var(--c-space-4); font-size: var(--c-font-base); color: var(--c-text); }
.public-note { margin: var(--c-space-4) 0 0; font-size: var(--c-font-sm); color: var(--c-text-subtle); }

@media (max-width: 1180px) {
  #preview-frame { height: 260px; }
}

/* ---- SMS composer ---- */

/*
  The TCPA banner. Louder than .banner-error because it is not reporting a problem with the page --
  it is telling the operator that the thing they are about to do carries a per-message statutory
  penalty. .banner-locked carries NO dismiss control and nothing in the stylesheet or the script can
  hide it; that is the point, and it is why the rule is written here rather than as a variant that a
  future edit could add a close button to.
*/
.banner-danger {
  background: var(--c-danger-light);
  border: 1px solid var(--c-danger-border);
  border-left: 4px solid var(--c-danger);
  color: var(--c-danger);
}

.banner-danger strong { display: block; margin-bottom: var(--c-space-1); }
.banner-danger p { margin: 0; font-size: var(--c-font-sm); line-height: 1.5; }

.banner-locked { position: relative; }

/* Segment meter: encoding, count against the cap, and what the run will bill. */
.meter {
  padding: var(--c-space-3);
  margin-bottom: var(--c-space-4);
  background: var(--c-surface-alt);
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius-sm);
}

.meter-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--c-space-3); }
.meter-stat { font-size: var(--c-font-sm); color: var(--c-text-muted); font-variant-numeric: tabular-nums; }
.meter-stat strong { font-size: var(--c-font-base); color: var(--c-text); }
.meter-stat .over-cap { color: var(--c-danger); }

.meter-cost {
  margin: var(--c-space-2) 0 0;
  font-size: var(--c-font-sm);
  color: var(--c-text-muted);
  font-variant-numeric: tabular-nums;
}

.meter-warn { margin: var(--c-space-3) 0 var(--c-space-1); font-size: var(--c-font-sm); color: var(--c-warn); }

.offender-list { margin: 0 0 var(--c-space-2); padding-left: var(--c-space-4); font-size: var(--c-font-sm); }
.offender-list li { color: var(--c-text-muted); }

/*
  The rendered text as a handset shows it. The appended STOP line is <mark>ed so the operator can
  see at a glance that it was added for them -- and that it is inside the character count.
*/
.sms-bubble {
  padding: var(--c-space-3);
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius);
  font-size: var(--c-font-base);
  line-height: 1.45;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.sms-suffix { background: var(--c-warn-light); color: var(--c-warn); border-radius: var(--c-radius-sm); }

/* ---------------------------------------------------------------------------
   Launch checklist and campaign detail (gate 6)
   --------------------------------------------------------------------------- */

/*
  A checklist row the app verified for itself: a badge and a statement, never a checkbox.
  Ticking a box next to a fact the app already knows only teaches operators to tick without reading.
*/
.checklist-row { display: flex; align-items: flex-start; gap: var(--c-space-2); }
.check-static { display: block; font-size: var(--c-font-base); color: var(--c-text); }

.progress-bar {
  height: 8px;
  margin-bottom: var(--c-space-4);
  background: var(--c-surface-alt);
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius-pill);
  overflow: hidden;
}

.progress-bar span {
  display: block;
  height: 100%;
  background: var(--c-accent);
  transition: width var(--c-transition);
}

.pager { display: flex; align-items: center; gap: var(--c-space-3); }

/* A link cannot be :disabled, so a paging link at the end of the range says so instead. */
.btn.is-disabled { opacity: 0.5; pointer-events: none; }

/* ---------------------------------------------------------------------------
   Analytics, dashboard and settings (gate 7)
   --------------------------------------------------------------------------- */

/*
  Result tiles. Three lines each -- what it is, how many, and at what rate -- because a count with
  no denominator and a rate with no count are each half an answer.
*/
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: var(--c-space-3);
}

.stat {
  padding: var(--c-space-3);
  background: var(--c-surface-alt);
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius-sm);
}

.stat-label {
  display: block;
  font-size: var(--c-font-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--c-text-muted);
}

.stat-value {
  display: block;
  margin-top: var(--c-space-1);
  font-size: var(--c-font-xl);
  font-weight: 600;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}

.stat-rate { display: block; font-size: var(--c-font-xs); color: var(--c-text-subtle); }

/*
  The "?" beside a metric that is deliberately not a number. Styled as a control rather than as
  decoration so it is obvious there is something to read, and given a real cursor so hovering it
  feels intentional.
*/
abbr[title] {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  margin-left: 2px;
  border: 1px solid var(--c-border-strong);
  border-radius: 50%;
  font-size: 9px;
  font-weight: 700;
  color: var(--c-text-subtle);
  text-decoration: none;
  cursor: help;
}

abbr[title]:hover { border-color: var(--c-text-subtle); color: var(--c-text-muted); }

/* Unsubscribe-rate band. Green under 0.5%, amber to 1%, red above -- the plan's thresholds. */
.rate-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  margin-right: var(--c-space-2);
  border-radius: 50%;
  vertical-align: middle;
}

.rate-ok { background: var(--c-ok); }
.rate-warn { background: var(--c-warn); }
.rate-bad { background: var(--c-danger); }

/* A live campaign on the dashboard: name, state, bar, counts. */
.mini-campaign { padding-bottom: var(--c-space-4); }
.mini-campaign + .mini-campaign { padding-top: var(--c-space-4); border-top: 1px solid var(--c-border); }

.mini-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--c-space-2);
  margin-bottom: var(--c-space-2);
  font-weight: 500;
}

.mini-campaign .progress-bar { margin-bottom: var(--c-space-1); }
.mini-foot { font-size: var(--c-font-sm); color: var(--c-text-subtle); }


/* ---------------------------------------------------------------------------
   Contacts list
   --------------------------------------------------------------------------- */

/* The quick filters take their own row inside the search form: four toggles side by side read
   faster than four stacked fields, and they have to submit with the search box. */
.filter-checks { display: flex; flex-wrap: wrap; gap: var(--c-space-1); flex: 1 1 100%; }

/* An inactive license is not an error -- the dealer may simply have stopped trading -- so it is
   amber rather than the red this app reserves for something being wrong. */
.badge-warn { background: var(--c-warn-light); border-color: var(--c-warn-border); color: var(--c-warn); }

/* Reachability in one column: reachable, nothing on file, or on file but blocked. Blocked is the
   loudest of the three on purpose; it is the state that must never be misread as reachable. */
.reach { white-space: nowrap; }
.reach-icon { margin-right: var(--c-space-1); font-size: var(--c-font-lg); line-height: 1; cursor: help; }
.reach-on { color: var(--c-ok); }
.reach-off { color: var(--c-border-strong); }
.reach-blocked { color: var(--c-danger); }

/* An expanded row and its detail read as one block. Scoped to .contact-detail: the import
   history's own .detail-row is a bare <pre> that already styles itself, and must not inherit this. */
.table tbody tr.is-open > td { background: var(--c-accent-light); border-bottom-color: var(--c-accent-light); }

.detail-row.contact-detail > td {
  background: var(--c-surface-alt);
  padding-top: var(--c-space-4);
  padding-bottom: var(--c-space-4);
}

.detail-row.contact-detail .table {
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius-sm);
}

.detail-head { display: flex; align-items: baseline; gap: var(--c-space-2); margin-top: var(--c-space-6); }
.detail-empty { margin: var(--c-space-2) 0 0; font-size: var(--c-font-sm); color: var(--c-text-subtle); }

/* ---------------------------------------------------------------------------
   Reply inbox, AI composer panel and contact cleanup
   --------------------------------------------------------------------------- */

/* Coloured chips. The neutral .chip already exists and stays the default; these are modifiers so a
   triage label reads at a glance without inventing a second chip component. */
.chip-ok { background: var(--c-ok-light); border-color: var(--c-ok-border); color: var(--c-ok); }
.chip-info { background: var(--c-accent-light); border-color: var(--c-accent-border); color: var(--c-accent-active); }
.chip-warn { background: var(--c-warn-light); border-color: var(--c-warn-border); color: var(--c-warn); }
.chip-muted { background: var(--c-surface-alt); border-color: var(--c-border); color: var(--c-text-subtle); }

/* Who owns a thread. Deliberately the quietest chip on the row -- an outline rather than a fill --
   because it sits beside the triage label, which is the one an operator is scanning FOR. A second
   filled chip there would compete with it and neither would read at a glance. */
.chip-rep { background: transparent; border-color: var(--c-border-strong); color: var(--c-text-muted); }

/* The nav badge. Absent at zero (the script hides it) rather than rendering "0", which teaches an
   operator to stop reading it. */
.nav-item { position: relative; }

.nav-badge {
  float: right;
  min-width: 18px;
  padding: 0 5px;
  border-radius: var(--c-radius-pill);
  background: var(--c-accent);
  color: var(--c-on-accent);
  font-size: var(--c-font-xs);
  font-weight: 600;
  line-height: 18px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.nav-item.is-active .nav-badge { background: var(--c-accent-active); }

/* ---- conversation list ---- */

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

.thread-item + .thread-item { border-top: 1px solid var(--c-border); }

.thread-link {
  display: flex;
  align-items: flex-start;
  gap: var(--c-space-3);
  padding: var(--c-space-3) var(--c-space-4);
  color: var(--c-text);
  transition: background var(--c-transition);
}

.thread-link:hover { background: var(--c-surface-alt); text-decoration: none; }
.thread-link:active { background: var(--c-border); }
.thread-link:focus-visible { outline: 2px solid var(--c-accent); outline-offset: -2px; }

.thread-icon { flex: 0 0 auto; font-size: var(--c-font-lg); line-height: 1.4; color: var(--c-text-subtle); }
.thread-icon-sms { color: var(--c-accent); }

.thread-main { flex: 1; min-width: 0; }

.thread-top { display: flex; flex-wrap: wrap; align-items: center; gap: var(--c-space-2); }
.thread-who { font-weight: 500; }

.thread-preview {
  display: block;
  margin-top: 2px;
  font-size: var(--c-font-sm);
  color: var(--c-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.thread-direction { color: var(--c-text-subtle); }

.thread-when {
  flex: 0 0 auto;
  font-size: var(--c-font-sm);
  color: var(--c-text-subtle);
  white-space: nowrap;
}

/* Unread is carried by weight AND by a rule down the left edge: weight alone is invisible to
   anyone who cannot compare two rows side by side. */
.thread-item.is-unread .thread-link { border-left: 2px solid var(--c-accent); }
.thread-item.is-unread .thread-who { font-weight: 600; }
.thread-item.is-unread .thread-preview { color: var(--c-text); }

.thread-list-compact .thread-link { padding-left: 0; padding-right: 0; }
.thread-list-compact .thread-item.is-unread .thread-link { border-left: none; }

/* ---- one thread ---- */

.msg-list { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: var(--c-space-4); }

.msg { display: flex; flex-direction: column; max-width: 80%; }
.msg-in { align-self: flex-start; align-items: flex-start; }
.msg-out { align-self: flex-end; align-items: flex-end; }

.msg-bubble {
  padding: var(--c-space-3);
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius);
  font-size: var(--c-font-base);
  line-height: 1.45;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.msg-in .msg-bubble { background: var(--c-surface-alt); }
.msg-out .msg-bubble { background: var(--c-accent-light); border-color: var(--c-accent-border); }

.msg-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--c-space-2);
  margin-top: var(--c-space-1);
  font-size: var(--c-font-xs);
  color: var(--c-text-subtle);
}

.triage-head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--c-space-2); margin-bottom: var(--c-space-2); }
.triage-summary { margin: 0 0 var(--c-space-3); font-size: var(--c-font-base); color: var(--c-text); }

#suggested-reply { max-height: 200px; margin-bottom: var(--c-space-2); }

/* ---- AI composer panel ---- */

/* Bounded on purpose: everything inside this border is a request to a model, and everything outside
   it is the campaign. The tint is what makes that boundary visible without a second heading. */
.ai-panel { border-color: var(--c-border-strong); background: var(--c-surface); }
.ai-panel .panel-head { background: var(--c-surface-alt); border-top-left-radius: var(--c-radius); border-top-right-radius: var(--c-radius); }

.ai-fieldset { margin: 0; padding: 0; border: none; }
.ai-fieldset:disabled { opacity: 0.55; }
.ai-fieldset .form-actions { margin-bottom: 0; }

.ai-notes { margin: 0 0 var(--c-space-3); font-size: var(--c-font-sm); color: var(--c-text-muted); }

/* ---- contact cleanup ---- */

/* Faceted filters. Chips rather than dropdowns because the COUNT is the thing being read: an
   operator deciding what to do about 5,528 dead findings has to see 5,528 before they choose. */
.filter-facet { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--c-space-3); }
.filter-facet + .filter-facet { margin-top: var(--c-space-2); }

.facet-label {
  flex: 0 0 52px;
  font-size: var(--c-font-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--c-text-muted);
}

.facet {
  display: inline-flex;
  align-items: center;
  gap: var(--c-space-2);
  padding: 2px var(--c-space-2);
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius-sm);
  background: var(--c-surface);
  font-size: var(--c-font-sm);
  color: var(--c-text-muted);
  transition: background var(--c-transition), border-color var(--c-transition), color var(--c-transition);
}

.facet:hover { background: var(--c-surface-alt); border-color: var(--c-border-strong); color: var(--c-text); text-decoration: none; }
.facet:active { background: var(--c-border); }

.facet.is-active {
  background: var(--c-accent-light);
  border-color: var(--c-accent);
  color: var(--c-accent-active);
  font-weight: 500;
}

.facet-count { font-variant-numeric: tabular-nums; color: var(--c-text-subtle); }
.facet.is-active .facet-count { color: var(--c-accent-active); }

.bulk-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--c-space-3);
  border-bottom: 1px solid var(--c-border);
}

/* The filter-wide apply sits above the per-row one and is tinted, because the two mean different
   things: this one acts on everything the filter selects, including the 5,478 rows not on screen. */
.bulk-bar-all { background: var(--c-warn-light); border-bottom-color: var(--c-warn-border); }

.confirm-list { margin: var(--c-space-2) 0; padding-left: var(--c-space-4); font-size: var(--c-font-sm); }
.confirm-list li { color: var(--c-text-muted); }

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

.finding { padding: var(--c-space-4); }
.finding + .finding { border-top: 1px solid var(--c-border); }

/* A decided finding recedes. It is still readable -- the audit trail is the point -- but it stops
   competing with the ones that still need someone. */
.finding-applied, .finding-dismissed { background: var(--c-surface-alt); }
.finding-applied .finding-cards, .finding-dismissed .finding-cards { opacity: 0.7; }

.finding-head { display: flex; align-items: flex-start; gap: var(--c-space-3); }
.finding-check { flex: 0 0 20px; margin: 2px 0 0; }
.finding-headings { flex: 1; min-width: 0; }

.finding-title { display: flex; flex-wrap: wrap; align-items: center; gap: var(--c-space-2); }
.finding-reason { margin: var(--c-space-1) 0 0; font-size: var(--c-font-sm); color: var(--c-text-muted); }

.finding-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: var(--c-space-3);
  margin-top: var(--c-space-3);
}

.finding-card {
  padding: var(--c-space-3);
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius-sm);
}

/* The two that carry the decision: what survives, and what goes. Border-left rather than a fill, so
   the cards stay comparable at a glance -- comparing them is the whole job. */
.finding-card.is-keeper { border-left: 3px solid var(--c-ok); background: var(--c-ok-light); }
.finding-card.is-target { border-left: 3px solid var(--c-warn); }

.finding-card-head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--c-space-2); margin-bottom: var(--c-space-2); }

/* An archived contact's row, on the contact list. Muted, never hidden. */
.table tbody tr.is-archived td { color: var(--c-text-subtle); }
.table tbody tr.is-archived td:first-child { color: var(--c-text-muted); }

/* Label text for a control that carries its meaning in its position rather than in words. */
.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;
}

@media (max-width: 720px) {
  .msg { max-width: 100%; }
}

/* ---------------------------------------------------------------------------
   Demo funnel and conversation pipeline
   --------------------------------------------------------------------------- */

/*
  The honeypot on the public demo form. Positioned off-screen rather than display:none, because some
  form-filling bots skip hidden inputs and happily fill one that is merely out of view -- which is
  the whole point of the field. aria-hidden and tabindex="-1" in the markup keep it away from
  keyboard and screen-reader users; this only keeps it away from sighted ones.
*/
.hp-field {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/*
  A conversation row that carries a stage control. A MODIFIER rather than a change to .thread-item,
  because the dashboard's compact list uses the same rows without one and must not gain a flex
  context it never asked for.
*/
.thread-item-staged {
  display: flex;
  align-items: center;
  gap: var(--c-space-2);
  padding-right: var(--c-space-4);
}

.thread-item-staged .thread-link { flex: 1 1 auto; min-width: 0; }

.thread-stage { flex: 0 0 auto; display: flex; align-items: center; gap: var(--c-space-2); }

/* Matches .btn-sm, so a select and a button on the same row line up. */
.input-sm {
  height: 28px;
  padding: 0 var(--c-space-2);
  font-size: var(--c-font-sm);
}

/* The due-follow-ups pill is a work queue, not a stage, so it reads as one. */
.facet-due.is-active {
  background: var(--c-warn-light);
  border-color: var(--c-warn-border);
  color: var(--c-warn);
}

.facet-due.is-active .facet-count { color: var(--c-warn); }

/* A handled demo request recedes -- still readable, no longer competing with the ones that are not. */
.table tbody tr.is-handled td { color: var(--c-text-subtle); }

@media (max-width: 720px) {
  /* The stage control drops under the row rather than squeezing the preview to nothing. */
  .thread-item-staged { flex-wrap: wrap; }
  .thread-item-staged .thread-link { flex: 1 1 100%; }
  .thread-stage { padding: 0 0 var(--c-space-3) var(--c-space-4); }
}

/* ---------------------------------------------------------------------------
   Status badges, funnel, metric rows and the event timeline
   --------------------------------------------------------------------------- */

/* The blue badge. Completes the set beside pass/fail/warn/none for a state that is
   neither good news nor bad news -- it is simply in progress. */
.badge-info { background: var(--c-accent-light); border-color: var(--c-accent-border); color: var(--c-accent-active); }

/*
  Campaign status, coloured by what the status MEANS rather than by a class the view
  picks. The vocabulary is closed (SendPump's CampaignStatus) and the mapping is
  presentation, so it lives here: the view writes the status it already has into a data
  attribute and gets the right colour. An unknown status falls through to neutral, which
  is the honest default for a state this stylesheet has not been taught.
*/
.badge-status { background: var(--c-surface-alt); border-color: var(--c-border); color: var(--c-text-muted); }
.badge-status[data-status="scheduled"],
.badge-status[data-status="sending"] { background: var(--c-accent-light); border-color: var(--c-accent-border); color: var(--c-accent-active); }
.badge-status[data-status="paused"] { background: var(--c-warn-light); border-color: var(--c-warn-border); color: var(--c-warn); }
.badge-status[data-status="sent"] { background: var(--c-ok-light); border-color: var(--c-ok-border); color: var(--c-ok); }
.badge-status[data-status="killed"] { background: var(--c-danger-light); border-color: var(--c-danger-border); color: var(--c-danger); }

/* A progress bar inside a table cell: thin enough to sit under a number without becoming
   the loudest thing in the row. */
.progress-slim {
  display: block;
  height: 4px;
  min-width: 64px;
  margin: var(--c-space-1) 0 0;
  border: none;
  background: var(--c-border);
}

/*
  The funnel. Each bar is drawn as a share of the FIRST step, so the shape on screen is the
  shape of the drop-off; the rate under each bar is that step's share of the one BEFORE it,
  which is the question actually being asked at each arrow. Two different denominators, and
  conflating them is how a funnel starts lying.
*/
.funnel { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--c-space-3); }

.funnel-step {
  display: grid;
  grid-template-columns: minmax(88px, 132px) minmax(0, 1fr) auto;
  align-items: center;
  column-gap: var(--c-space-3);
  row-gap: var(--c-space-1);
}

.funnel-label {
  font-size: var(--c-font-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--c-text-muted);
}

.funnel-track {
  height: 10px;
  background: var(--c-surface-alt);
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius-sm);
  overflow: hidden;
}

.funnel-fill {
  display: block;
  height: 100%;
  background: var(--c-accent);
  transition: width var(--c-transition);
}

.funnel-value {
  font-size: var(--c-font-lg);
  font-weight: 600;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
}

/* The last step is a LEAD, not a behaviour -- somebody typed their own name in. It is the
   one step on the chart that is an outcome, so it is the one step that is not blue.
   Only the FILL is green: a green zero would read as good news about no leads at all,
   whereas a zero-width bar simply shows nothing, which is the truth. */
.funnel-step-goal .funnel-fill { background: var(--c-ok); }

.funnel-note {
  grid-column: 2 / -1;
  margin: 0;
  font-size: var(--c-font-sm);
  color: var(--c-text-subtle);
}

/*
  Aligned metric rows. One grid for the whole block with the rows contributing cells
  (display: contents), so every label, every number and every rate line up down the panel
  -- which a grid of independent tiles cannot do once the labels differ in length.
*/
.metric-list {
  display: grid;
  grid-template-columns: minmax(110px, max-content) minmax(56px, max-content) minmax(0, 1fr);
  align-items: baseline;
  column-gap: var(--c-space-4);
}

.metric { display: contents; }

.metric > * { padding: var(--c-space-2) 0; border-bottom: 1px solid var(--c-border); }
.metric:last-child > * { border-bottom: none; }

.metric-label { font-size: var(--c-font-md); color: var(--c-text-muted); }
.metric-value { font-size: var(--c-font-lg); font-weight: 600; text-align: right; font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
.metric-note { font-size: var(--c-font-sm); color: var(--c-text-subtle); }

/*
  The provider conversation. A list rather than a table because nobody reads down these
  columns -- they read the newest few and stop -- and a rail of dots makes "newest first"
  legible at a glance. The dot takes its colour from the event name itself, so a new event
  type the provider starts sending arrives grey instead of arriving miscoloured.
*/
.timeline { list-style: none; margin: 0; padding: 0; }

.timeline-item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: baseline;
  column-gap: var(--c-space-3);
  padding: var(--c-space-2) var(--c-space-4);
  border-bottom: 1px solid var(--c-border);
}

.timeline-item:last-child { border-bottom: none; }
.timeline-item:hover { background: var(--c-surface-alt); }

.timeline-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--c-border-strong);
  transform: translateY(-1px);
}

.timeline-item[data-event*="deliver"] .timeline-dot { background: var(--c-ok); }
.timeline-item[data-event*="open"] .timeline-dot,
.timeline-item[data-event*="click"] .timeline-dot { background: var(--c-accent); }
.timeline-item[data-event*="unsub"] .timeline-dot,
.timeline-item[data-event*="defer"] .timeline-dot { background: var(--c-warn); }
.timeline-item[data-event*="bounce"] .timeline-dot,
.timeline-item[data-event*="drop"] .timeline-dot,
.timeline-item[data-event*="fail"] .timeline-dot,
.timeline-item[data-event*="spam"] .timeline-dot { background: var(--c-danger); }

.timeline-main { min-width: 0; }
.timeline-what { font-size: var(--c-font-md); font-weight: 500; }
.timeline-who { display: block; font-family: var(--c-mono); font-size: var(--c-font-sm); color: var(--c-text-muted); overflow-wrap: anywhere; }
.timeline-note { display: block; font-size: var(--c-font-xs); color: var(--c-danger); }
.timeline-when { font-size: var(--c-font-sm); color: var(--c-text-subtle); white-space: nowrap; font-variant-numeric: tabular-nums; }
.timeline-meta { display: block; font-size: var(--c-font-xs); color: var(--c-text-subtle); text-align: right; }

/* An unsigned event is not an error, but it is the one thing in this stream worth noticing
   without looking for it: it means the row arrived without proof of who sent it. */
.timeline-unsigned { color: var(--c-warn); font-weight: 600; }

@media (max-width: 720px) {
  .funnel-step { grid-template-columns: minmax(0, 1fr) auto; }
  .funnel-label { grid-column: 1; }
  .funnel-value { grid-column: 2; }
  .funnel-track { grid-column: 1 / -1; }
  .funnel-note { grid-column: 1 / -1; }

  /* Two columns, and the rate drops to its own line under them. The row rule moves with it,
     so one metric is still one bordered block. Specificity has to beat ".metric > *". */
  .metric-list { grid-template-columns: minmax(0, 1fr) auto; }
  .metric > .metric-note { grid-column: 1 / -1; padding-top: 0; }
  .metric > .metric-label, .metric > .metric-value { border-bottom: none; }
}

/* ---------------------------------------------------------------------------
   Inbox: the conversation list, and one conversation

   These two pages were held back while their features were still moving, so this
   is where they join the rest of the app. Presentation only -- every value below
   is a token that already existed, and no rule here changes what a control does.
   --------------------------------------------------------------------------- */

/* ---- the filter bar ---- */

/*
  Stage pills, the rep picker and the unread toggle are ONE bar rather than a form stacked over a
  row of links. They answer the same question -- which conversations am I looking at -- and an
  operator changing one of them is usually about to change another.
*/
.inbox-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--c-space-3);
  padding-top: var(--c-space-3);
  padding-bottom: var(--c-space-3);
  border-bottom: 1px solid var(--c-border);
}

/* The pills take the room and the controls keep their natural width, so the bar breaks by
   dropping the controls to a second line rather than by squeezing the counts. */
.inbox-toolbar .filter-facet { flex: 1 1 340px; }
.inbox-toolbar .inline-form { flex: 0 0 auto; align-items: center; gap: var(--c-space-2); }

/* :last-of-type as well as the plain selector, because .panel-body .field:last-of-type puts the
   bottom margin BACK for the stacked forms in a panel -- and this bar is not one of those. Same
   specificity, declared later, so the bar wins where it applies and nothing else changes. */
.inbox-toolbar .field, .inbox-toolbar .field:last-of-type { margin-bottom: 0; }

/*
  Label BESIDE its select, not above it. Every FORM in this app still puts the label over the input
  and that rule is not weakening -- but this is a filter bar, and the "Stage" label two controls to
  the left is already inline. One bar reads as one thing or it reads as nothing.
*/
.inbox-toolbar .field-inline { display: flex; align-items: center; gap: var(--c-space-2); }

.inbox-toolbar .field-inline label {
  margin-bottom: 0;
  font-size: var(--c-font-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--c-text-muted);
  white-space: nowrap;
}

/*
  28px down the whole bar, so the pills, the toggle, the select and the buttons sit ON one line
  rather than near it. The unread toggle needs its own margin reset: it is a <label> inside a
  .field, so ".field label" hands it the 4px gap a stacked form wants under its caption.
*/
.inbox-toolbar .check-tile { height: 28px; margin-bottom: 0; }
.inbox-toolbar .check-tile input { margin-top: 0; }
.inbox-toolbar .facet { padding-top: var(--c-space-1); padding-bottom: var(--c-space-1); }

/* The list ends where the panel ends; the pager after it needs its own rule, because the last row
   deliberately has none. */
.inbox-pager { border-top: 1px solid var(--c-border); }

/* ---- the conversation list ---- */

/*
  Row rules hang off a MODIFIER: the dashboard renders these same rows in a narrow card and must
  not inherit the icon tile, the row rhythm or the quieter timestamp. Only the icon DRAWING below
  is unscoped, because it describes an svg that exists in one place.

  Drawn rather than typed: a glyph renders at whatever weight the system font picks -- the same
  argument the empty-state icons already make -- and the tile around it is what gives the row a
  fixed left edge whichever channel the reply came in on.
*/
.thread-icon svg {
  display: block;
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.thread-list-inbox .thread-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  margin-top: 1px;
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius-sm);
  background: var(--c-surface-alt);
}

.thread-list-inbox .thread-icon-email { color: var(--c-text-muted); }
.thread-list-inbox .thread-icon-sms { border-color: var(--c-accent-border); background: var(--c-accent-light); }

/* Unread, said a third way. The left rule and the weight are already there; this is the one an
   operator picks out scanning a column of names, where there is no second row to compare against. */
.thread-unread {
  flex: 0 0 auto;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--c-accent);
}

/* Quieter than the preview it sits beside: the age of a thread is read after the name and the
   text, never before them. Tabular so a column of them does not jitter. */
.thread-list-inbox .thread-when {
  font-size: var(--c-font-xs);
  font-variant-numeric: tabular-nums;
  padding-top: 1px;
}

/*
  The row's stage control reads as grey chrome until it is reached for. It is the one control on a
  row whose whole job is to be a link, and a bordered box in accent colour there would compete with
  the name for the click.
*/
.thread-list-inbox .thread-stage .input { border-color: var(--c-border); color: var(--c-text-muted); }
.thread-list-inbox .thread-stage .input:hover { border-color: var(--c-border-strong); color: var(--c-text); }

/* The focus border is RESTATED, not inherited: the quiet default above is a three-class selector
   and out-specifies ".input:focus", so leaving it out would drop the accent ring on the one row
   control an operator can reach by keyboard. */
.thread-list-inbox .thread-stage .input:focus { border-color: var(--c-accent); color: var(--c-text); }

/* ---- one conversation ---- */

/* One square corner on the side a message came from. The cheapest way to say who is speaking
   without a tail, an avatar or a second colour. */
.msg-in .msg-bubble { border-bottom-left-radius: var(--c-radius-sm); }
.msg-out .msg-bubble { border-bottom-right-radius: var(--c-radius-sm); }

.msg-meta { font-variant-numeric: tabular-nums; }
.msg-who { font-weight: 600; color: var(--c-text-muted); }

/*
  Stage and owner are one decision -- who is working this and how far along is it -- so they are one
  row. auto-fit rather than a fixed pair: the rail is 380px until the layout collapses, and one
  column is the right answer after it does.

  No rule under it: the "Follow up" label below already carries one, and two hairlines a dozen
  pixels apart is noise where one of them is a divider.
*/
.pipeline-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--c-space-3);
  margin-bottom: var(--c-space-4);
}

.pipeline-row form { min-width: 0; }

/* :last-of-type for the same reason the filter bar states it: each of these fields is the last
   one in its own form, and .panel-body .field:last-of-type would otherwise hang a stacked-form
   margin under a row that is not stacked. */
.pipeline-row .field, .pipeline-row .field:last-of-type { margin-bottom: 0; }

/* ---- the drafted reply ---- */

/*
  The same family as .ai-panel -- everything inside the border was written by a model -- but tinted
  and rule-marked rather than merely bounded. This one sits in a rail of panels that all look alike,
  and the operator has to see WITHOUT looking which of that text is not theirs.
*/
.ai-panel-draft {
  border-color: var(--c-accent-border);
  border-left: 3px solid var(--c-accent);
  background: var(--c-accent-light);
}

.ai-panel-draft .panel-head { border-bottom-color: var(--c-accent-border); }

/* The draft goes back onto white inside the tint: it is a quotation of what would be sent, and it
   has to be exactly as readable as the reply box it is about to fill. */
.ai-panel-draft .preview-text {
  background: var(--c-surface);
  border-color: var(--c-accent-border);
  color: var(--c-text);
}

/* Says who wrote it, where the eye lands first. */
.ai-tag {
  display: inline-flex;
  align-items: center;
  padding: 1px var(--c-space-2);
  border: 1px solid var(--c-accent-border);
  border-radius: var(--c-radius-pill);
  background: var(--c-surface);
  font-size: var(--c-font-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--c-accent-active);
  white-space: nowrap;
}

/* ---- the reply box ---- */

/*
  One grid for the whole reply block, so the box, the meter and the send row are rows of a single
  column rather than three elements whose spacing comes out of three different margins.
*/
#reply-form { display: grid; gap: var(--c-space-3); }
#reply-form > .field, #reply-form > .meter, #reply-form > .banner { margin-bottom: 0; }

/* The send row is separated by a rule rather than by a gap the eye has to measure, and it is the
   last thing in the panel, so it carries no trailing margin. */
.reply-actions {
  align-items: center;
  margin-bottom: 0;
  padding-top: var(--c-space-3);
  border-top: 1px solid var(--c-border);
}

@media (max-width: 720px) {
  /* The controls stop trying to share a line with the pills and take their own. */
  .inbox-toolbar .inline-form { flex: 1 1 100%; }
}

/* Utilities last: a single-class hide has to beat the single-class rule it is hiding. */
.is-hidden { display: none; }
