/* ══════════════════════════════════════════
   Attune Design System Tokens
   Do not edit values here.
   ══════════════════════════════════════════ */

:root {
  /* ─── Font families ─── */
  --font-sans: 'General Sans Variable', 'General Sans', -apple-system, BlinkMacSystemFont, sans-serif;
  --font-mono: 'SF Mono', 'IBM Plex Mono', 'Menlo', monospace;

  /* ─── Palette: Purple ─── */
  --palette-purple-50:  #f5f3ff;
  --palette-purple-100: #ece8fb;
  --palette-purple-200: #c4b5fc;
  --palette-purple-300: #a78bfa;
  --palette-purple-400: #967fee;
  --palette-purple-500: #6d3ce8;
  --palette-purple-600: #4c02e8;
  --palette-purple-700: #3b01b8;
  --palette-purple-800: #2a0182;
  --palette-purple-900: #1a0054;

  /* ─── Palette: Emerald ─── */
  --palette-emerald-50:  #effdf5;
  --palette-emerald-100: #d1fae5;
  --palette-emerald-200: #6ee7b7;
  --palette-emerald-300: #6dd3a8;
  --palette-emerald-400: #34d399;
  --palette-emerald-500: #10b981;
  --palette-emerald-600: #059669;
  --palette-emerald-700: #047857;
  --palette-emerald-800: #065f46;
  --palette-emerald-900: #064e3b;

  /* ─── Palette: Neutrals ─── */
  --palette-neutrals-white: #ffffff;
  --palette-neutrals-50:    #fafafa;
  --palette-neutrals-100:   #f3f4f6;
  --palette-neutrals-200:   #e5e7eb;
  --palette-neutrals-300:   #d1d5db;
  --palette-neutrals-400:   #9da3ae;
  --palette-neutrals-500:   #6c727f;
  --palette-neutrals-700:   #394150;
  --palette-neutrals-800:   #212936;
  --palette-neutrals-900:   #121826;

  /* ─── Semantic: Brand ─── */
  --brand-primary:   #4c02e8;
  --brand-hover:     #3b01b8;
  --brand-pressed:   #2a0182;
  --brand-subtle:    #f5f3ff;
  --brand-secondary: #059669;

  /* ─── Semantic: Text ─── */
  --text-primary:    #121826;
  --text-secondary:  #394150;
  --text-muted:      #9da3ae;
  --text-link:       #4c02e8;
  --text-on-primary: #ffffff;
  --text-on-accent:  #065f46;
  --text-on-brand:   #2a0182;

  /* ─── Semantic: Background ─── */
  --bg-page:     #f8f7f4;
  --bg-surface:  #ffffff;
  --bg-elevated: #fafafa;
  --bg-subtle:   #f5f3ff;
  --bg-tint:     rgba(18, 24, 38, 0.15);

  /* ─── Semantic: Border ─── */
  --border-primary:   #e5e7eb;
  --border-secondary: #9da3ae;
  --border-focus:     #967fee;

  /* ─── Semantic: Status ─── */
  --status-warning-default:    #fcf3cc;
  --status-warning-subtle:     #e9a23b;
  --status-error-default:      #f9e3e2;
  --status-error-subtle:       #dd524c;
  --status-error-hover:        #c24741;
  --status-error-text:         #c12b25;
  --status-info-default:       #dee9fc;
  --status-info-subtle:        #4e80ee;
  --status-success-default:    #effdf5;
  --status-success-subtle:     #059669;
  --status-badge-confirmation: #d1fae5;

  /* ─── Semantic: Journey nodes ─── */
  --node-message:  #4c02e8;
  --node-check-in: #059669;
  --node-branch:   #e9a23b;
  --node-handoff:  #dd524c;

  /* ─── Radii ─── */
  --radius-default: 14px;
  --radius-small:   12px;

  /* ─── Shadows ─── */
  --shadow-container-elevated-bg: 0 3px 6px -2px rgba(0, 0, 0, 0.04), 0 1px 1px 0 rgba(0, 0, 0, 0.07);
  --shadow-elevated-sm:           0 1px 0.5px 0 rgba(0, 0, 0, 0.07), 0 3px 3px 0 rgba(0, 0, 0, 0.04);

  /* ─── Typography: Display ─── */
  --text-display-large-font-family:    var(--font-sans);
  --text-display-large-font-weight:    700;
  --text-display-large-font-size:      48px;
  --text-display-large-line-height:    56px;
  --text-display-large-letter-spacing: -0.01em;

  --text-display-small-font-family:    var(--font-sans);
  --text-display-small-font-weight:    700;
  --text-display-small-font-size:      36px;
  --text-display-small-line-height:    44px;
  --text-display-small-letter-spacing: -0.01em;

  /* ─── Typography: Heading ─── */
  --text-h1-font-family:    var(--font-sans);
  --text-h1-font-weight:    600;
  --text-h1-font-size:      24px;
  --text-h1-line-height:    32px;
  --text-h1-letter-spacing: -0.01em;

  --text-h2-font-family:    var(--font-sans);
  --text-h2-font-weight:    600;
  --text-h2-font-size:      20px;
  --text-h2-line-height:    28px;
  --text-h2-letter-spacing: -0.005em;

  --text-h3-font-family:    var(--font-sans);
  --text-h3-font-weight:    500;
  --text-h3-font-size:      16px;
  --text-h3-line-height:    24px;
  --text-h3-letter-spacing: 0;

  --text-h4-font-family:    var(--font-sans);
  --text-h4-font-weight:    500;
  --text-h4-font-size:      14px;
  --text-h4-line-height:    20px;
  --text-h4-letter-spacing: 0;

  /* ─── Typography: Body ─── */
  --text-body-large-font-family:    var(--font-sans);
  --text-body-large-font-weight:    400;
  --text-body-large-font-size:      15px;
  --text-body-large-line-height:    24px;
  --text-body-large-letter-spacing: 0;

  --text-body-font-family:    var(--font-sans);
  --text-body-font-weight:    400;
  --text-body-font-size:      14px;
  --text-body-line-height:    22px;
  --text-body-letter-spacing: 0;

  --text-body-bold-font-family:    var(--font-sans);
  --text-body-bold-font-weight:    600;
  --text-body-bold-font-size:      14px;
  --text-body-bold-line-height:    22px;
  --text-body-bold-letter-spacing: 0;

  --text-body-small-font-family:    var(--font-sans);
  --text-body-small-font-weight:    400;
  --text-body-small-font-size:      13px;
  --text-body-small-line-height:    20px;
  --text-body-small-letter-spacing: 0;

  /* ─── Typography: Caption ─── */
  --text-caption-font-family:    var(--font-sans);
  --text-caption-font-weight:    400;
  --text-caption-font-size:      12px;
  --text-caption-line-height:    18px;
  --text-caption-letter-spacing: 0.01em;

  --text-caption-bold-font-family:    var(--font-sans);
  --text-caption-bold-font-weight:    600;
  --text-caption-bold-font-size:      12px;
  --text-caption-bold-line-height:    18px;
  --text-caption-bold-letter-spacing: 0;

  /* ─── Typography: Data (mono) ─── */
  --text-data-font-family:    var(--font-mono);
  --text-data-font-weight:    500;
  --text-data-font-size:      13px;
  --text-data-line-height:    18px;
  --text-data-letter-spacing: 0;

  --text-data-small-font-family:    var(--font-mono);
  --text-data-small-font-weight:    500;
  --text-data-small-font-size:      12px;
  --text-data-small-line-height:    16px;
  --text-data-small-letter-spacing: 0;

  /* ─── Typography: UI ─── */
  --text-ui-nav-font-family:    var(--font-sans);
  --text-ui-nav-font-weight:    500;
  --text-ui-nav-font-size:      13px;
  --text-ui-nav-line-height:    18px;
  --text-ui-nav-letter-spacing: 0;

  --text-ui-nav-550-font-family:    var(--font-sans);
  --text-ui-nav-550-font-weight:    550;
  --text-ui-nav-550-font-size:      13px;
  --text-ui-nav-550-line-height:    18px;
  --text-ui-nav-550-letter-spacing: 0;

  --text-ui-label-font-family:    var(--font-sans);
  --text-ui-label-font-weight:    500;
  --text-ui-label-font-size:      12px;
  --text-ui-label-line-height:    16px;
  --text-ui-label-letter-spacing: 0.005em;

  --text-ui-label-small-font-family:    var(--font-sans);
  --text-ui-label-small-font-weight:    500;
  --text-ui-label-small-font-size:      10px;
  --text-ui-label-small-line-height:    12px;
  --text-ui-label-small-letter-spacing: 0;

  --text-ui-badge-font-family:    var(--font-sans);
  --text-ui-badge-font-weight:    500;
  --text-ui-badge-font-size:      11px;
  --text-ui-badge-line-height:    16px;
  --text-ui-badge-letter-spacing: 0.005em;

  --text-ui-badge-small-font-family:    var(--font-sans);
  --text-ui-badge-small-font-weight:    700;
  --text-ui-badge-small-font-size:      9px;
  --text-ui-badge-small-line-height:    14px;
  --text-ui-badge-small-letter-spacing: 0;

  --text-btn-font-family:    var(--font-sans);
  --text-btn-font-weight:    500;
  --text-btn-font-size:      14px;
  --text-btn-line-height:    20px;
  --text-btn-letter-spacing: 0;

  --text-btn-small-font-family:    var(--font-sans);
  --text-btn-small-font-weight:    500;
  --text-btn-small-font-size:      12px;
  --text-btn-small-line-height:    18px;
  --text-btn-small-letter-spacing: 0;

  /* ─── Backwards-compat aliases ───
     Old names used by existing rules below; map to new tokens so existing CSS
     keeps rendering without modification. Remove once callers migrate. */
  --bg:           var(--bg-page);
  --surface:      var(--bg-surface);
  --surface-soft: #FFFBF6;
  --border:       var(--border-primary);
  --border-dark:  var(--border-secondary);
  --text:         var(--text-primary);
  --text-2:       var(--text-secondary);
  --purple:       var(--brand-primary);
  --purple-dim:   var(--palette-purple-50);
  --cyan:         var(--brand-secondary);
  --cyan-dim:     var(--palette-emerald-50);
  --pink:         #E9B1D3;
  --pink-dim:     var(--status-error-default);
  --font:         -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", system-ui, sans-serif;
  --mono:         var(--font-mono);
}

/* ─── Text utility classes ─── */

.text-display-large {
  font-family:    var(--text-display-large-font-family);
  font-weight:    var(--text-display-large-font-weight);
  font-size:      var(--text-display-large-font-size);
  line-height:    var(--text-display-large-line-height);
  letter-spacing: var(--text-display-large-letter-spacing);
}
.text-display-small {
  font-family:    var(--text-display-small-font-family);
  font-weight:    var(--text-display-small-font-weight);
  font-size:      var(--text-display-small-font-size);
  line-height:    var(--text-display-small-line-height);
  letter-spacing: var(--text-display-small-letter-spacing);
}
.text-h1 {
  font-family:    var(--text-h1-font-family);
  font-weight:    var(--text-h1-font-weight);
  font-size:      var(--text-h1-font-size);
  line-height:    var(--text-h1-line-height);
  letter-spacing: var(--text-h1-letter-spacing);
}
.text-h2 {
  font-family:    var(--text-h2-font-family);
  font-weight:    var(--text-h2-font-weight);
  font-size:      var(--text-h2-font-size);
  line-height:    var(--text-h2-line-height);
  letter-spacing: var(--text-h2-letter-spacing);
}
.text-h3 {
  font-family:    var(--text-h3-font-family);
  font-weight:    var(--text-h3-font-weight);
  font-size:      var(--text-h3-font-size);
  line-height:    var(--text-h3-line-height);
  letter-spacing: var(--text-h3-letter-spacing);
}
.text-h4 {
  font-family:    var(--text-h4-font-family);
  font-weight:    var(--text-h4-font-weight);
  font-size:      var(--text-h4-font-size);
  line-height:    var(--text-h4-line-height);
  letter-spacing: var(--text-h4-letter-spacing);
}
.text-body-large {
  font-family:    var(--text-body-large-font-family);
  font-weight:    var(--text-body-large-font-weight);
  font-size:      var(--text-body-large-font-size);
  line-height:    var(--text-body-large-line-height);
  letter-spacing: var(--text-body-large-letter-spacing);
}
.text-body {
  font-family:    var(--text-body-font-family);
  font-weight:    var(--text-body-font-weight);
  font-size:      var(--text-body-font-size);
  line-height:    var(--text-body-line-height);
  letter-spacing: var(--text-body-letter-spacing);
}
.text-body-bold {
  font-family:    var(--text-body-bold-font-family);
  font-weight:    var(--text-body-bold-font-weight);
  font-size:      var(--text-body-bold-font-size);
  line-height:    var(--text-body-bold-line-height);
  letter-spacing: var(--text-body-bold-letter-spacing);
}
.text-body-small {
  font-family:    var(--text-body-small-font-family);
  font-weight:    var(--text-body-small-font-weight);
  font-size:      var(--text-body-small-font-size);
  line-height:    var(--text-body-small-line-height);
  letter-spacing: var(--text-body-small-letter-spacing);
}
.text-caption {
  font-family:    var(--text-caption-font-family);
  font-weight:    var(--text-caption-font-weight);
  font-size:      var(--text-caption-font-size);
  line-height:    var(--text-caption-line-height);
  letter-spacing: var(--text-caption-letter-spacing);
}
.text-caption-bold {
  font-family:    var(--text-caption-bold-font-family);
  font-weight:    var(--text-caption-bold-font-weight);
  font-size:      var(--text-caption-bold-font-size);
  line-height:    var(--text-caption-bold-line-height);
  letter-spacing: var(--text-caption-bold-letter-spacing);
  text-transform: uppercase;
}
.text-data {
  font-family:    var(--text-data-font-family);
  font-weight:    var(--text-data-font-weight);
  font-size:      var(--text-data-font-size);
  line-height:    var(--text-data-line-height);
  letter-spacing: var(--text-data-letter-spacing);
}
.text-data-small {
  font-family:    var(--text-data-small-font-family);
  font-weight:    var(--text-data-small-font-weight);
  font-size:      var(--text-data-small-font-size);
  line-height:    var(--text-data-small-line-height);
  letter-spacing: var(--text-data-small-letter-spacing);
}
.text-ui-nav {
  font-family:    var(--text-ui-nav-font-family);
  font-weight:    var(--text-ui-nav-font-weight);
  font-size:      var(--text-ui-nav-font-size);
  line-height:    var(--text-ui-nav-line-height);
  letter-spacing: var(--text-ui-nav-letter-spacing);
}
.text-ui-nav-550 {
  font-family:    var(--text-ui-nav-550-font-family);
  font-weight:    var(--text-ui-nav-550-font-weight);
  font-size:      var(--text-ui-nav-550-font-size);
  line-height:    var(--text-ui-nav-550-line-height);
  letter-spacing: var(--text-ui-nav-550-letter-spacing);
}
.text-ui-label {
  font-family:    var(--text-ui-label-font-family);
  font-weight:    var(--text-ui-label-font-weight);
  font-size:      var(--text-ui-label-font-size);
  line-height:    var(--text-ui-label-line-height);
  letter-spacing: var(--text-ui-label-letter-spacing);
}
.text-ui-badge {
  font-family:    var(--text-ui-badge-font-family);
  font-weight:    var(--text-ui-badge-font-weight);
  font-size:      var(--text-ui-badge-font-size);
  line-height:    var(--text-ui-badge-line-height);
  letter-spacing: var(--text-ui-badge-letter-spacing);
}
.text-btn {
  font-family:    var(--text-btn-font-family);
  font-weight:    var(--text-btn-font-weight);
  font-size:      var(--text-btn-font-size);
  line-height:    var(--text-btn-line-height);
  letter-spacing: var(--text-btn-letter-spacing);
}
.text-btn-small {
  font-family:    var(--text-btn-small-font-family);
  font-weight:    var(--text-btn-small-font-weight);
  font-size:      var(--text-btn-small-font-size);
  line-height:    var(--text-btn-small-line-height);
  letter-spacing: var(--text-btn-small-letter-spacing);
}

* { box-sizing: border-box; margin: 0; padding: 0; }

body { font-family: var(--font); background: var(--bg); color: var(--text); min-height: 100vh; }

/* ══════════════════════════════════════════
   Product Show Page
   ══════════════════════════════════════════ */

/* Nav */
.pd-nav {
  display: flex; align-items: center; justify-content: space-between;
  padding: 0 28px; height: 56px;
  background: var(--surface); border-bottom: 1.5px solid var(--border);
  position: sticky; top: 0; z-index: 100;
  gap: 16px;
}
.pd-nav-left { display: flex; align-items: center; gap: 8px; min-width: 0; }
.pd-back { font-size: 0.82rem; color: var(--text-muted); text-decoration: none; flex-shrink: 0; }
.pd-back:hover { color: var(--text); }
.pd-sep { color: var(--border-dark); flex-shrink: 0; }
.pd-title { font-size: 0.9rem; font-weight: 600; color: var(--text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pd-nav-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }

/* Buttons */
.btn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 16px; border-radius: 7px;
  font-size: 0.84rem; font-weight: 600; font-family: var(--font);
  cursor: pointer; border: 1.5px solid transparent;
  text-decoration: none; transition: all 0.15s; white-space: nowrap;
  line-height: 1;
}
.btn-ghost   { background: transparent; color: var(--text-muted); border-color: var(--border); }
.btn-ghost:hover { background: var(--bg); color: var(--text); border-color: var(--border-dark); }
.btn-outline { background: var(--surface); color: var(--text-2); border-color: var(--border); }
.btn-outline:hover { border-color: var(--border-dark); background: var(--bg); }
.btn-primary { background: var(--purple); color: #fff; border-color: var(--purple); }
.btn-primary:hover { background: #3a01b0; border-color: #3a01b0; }
.btn-primary:disabled { opacity: 0.38; cursor: not-allowed; }
.btn-cyan    { background: var(--cyan); color: var(--text-2); border-color: var(--cyan); }
.btn-cyan:hover { background: #28e8b8; }
.btn-success { background: #16a34a; color: #fff; border-color: #16a34a; }
.btn-success:hover { background: #15803d; }
.btn-sm { padding: 6px 12px; font-size: 0.78rem; }
.btn-xs { padding: 4px 9px; font-size: 0.72rem; }

/* Pills */
.pill {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px 9px; border-radius: 100px;
  font-size: 0.7rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.05em;
}
.pill-dot { width: 5px; height: 5px; border-radius: 50%; }
.pill-ready     { background: var(--cyan-dim); color: #0a6b52; }
.pill-ready .pill-dot { background: var(--cyan); }
.pill-ingested  { background: var(--cyan-dim); color: #0a6b52; }
.pill-ingested .pill-dot { background: var(--cyan); }
.pill-ingesting { background: #FEF9C3; color: #854D0E; }
.pill-ingesting .pill-dot { background: #EAB308; }
.pill-draft     { background: var(--border); color: var(--text-muted); }
.pill-draft .pill-dot { background: #bbb; }
.pill-failed    { background: var(--pink-dim); color: #9F1239; }
.pill-failed .pill-dot { background: var(--pink); }
.pill-published { background: var(--cyan-dim); color: #0a6b52; }
.pill-published .pill-dot { background: var(--cyan); }
.pill-archived  { background: var(--border); color: var(--text-muted); }
.pill-archived .pill-dot { background: #bbb; }

/* Shell */
.pd-shell { display: flex; min-height: calc(100vh - 56px); }

/* Left Rail */
.pd-rail {
  width: 264px; flex-shrink: 0;
  background: var(--surface-soft);
  border-right: 1.5px solid var(--border);
  padding: 20px 16px;
  display: flex; flex-direction: column; gap: 20px;
  position: sticky; top: 56px;
  height: calc(100vh - 56px);
  overflow-y: auto;
}
.rail-section { display: flex; flex-direction: column; gap: 8px; }
.rail-label {
  font-size: 0.68rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--text-muted);
}
.rail-divider { border: none; border-top: 1.5px solid var(--border); }

/* Product card */
.product-card {
  background: var(--surface); border: 1.5px solid var(--border);
  border-radius: 10px; padding: 14px;
}
.product-card-name { font-size: 0.92rem; font-weight: 700; color: var(--text-2); line-height: 1.3; margin-bottom: 3px; }
.product-card-cat  { font-size: 0.78rem; color: var(--text-muted); margin-bottom: 10px; }
.product-card-url  {
  font-size: 0.72rem; color: var(--purple); text-decoration: none;
  font-family: var(--mono); display: block; word-break: break-all;
}
.product-card-url:hover { text-decoration: underline; }

/* Setup checklist */
.checklist { display: flex; flex-direction: column; gap: 2px; }
.checklist-step {
  display: flex; align-items: flex-start; gap: 9px;
  padding: 7px 8px; border-radius: 7px;
  cursor: pointer; transition: background 0.12s;
  border: none; background: transparent; font-family: var(--font);
  text-align: left; width: 100%;
  text-decoration: none; color: inherit;
}
.checklist-step:hover { background: var(--bg); }
.checklist-step.active { background: var(--purple-dim); }
.step-icon {
  width: 18px; height: 18px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 0.6rem; font-weight: 800; flex-shrink: 0; margin-top: 1px;
  border: 2px solid var(--border-dark);
  color: var(--text-muted); background: var(--surface);
}
.step-icon.done  { background: var(--cyan); border-color: var(--cyan); color: var(--text-2); }
.step-icon.active { border-color: var(--purple); color: var(--purple); }
.step-icon.pending { opacity: 0.5; }
.step-body { display: flex; flex-direction: column; gap: 1px; flex: 1; }
.step-label { font-size: 0.8rem; font-weight: 600; color: var(--text-2); }
.step-label.muted { color: var(--text-muted); font-weight: 500; }
.step-sub   { font-size: 0.7rem; color: var(--text-muted); }

/* Attention badge */
.attention-badge {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 17px; height: 17px; border-radius: 100px;
  background: var(--purple); color: #fff;
  font-size: 0.62rem; font-weight: 800;
  padding: 0 4px; margin-left: auto; flex-shrink: 0;
}

/* Main */
.pd-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }

/* Tab panels */
.tab-panel { padding: 28px; overflow-y: auto; }
/* ATT-77: Sources tab opts out of the legacy 28px inner padding AND the
   legacy inner-scroll. The new .ps-content shell already provides the
   outer 32px padding AND is the page's scroll container; .tab-panel's
   overflow-y: auto created a nested scroll context that surfaced as a
   double scrollbar in Safari once the source list got long enough. */
/* padding-bottom lives on the (scrollable) tab content, not on .ps-content:
   .ps-content is the overflow-y:auto scroll container and its bottom padding is
   clipped at scroll-end, so a long tab's last item would butt the bottom edge.
   32px matches .ps-content's inset → consistent bottom spacing across all
   tabs whether or not they scroll. Top/left/right stay flush. */
.tab-panel--flush { padding: 0 0 32px; overflow-y: visible; }

/* Live Status wrapper is always rendered as a Turbo Stream target, but is
   empty when no JobRun exists. As an empty flex child of .ps-tab-content
   it still claims the parent's 14px gap, leaving phantom space above the
   tab content. Hide the wrapper when it contains no element children. */
[id^="product_"][id$="_live_status"]:not(:has(*)) { display: none; }

/* Section header */
.section-hdr {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 14px; padding-bottom: 10px;
  border-bottom: 1.5px solid var(--border);
}
.section-title {
  font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--text-muted);
}
.section-count { font-family: var(--mono); font-size: 0.72rem; color: var(--text-muted); }

/* ATT-77 Chunk 3 — sub-section header variant used by the Sources tab's
   DISCOVERED SOURCES / SOURCES blocks. Drops the divider line below the
   header (Figma 1:1211 / 1:1587 do not include one) and lightens the
   bottom spacing so the intro paragraph / list sits close. */
.section-hdr--flush {
  border-bottom: 0;
  margin-bottom: 8px;
  padding-bottom: 0;
}
/* Caption/Bold treatment for sub-section captions ("DISCOVERED SOURCES",
   "SOURCES"). Figma 67:11673 style = Caption/Bold (Semibold 600, 12/18,
   letter-spacing 0), uppercase, text-muted. Distinct from .section-title
   (legacy Bold 700 + 0.08em letter-spacing) which the Brief tab still
   uses. */
.section-caption {
  font-family:    var(--text-caption-bold-font-family);
  font-weight:    var(--text-caption-bold-font-weight);
  font-size:      var(--text-caption-bold-font-size);
  line-height:    var(--text-caption-bold-line-height);
  letter-spacing: var(--text-caption-bold-letter-spacing);
  text-transform: uppercase;
  color: var(--text-muted);
}
/* Right-side cluster of count + action buttons inside a sub-section header. */
.section-actions {
  display: flex;
  align-items: center;
  gap: 12px;
}
/* Inline form wrapper used so button_to / form_with don't insert a block
   break between adjacent sub-header action buttons. */
.kb-action-form,
.disco-action-form {
  display: inline-flex;
  margin: 0;
}
/* Explanatory paragraph that sits between a sub-header and its list
   (currently only the "Sources found during discovery." line). */
.section-intro {
  font-family:    var(--text-caption-font-family);
  font-weight:    var(--text-caption-font-weight);
  font-size:      var(--text-caption-font-size);
  line-height:    var(--text-caption-line-height);
  letter-spacing: var(--text-caption-letter-spacing);
  color: var(--text-muted);
  margin-bottom: 12px;
}

@keyframes spin { to { transform: rotate(360deg); } }
.spinner {
  width: 14px; height: 14px; border: 2px solid #EAB308;
  border-top-color: transparent; border-radius: 50%;
  animation: spin 0.8s linear infinite; flex-shrink: 0;
}

/* Empty state */
.empty { text-align: center; padding: 56px 32px; }
.empty-icon  { font-size: 2rem; margin-bottom: 12px; }
.empty-title { font-size: 0.95rem; font-weight: 600; color: var(--text-2); margin-bottom: 6px; }
.empty-body  { font-size: 0.84rem; color: var(--text-muted); line-height: 1.6; max-width: 320px; margin: 0 auto 20px; }

/* ── SOURCES TAB ── */
.sources-split { display: flex; flex-direction: column; gap: 32px; }

/* ========================================================================
   ATT-77 Chunk 2 — cell-based source row (.src-*) and discovered-source
   card (.disco-*). Coexists with the legacy .ks-table / .disc-* during the
   transition; Chunk 3 swaps the partial usages and Chunk 7 removes the
   legacy CSS. New classes only — do not touch the legacy block above.
   Figma: 1:1929 / 1:1587 (rows), 5:899 (cards).
   ====================================================================== */

/* ─── Source row ─── */
.src-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding: 8px 12px;
  background: var(--bg-surface);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-small);
  box-shadow: var(--shadow-elevated-sm);
  transition: border-color 0.12s, box-shadow 0.12s;
}
.src-row:hover {
  border-color: var(--brand-primary);
}
.src-row-left {
  display: flex;
  align-items: center;
  gap: 24px;
  min-width: 0;
  /* Grow to fill the row's main axis so the title cell (which uses
     flex: 1 1 0 below) has space to absorb. The kebab is pushed to the
     right edge by .src-row's justify-content: space-between. */
  flex: 1 1 auto;
}
/* Column cells — fixed slot widths per Figma 1:1929 anatomy for the pill
   columns so they align across rows regardless of pill content length.
   The title column uses flex: 1 1 0 so every row's title slot is the
   SAME width (whatever the row has left over after the fixed cells), and
   pills sit immediately to the right of the title with the consistent
   24px gap. */
.src-row-cell {
  display: flex;
  align-items: center;
  min-width: 0;
}
.src-row-cell--title       { flex: 1 1 0;    min-width: 0; }
.src-row-cell--kind        { flex: 0 0 50px; }
.src-row-cell--provenance  { flex: 0 0 92px; }
.src-row-cell--status      { flex: 0 0 78px; }
.src-row-cell--date        { flex: 0 0 96px; }
.src-row-title {
  font-family:    var(--text-body-bold-font-family);
  font-weight:    var(--text-body-bold-font-weight);
  font-size:      var(--text-body-bold-font-size);
  line-height:    var(--text-body-bold-line-height);
  letter-spacing: var(--text-body-bold-letter-spacing);
  color: var(--text-primary);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.src-row-date {
  font-family: var(--font-mono);
  font-size:   var(--text-caption-font-size);
  line-height: var(--text-caption-line-height);
  color: var(--text-muted);
}
.src-row-kebab {
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 0;
  border-radius: 9999px;
  color: var(--text-muted);
  cursor: pointer;
  padding: 0;
}
.src-row-kebab:hover {
  background: var(--palette-neutrals-100);
  color: var(--text-secondary);
}
.src-row-kebab-icon {
  width: 20px;
  height: 20px;
}

/* ─── Discovered-source card ─── */
.disco-card {
  display: flex;
  align-items: stretch;
  gap: 20px;
  padding: 16px 20px;
  background: var(--bg-surface);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-small);
  transition: border-color 0.12s;
}
.disco-card:hover {
  border-color: var(--brand-primary);
}
.disco-score {
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: 10px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  align-self: center;
}
/* Score number: Figma "Data/Default" — bind to the --text-data-* token
   group (SF Mono Medium 13/18, letter-spacing 0). */
.disco-score-num {
  font-family:    var(--text-data-font-family);
  font-weight:    var(--text-data-font-weight);
  font-size:      var(--text-data-font-size);
  line-height:    var(--text-data-line-height);
  letter-spacing: var(--text-data-letter-spacing);
}
/* "Score" label: Figma "UI/Label Small" — bind to the --text-ui-label-small-*
   token group (General Sans Variable Medium 10/12, letter-spacing 0). */
.disco-score-label {
  font-family:    var(--text-ui-label-small-font-family);
  font-weight:    var(--text-ui-label-small-font-weight);
  font-size:      var(--text-ui-label-small-font-size);
  line-height:    var(--text-ui-label-small-line-height);
  letter-spacing: var(--text-ui-label-small-letter-spacing);
}
/* High tier uses --status-badge-confirmation (NOT --status-success-default —
   that's the pale mint used for the kb-summary banner). Text token is
   --palette-emerald-800 per Figma 91:15053. */
.disco-score-success {
  background: var(--status-badge-confirmation);
  color:      var(--palette-emerald-800);
}
.disco-score-warning {
  background: var(--status-warning-default);
  color:      var(--status-warning-subtle);
}
.disco-score-error {
  background: var(--status-error-default);
  color:      var(--status-error-text);
}
.disco-body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  justify-content: center;
}
.disco-title {
  font-family:    var(--text-body-bold-font-family);
  font-weight:    var(--text-body-bold-font-weight);
  font-size:      var(--text-body-bold-font-size);
  line-height:    var(--text-body-bold-line-height);
  letter-spacing: var(--text-body-bold-letter-spacing);
  color: var(--text-primary);
}
.disco-rationale {
  font-family:    var(--text-caption-font-family);
  font-weight:    var(--text-caption-font-weight);
  font-size:      var(--text-caption-font-size);
  line-height:    var(--text-caption-line-height);
  letter-spacing: var(--text-caption-letter-spacing);
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.disco-meta {
  display: flex;
  align-items: center;
  gap: 20px;
  margin-top: 2px;
}
.disco-type {
  flex-shrink: 0;
}
.disco-url {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 16px;
  color: var(--brand-primary);
  text-decoration: none;
}
.disco-url:hover { text-decoration: underline; }
.disco-url-icon {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
}
.disco-actions {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 8px;
  align-self: center;
}

/* Sources tab — workflow-state-aware CTA card (ATT-77, Figma 67:11603).
   Shown only when next_workflow_step is :analyze or :discover. Sits above
   the Knowledge Base section as a workflow prompt, not an empty-list
   placeholder — the source list still renders below when sources exist. */
.kb-cta-card {
  max-width: 584px;
  margin: 0 auto 24px;
  border: 1px solid var(--status-info-default);
  border-radius: var(--radius-small);
  padding: 16px 20px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
}
.kb-cta-text {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  text-align: center;
}
.kb-cta-title {
  font-family:    var(--text-h4-font-family);
  font-weight:    var(--text-h4-font-weight);
  font-size:      var(--text-h4-font-size);
  line-height:    var(--text-h4-line-height);
  letter-spacing: var(--text-h4-letter-spacing);
  color: var(--text-secondary);
}
.kb-cta-sub {
  font-family:    var(--text-caption-font-family);
  font-weight:    var(--text-caption-font-weight);
  font-size:      var(--text-caption-font-size);
  line-height:    var(--text-caption-line-height);
  letter-spacing: var(--text-caption-letter-spacing);
  color: var(--text-muted);
}
.kb-cta-form {
  margin: 0;
}

/* ATT-77 Chunk 3 — sub-section wrapper + list stack for the Sources tab.
   Two sub-sections share the same .kb-sub container (DISCOVERED SOURCES,
   SOURCES). The list is a vertical stack of Chunk 2 row/card components. */
.kb-sub {
  display: flex;
  flex-direction: column;
  margin-bottom: 24px;
}
.kb-sub:last-child {
  margin-bottom: 0;
}
.kb-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* Summary banner shown when every discovered source has been accepted or
   dismissed (the DISCOVERED SOURCES sub-section would otherwise be empty).
   Replaces the old inline-styled cyan-dim banner. */
.kb-summary-banner {
  background: var(--status-success-default);
  color:      var(--status-success-subtle);
  border: 1px solid var(--status-success-subtle);
  border-radius: var(--radius-small);
  padding: 10px 14px;
  margin-bottom: 24px;
  font-family:    var(--text-body-small-font-family);
  font-weight:    500;
  font-size:      var(--text-body-small-font-size);
  line-height:    var(--text-body-small-line-height);
  letter-spacing: var(--text-body-small-letter-spacing);
}

/* ATT-77 Chunk 5 — "+" popover wrapper. Anchors the absolute-positioned
   .ps-menu-dropdown to the "+" trigger so the right-anchor rule from
   .ps-menu-dropdown--grouped (left:auto; right:0) keeps the popover
   on-screen. Mirrors the .src-row-kebab-wrap pattern. */
.add-link-popover-wrap {
  position: relative;
  flex-shrink: 0;
}
/* Open-state visual on the "+" trigger (mirrors .src-row-kebab[aria-expanded]
   and .btn-icon-elevated:hover token usage). */
.btn-icon-elevated[aria-expanded="true"] {
  border-color: var(--brand-hover);
  color: var(--brand-hover);
}

/* Inline Add-link form (Figma 70:12311). Hidden by default; revealed by
   the .add-link-form--visible modifier when "+" → "Add link" fires.
   Plain display toggle — no max-height transition (a fixed max-height
   would clip the form whenever its actual height drifts).
   ──
   Two-row column: the input + button row sits on top, and an inline
   error message (when present) spans the full width below. Putting the
   error outside the input row keeps the buttons center-aligned with the
   input in both states (matches Figma's default-state alignment); the
   error has full row width to render without crowding the buttons. */
.add-link-form {
  display: none;
  flex-direction: column;
  gap: 4px;
  width: 100%;
  margin: 0;
}

/* ATT-77 Chunk 6c — hidden upload form. No visible UI per Figma 70:13716
   (native OS picker only, no drop zone). The Stimulus add_files controller
   programmatically clicks the inner <input type="file"> and requestSubmits
   this form when files are picked. */
.add-files-form {
  display: none;
}
.add-link-form--visible {
  display: flex;
  margin-bottom: 8px;
}
.add-link-form-row {
  display: flex;
  align-items: center;
  gap: 24px;
  width: 100%;
}
/* Layout wrapper around the input — flex-grow only. The red field
   treatment in the error case comes from adding .form-field-error to
   this same element (existing descendant selector targets .form-input). */
.add-link-form-field {
  flex: 1 1 auto;
  min-width: 0;
}
/* Override .form-input height (36→42) and background (--bg-elevated→
   --bg-surface) to match the Figma input spec (h-42, white). Inherits
   border, padding, font, focus styling from .form-input. */
.add-link-form-input {
  height: 42px;
  background: var(--bg-surface);
}
/* Inline validation error rendered under the input when the server
   re-renders the partial with add_link_error. Caption tokens + status
   error color; the matching red field treatment comes from the existing
   .form-field-error rule applied to the wrapper. */
.add-link-form-error {
  font-family:    var(--text-caption-font-family);
  font-weight:    var(--text-caption-font-weight);
  font-size:      var(--text-caption-font-size);
  line-height:    var(--text-caption-line-height);
  letter-spacing: var(--text-caption-letter-spacing);
  color: var(--status-error-text);
}
.add-link-form-actions {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 10px;
}
/* ========================================================================
   Form fields (shared component — ATT-85, Figma node 303:1122)
   ------------------------------------------------------------------------
   App-wide unprefixed form-field component. Default state: grey-fill
   input, neutral 1px border, label above. Error state: add
   .form-field-error to the .form-field wrapper — the label and the
   input/select/textarea inside render red. Used by every form in the app;
   do NOT nest under a surface prefix (.ps-*, .pi-*, etc.).
   ====================================================================== */
.form-field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.form-label {
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: 12px;
  line-height: 16px;
  letter-spacing: 0.5px;
  color: var(--text-secondary);
}
.form-input,
.form-textarea,
.form-select {
  display: block;
  width: 100%;
  padding: 0 12px;
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-small);
  background: var(--bg-elevated);
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: 14px;
  line-height: 20px;
  color: var(--text-primary);
  margin: 0;
}
.form-input,
.form-select {
  height: 36px;
}
.form-textarea {
  padding: 9px 12px;
  resize: vertical;
  min-height: 72px;
}
.form-input:focus,
.form-textarea:focus,
.form-select:focus {
  outline: none;
  border-color: var(--border-focus);
  background: var(--bg-surface);
  box-shadow: 0 0 0 3px var(--brand-subtle);
}
/* ATT-246 — the ENABLED placeholder. It had no rule at all, so every input and
   textarea in the app fell back to Chrome's UA default (#757575), which is not a
   palette colour: it sits between neutrals-400 (#9da3ae) and neutrals-500
   (#6c727f) and is DARKER than the .form-hint sitting directly under the field.
   A placeholder reading heavier than the hint below it reads as entered text,
   which is exactly how it was spotted.

   --text-muted, matching the approved styleguide (form_components, "TEXT INPUT").
   That page rendered its own two placeholder examples in two different colours:
   the mono-prefix variant had .input-prefix input::placeholder and looked right,
   the leading-icon one had nothing and did not. The comment on that prefix rule
   claims it exists "to match the muted treatment of the base inputs" — a premise
   that was never true until this rule. It stays anyway: its input carries no
   .form-input class, so it is not covered here.

   `opacity: 1` because Firefox applies a default placeholder opacity that would
   lighten the token further. */
.form-input::placeholder,
.form-textarea::placeholder {
  color: var(--text-muted);
  opacity: 1;
}

/* Disabled — greyer fill, dark-but-muted text (ATT-122). -webkit-text-fill-color
   is needed because WebKit otherwise overrides `color` on disabled inputs. */
.form-input:disabled,
.form-textarea:disabled,
.form-select:disabled {
  background: var(--palette-neutrals-100);
  color: var(--palette-neutrals-700);
  -webkit-text-fill-color: var(--palette-neutrals-700);
  cursor: not-allowed;
}
/* Disabled placeholder = neutrals-400 (lighter than the neutrals-700 disabled
   entered-text — a placeholder always reads as secondary). The
   -webkit-text-fill-color override is required: the input's own disabled
   -webkit-text-fill-color would otherwise paint the placeholder neutrals-700 too
   (ATT-162 — first disabled input whose placeholder is the visible text). */
.form-input:disabled::placeholder,
.form-textarea:disabled::placeholder {
  color: var(--palette-neutrals-400);
  -webkit-text-fill-color: var(--palette-neutrals-400);
  opacity: 1;
}
.form-select {
  appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'><path d='M3 5l3 3 3-3' stroke='%23394150' stroke-width='1.4' fill='none' stroke-linecap='round' stroke-linejoin='round'/></svg>");
  background-repeat: no-repeat;
  background-position: right 10px center;
  background-size: 12px;
  padding-right: 32px;
}

.form-hint {
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: 13px;
  line-height: 20px;
  color: var(--text-muted);
  margin: 0;
}

/* Error variant — apply .form-field-error to the .form-field wrapper.
   Only the input/select/textarea/dropdown border and the hint render in
   --status-error-subtle. The LABEL stays --text-secondary (per the
   form-components styleguide error example + Figma 413:3244, where the
   field label is text-secondary in the error state — it does NOT go red). */
.form-field-error .form-input,
.form-field-error .form-textarea,
.form-field-error .form-select {
  border-color: var(--status-error-subtle);
}
/* ATT-266 — .input-prefix OWNS ITS BORDER, so the rule above cannot reach it.
   The component draws the stroke on the WRAPPER (the mono prefix and the input
   share one box), and the input inside carries no .form-input class at all — so
   a .form-field-error field built from this variant reddened nothing and a
   merchant saw a message with no field attached to it.

   NEVER WORKED, rather than regressed: `git log -S "form-field-error
   .input-prefix"` finds nothing. It stayed invisible because the form-components
   styleguide documents this variant (Filled, Empty) and documents the error state
   (on a plain .form-input) and never crosses the two — the DS page has the same
   gap the CSS did.

   Same shape and same fix as .form-field-error .ctl-trigger (:7753) and
   .form-field-error .ob-otp-box (:6856): a component whose border is not on an
   element the canonical rule lists needs naming. AFTER :focus-within, so a
   focused field in error keeps the red stroke, which is how the canonical rule
   behaves too. */
.form-field-error .input-prefix { border-color: var(--status-error-subtle); }

/* Inline error hint — the .form-hint slot rendered in red + medium weight
   (ATT-138). Applies automatically to a hint inside a .form-field-error
   wrapper, and standalone via .form-hint-error for form-level errors that
   aren't tied to a single field (e.g. the product-selection errors). */
.form-field-error .form-hint,
.form-hint-error {
  color: var(--status-error-text);
  font-weight: 500;
}

/* ------------------------------------------------------------------------
   Form error banner — sits at the top of a form (typically inside
   .ps-modal-body) when validation fails. Pair with .form-field-error on
   the affected field wrappers. See Figma 107:2825. The wiring of when
   to render this banner is the consuming form's job — this rule is only
   the visual shell. Built ATT-85; wired in the next prompt.
   ---------------------------------------------------------------------- */
.form-error-banner {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 12px 14px;
  border: 1px solid var(--status-error-subtle);
  border-radius: var(--radius-small);
  background: var(--status-error-default);
  color: var(--status-error-text);
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: 14px;
  line-height: 20px;
}
.form-error-banner strong {
  font-weight: 600;
}

/* ── BRIEF TAB ── */

/* ATT-79 — Brief tab empty state ("No Product Brief generated yet").
   Figma "Info Dialog" (9GqHh6I460fmgxRzgYEjom nodes 5:956 / 170:2814). The
   card is identical across the disabled (:analyze / :discover) and enabled
   (:review / :brief) states — only the body copy and the CTA button variant
   change, both handled in _brief.html.erb. Same centered 584px Info Dialog
   card as the Sources .kb-cta-card; the only difference is .kb-cta-card
   carries a list-stack margin-bottom (it sits above the source list), while
   this is a standalone card (margin: 0 auto). Tokens only — no hardcoded hex. */
.brief-empty-card {
  max-width: 500px;
  margin: 0 auto;
  border: 1px solid var(--status-info-default);
  border-radius: var(--radius-small);
  padding: 16px 20px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}
.brief-empty-card form { margin: 0; }
/* ATT-246 — display:flex above BEATS the hidden attribute's UA display:none, so
   `element.hidden = true` on this card does nothing at all. Four other classes in
   this sheet carry the same guard (.opt, .jc-tab-panel, .jc-rail-empty,
   .jc-suggestion) for the same reason; this is the fifth, added when Settings →
   Brand voice hid the "no voice yet" card on starting a run and it stayed on
   screen underneath the progress strip. */
.brief-empty-card[hidden] { display: none; }
/* Icon variant for .brief-empty-card. ATT-134. Used today only on the
   Collections Index (Figma 342:13545); future index empty states with icons
   can adopt the same class. The SVG comes from a `shared/_icon_*` partial via
   a `class:` local. Color = palette/neutrals/300 per Figma 342:13545
   (the prompt's text-secondary was an eyeball error — Arash confirmed Figma). */
.brief-empty-card-icon {
  width: 37px;
  height: 40px;
  color: var(--palette-neutrals-300);
  flex-shrink: 0;
}
.brief-empty-text {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  width: 100%;
}
.brief-empty-title {
  font-family:    var(--text-h4-font-family);
  font-weight:    var(--text-h4-font-weight);
  font-size:      var(--text-h4-font-size);
  line-height:    var(--text-h4-line-height);
  letter-spacing: var(--text-h4-letter-spacing);
  color: var(--text-secondary);
  text-align: center;
}
.brief-empty-body {
  font-family:    var(--text-caption-font-family);
  font-weight:    var(--text-caption-font-weight);
  font-size:      var(--text-caption-font-size);
  line-height:    var(--text-caption-line-height);
  letter-spacing: var(--text-caption-letter-spacing);
  color: var(--text-muted);
  text-align: center;
  margin: 0;
}
/* ATT-161 — the Audiences Journeys tab empty state (Figma 675:3913) is the
   BORDERLESS variant of the info-dialog: no card border, and the title is
   text-muted (not text-secondary). Spacing/structure/body otherwise reuse
   .brief-empty-card / .brief-empty-text / .brief-empty-body. Scoped so the
   bordered .brief-empty-card on every other surface is unaffected. */
.brief-empty-card.brief-empty-borderless { border: none; }
.brief-empty-borderless .brief-empty-title { color: var(--text-muted); }

/* ATT-79 — populated brief view. Figma Application Spec
   (9GqHh6I460fmgxRzgYEjom) nodes 9:1888 (full tab) / 28:3706 (toolbar) /
   9:1981 (claim card) / 9:1990 (evidence). Tokens only — no hardcoded hex.
   Reuses the .pill-label and .btn-elevated-xs DS components; the classes
   below are brief-specific layout + typography. Mono "Data" text uses a
   literal 13px because the type system has no mono type-scale token (the old
   .brief-meta/.evidence-src used literal rem sizes for the same reason). */

/* Toolbar (node 28:3706) — Version/Generated data left, actions right. */
.brief-toolbar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; margin-bottom: 24px;
}
.brief-toolbar-data { display: flex; align-items: center; gap: 20px; font-family: var(--mono); font-size: 13px; }
.brief-data-label { color: var(--text-muted); }
.brief-data-value { font-weight: 700; color: var(--text-secondary); }
.brief-toolbar-actions { display: flex; align-items: center; gap: 8px; }
.brief-toolbar-actions form { margin: 0; }

/* Section block (node 9:1976) */
.brief-section { margin-bottom: 28px; }
.brief-section-hdr { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 12px; }
.brief-section-title {
  font-family:    var(--text-caption-bold-font-family);
  font-weight:    var(--text-caption-bold-font-weight);
  font-size:      var(--text-caption-bold-font-size);
  line-height:    var(--text-caption-bold-line-height);
  letter-spacing: var(--text-caption-bold-letter-spacing);
  text-transform: uppercase; color: var(--text-muted);
}
.brief-section-count { font-family: var(--mono); font-size: 13px; color: var(--text-muted); }
.brief-section-count-num { font-weight: 700; color: var(--text-secondary); }
.brief-section-empty {
  font-family:    var(--text-caption-font-family);
  font-weight:    var(--text-caption-font-weight);
  font-size:      var(--text-caption-font-size);
  line-height:    var(--text-caption-line-height);
  letter-spacing: var(--text-caption-letter-spacing);
  color: var(--text-muted); margin: 0;
}

/* ATT-147 — group brief per-product header. The product name is H4 (text
   primary, weight 550) with the priority pill set immediately beside it (a
   left-aligned flex row, NOT the section header's space-between), so the
   product reads as a tier above its sub-section captions. */
.gb-product-header {
  display: flex; align-items: center; gap: 8px; margin-bottom: 12px;
}
.gb-product-name {
  font-family:    var(--text-h4-font-family);
  font-weight:    550;
  font-size:      var(--text-h4-font-size);
  line-height:    var(--text-h4-line-height);
  letter-spacing: var(--text-h4-letter-spacing);
  color: var(--text-primary);
}

/* Claim card (node 9:1981) — white surface; full 1px + 6px LEFT border, both
   in the confidence color (set by the .claim-card-* modifier). */
.claim-card {
  background: var(--bg-surface);
  border: 1px solid; border-left-width: 6px;
  border-radius: var(--radius-small); padding: 16px; margin-bottom: 16px;
}
.claim-card-high   { border-color: var(--palette-emerald-100); }
.claim-card-medium { border-color: var(--status-warning-default); }
.claim-card-low    { border-color: var(--status-error-default); }
/* ATT-147 — group brief "Product differences" reuse the claim-card chrome with
   a saturated warning border (Figma 493:3149 reuses the "brief item row/inner"
   claim-card component). Stronger than -medium's pale --status-warning-default
   so a contradiction reads as more than a medium-confidence claim. */
.claim-card-warning { border-color: var(--status-warning-subtle); }
.claim-text {
  font-family:    var(--text-body-bold-font-family);
  font-weight:    var(--text-body-bold-font-weight);
  font-size:      var(--text-body-bold-font-size);
  line-height:    var(--text-body-bold-line-height);
  letter-spacing: var(--text-body-bold-letter-spacing);
  color: var(--text-primary); margin: 0 0 12px;
}
.claim-pills { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }

/* Evidence quote block (node 9:1990) — muted surface, 4px left border,
   excerpt left + right-aligned "Source: <host>". */
.evidence-quote {
  display: flex; align-items: baseline; justify-content: space-between; gap: 16px;
  background: var(--palette-neutrals-50);
  border-left: 4px solid var(--palette-neutrals-400);
  padding: 6px 12px; margin-bottom: 6px;
}
.evidence-quote:last-child { margin-bottom: 0; }
.evidence-excerpt {
  flex: 1;
  font-family:    var(--text-caption-font-family);
  font-weight:    var(--text-caption-font-weight);
  font-size:      var(--text-caption-font-size);
  line-height:    var(--text-caption-line-height);
  letter-spacing: var(--text-caption-letter-spacing);
  color: var(--text-secondary); margin: 0;
}
.evidence-src { flex-shrink: 0; font-family: var(--mono); font-size: 12px; text-align: right; white-space: nowrap; }
.evidence-src-label { color: var(--text-muted); }
/* Linked source (URL present) — brand, genuinely clickable. Fallback
   (no URL) is plain muted text, not a link. */
.evidence-src-link { color: var(--text-link); text-decoration: none; }
.evidence-src-link:hover { text-decoration: underline; }
.evidence-src-value-muted { color: var(--text-muted); }

/* Group brief "Product differences" body (ATT-147, Figma 493:3149). The card
   itself is .claim-card.claim-card-warning (chrome reused); the title is
   .claim-text. These are the two text rows beneath it: "Affects: <products>"
   (Body Small, neutrals-500, label in Medium) + the details line (Body
   Default, secondary). Gap mirrors the Figma source-info (6px between rows). */
.gb-difference-affects {
  font-family:    var(--text-body-small-font-family);
  font-weight:    var(--text-body-small-font-weight);
  font-size:      var(--text-body-small-font-size);
  line-height:    var(--text-body-small-line-height);
  letter-spacing: var(--text-body-small-letter-spacing);
  color: var(--palette-neutrals-500); margin: 0 0 6px;
}
.gb-difference-affects-label { font-weight: 500; }
.gb-difference-details {
  font-family:    var(--text-body-font-family);
  font-weight:    var(--text-body-font-weight);
  font-size:      var(--text-body-font-size);
  line-height:    var(--text-body-line-height);
  letter-spacing: var(--text-body-letter-spacing);
  color: var(--text-secondary); margin: 0;
}

/* Product Summary modal body (read-only prose) — Body Default. */
.product-summary-text {
  font-family:    var(--text-body-font-family);
  font-weight:    var(--text-body-font-weight);
  font-size:      var(--text-body-font-size);
  line-height:    var(--text-body-line-height);
  letter-spacing: var(--text-body-letter-spacing);
  color: var(--text-primary); margin: 0;
}
.product-summary-text-empty { color: var(--text-muted); }

/* Product Brief Version History overlay rows (Figma 28:4356 / 28:4387). One
   row per generated brief: Version (left) / Generated (right), mono Data/Small
   (12px). Reuses .brief-data-label (muted) / .brief-data-value (bold) for the
   label/value treatment — only the 12px mono size + row layout are row-specific
   (the toolbar's .brief-toolbar-data is 13px Data/Default). Tokens only. */
.version-history-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  padding: 16px 0;
  border-bottom: 1px solid var(--palette-neutrals-100);
  font-family: var(--mono);
  font-size: 12px;
}

/* ── INSIGHTS TAB ── */
/* ATT-79 Chunk 3a — populated view (Figma 189:2916): single-column stack of
   four full-width section cards, DS pills, purple header divider. Replaces the
   legacy .intel-grid / .intel-card / .itag rules. The .tooltip-* rules below
   are retained as-is (Chunk 3b restyles the source tooltip). Tokens only. */
.intel-stack { display: flex; flex-direction: column; gap: 16px; }
.intel-section {
  display: flex;
  flex-direction: column;
  gap: 16px;
  border: 1px solid var(--status-info-default);
  border-radius: var(--radius-small);
  padding: 16px 20px;
}
.intel-section-hdr {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--border-focus);
}
.intel-section-hdr-top { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.intel-section-title {
  font-family:    var(--text-caption-bold-font-family);
  font-weight:    var(--text-caption-bold-font-weight);
  font-size:      var(--text-caption-bold-font-size);
  line-height:    var(--text-caption-bold-line-height);
  letter-spacing: var(--text-caption-bold-letter-spacing);
  text-transform: uppercase;
  color: var(--text-muted);
}
.intel-section-count { font-family: var(--mono); font-size: 13px; color: var(--text-muted); white-space: nowrap; }
.intel-section-count-num { font-weight: 700; color: var(--text-secondary); }
.intel-section-desc {
  font-family:    var(--text-caption-font-family);
  font-weight:    var(--text-caption-font-weight);
  font-size:      var(--text-caption-font-size);
  line-height:    var(--text-caption-line-height);
  letter-spacing: var(--text-caption-letter-spacing);
  color: var(--text-muted);
  margin: 0;
}
.intel-items { display: flex; flex-direction: column; }
.intel-item {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px 0;
  border-bottom: 1px solid var(--border-primary);
}
.intel-item:first-child { padding-top: 0; }
.intel-item:last-child  { padding-bottom: 0; border-bottom: 0; }
.intel-item-detail {
  font-family:    var(--text-body-font-family);
  font-weight:    var(--text-body-font-weight);
  font-size:      var(--text-body-font-size);
  line-height:    var(--text-body-line-height);
  letter-spacing: var(--text-body-letter-spacing);
  color: var(--text-secondary);
  margin: 0;
}
.intel-item-tags { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.intel-item-tags-left { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.intel-item-timing {
  font-family:    var(--text-ui-nav-font-family);
  font-weight:    var(--text-ui-nav-font-weight);
  font-size:      var(--text-ui-nav-font-size);
  line-height:    var(--text-ui-nav-line-height);
  letter-spacing: var(--text-ui-nav-letter-spacing);
  color: var(--palette-neutrals-500);
}
.intel-corr { font-family: var(--mono); font-weight: 500; font-size: 13px; color: var(--text-muted); }
.intel-corr-icon { display: inline-flex; align-items: center; margin-left: 6px; color: var(--text-muted); cursor: pointer; }
.intel-corr-icon svg { width: 18px; height: 18px; }

/* Tooltip — ATT-79 Chunk 3b. The source popover reuses the menu-dropdown
   surface (.ps-menu-dropdown): white surface, 0.5px border, elevated shadow,
   14px padding. Source rows are styled like .ps-menu-item with an external-link
   icon and a brand-primary link color. Show + position are driven by
   source_tooltip_controller (position: fixed on hover so it escapes
   .ps-content's overflow clip, placed above the trigger and flipped/clamped to
   stay on-screen) — no CSS :hover reveal, and pointer-events are toggled by the
   controller so the hidden box never blocks clicks. */
.tooltip-wrap { position: relative; display: inline-flex; align-items: center; }
.tooltip-box {
  position: absolute;
  bottom: calc(100% + 6px);
  right: 0;
  left: auto;
  min-width: 200px;
  max-width: 280px;
  background: var(--bg-surface);
  border: 0.5px solid var(--border-primary);
  border-radius: var(--radius-small);
  box-shadow: var(--shadow-container-elevated-bg);
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.15s;
  z-index: 50;
}
.tooltip-box-label {
  padding: 8px 10px 4px;
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: 12px;
  line-height: 18px;
  color: var(--text-muted);
}
.tooltip-source-link {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  border-radius: 8px;
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: 12px;
  line-height: 18px;
  color: var(--brand-primary);
  text-decoration: none;
  word-break: break-all;
}
.tooltip-source-link:visited { color: var(--brand-primary); }
.tooltip-source-link:hover { background: var(--bg-elevated); color: var(--brand-primary); }
.tooltip-source-icon { width: 14px; height: 14px; flex-shrink: 0; color: currentColor; }


/* ── JOURNEYS TAB ── */
.journey-list { display: flex; flex-direction: column; gap: 8px; }
.journey-row {
  display: flex; align-items: center; justify-content: space-between;
  background: var(--surface); border: 1.5px solid var(--border);
  border-radius: 10px; padding: 14px 18px;
  text-decoration: none; transition: border-color 0.15s;
}
.journey-row:hover { border-color: var(--border-dark); }
.journey-row-name { font-size: 0.92rem; font-weight: 600; color: var(--text-2); margin-bottom: 3px; }
.journey-row-meta { font-size: 0.78rem; color: var(--text-muted); font-family: var(--mono); }
.journey-row-right { display: flex; align-items: center; gap: 8px; }

.journey-row-wrapper {
  display: grid;
  grid-template-columns: 1fr 60px;
  border-bottom: 1px solid var(--border);
}
.journey-row-wrapper:last-child { border-bottom: none; }
.journey-row-wrapper .journey-row { border-bottom: none; }
.journey-delete-btn {
  background: none; border: none; border-left: 1px solid var(--border);
  font-size: 0.76rem; color: var(--text-muted); cursor: pointer;
  font-family: var(--font); font-weight: 500; width: 100%;
  transition: color 0.12s, background 0.12s;
}
.journey-delete-btn:hover { color: #9F1239; background: #fff1f2; }

/* ATT-79 Journeys Chunk 3 — per-tab toolbar + list wrappers (Figma 28:3716).
   The row/cell CSS was promoted to the shared .jli-* component, and the delete
   link to .btn-link-destructive, in ATT-120; only these two wrappers remain,
   still used by products/tabs/_journeys_content.html.erb. */
.jt-toolbar { display: flex; justify-content: flex-end; margin-bottom: 20px; }
.jt-list { display: flex; flex-direction: column; gap: 8px; }

/* ── Sidebar inline category editor ── */
.product-card-cat-row {
  display: flex; align-items: center; gap: 4px;
  margin: 2px 0 4px;
}
.product-card-cat {
  font-size: 0.76rem; color: var(--text-muted);
  line-height: 1.3;
}
.cat-edit-btn {
  background: none; border: none; padding: 0 2px;
  cursor: pointer; color: var(--text-muted);
  line-height: 1; display: flex; align-items: center;
  opacity: 0; transition: opacity 0.12s;
  flex-shrink: 0;
}
.product-card:hover .cat-edit-btn { opacity: 1; }
.cat-edit-btn:hover { color: var(--purple); }

/* Inline edit form hidden by default, toggled open via JS */
.cat-edit-form { display: none; margin-top: 6px; }
.cat-edit-form.open {
  display: flex; flex-direction: column; gap: 6px;
}

.cat-edit-select {
  width: 100%; font-size: 0.76rem; font-family: var(--font);
  background: var(--bg); color: var(--text-2);
  border: 1.5px solid var(--border); border-radius: 6px;
  padding: 5px 24px 5px 8px;
  appearance: none; cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' fill='none'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23888' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 7px center;
}
.cat-edit-select:focus {
  outline: none; border-color: var(--purple);
  box-shadow: 0 0 0 3px var(--purple-dim);
}
.cat-edit-actions {
  display: flex; align-items: center; gap: 6px;
}
.cat-save-btn {
  font-size: 0.72rem; font-weight: 600; font-family: var(--font);
  background: var(--purple); color: #fff;
  border: none; border-radius: 6px;
  padding: 5px 12px; cursor: pointer; white-space: nowrap;
  transition: opacity 0.12s;
}
.cat-save-btn:hover { opacity: 0.85; }
.cat-cancel-btn {
  font-size: 0.76rem; font-weight: 500; font-family: var(--font);
  background: none; color: var(--text-muted);
  border: none; cursor: pointer; padding: 4px;
  line-height: 1;
}
.cat-cancel-btn:hover { color: var(--text); }

/* Job progress panel */
.jp-panel {
  background: #fff;
  border: 1.5px solid var(--border);
  border-radius: 12px;
  padding: 20px 24px;
  margin-bottom: 24px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.jp-step {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 0.84rem;
  color: var(--text-muted);
}
.jp-active { color: #131313; font-weight: 500; }
.jp-past   { color: #a0a0a0; }
.jp-done   { color: #16a34a; font-weight: 500; }
.jp-error  { color: #9F1239; font-weight: 500; }
.jp-icon   { width: 18px; text-align: center; font-size: 0.78rem; flex-shrink: 0; }
.jp-msg    { line-height: 1.4; }
@keyframes jp-spin { to { transform: rotate(360deg); } }
.jp-spinner {
  display: inline-block;
  width: 12px; height: 12px;
  border: 2px solid #e5e5e5;
  border-top-color: #4C02E8;
  border-radius: 50%;
  animation: jp-spin 0.7s linear infinite;
}

/* ─────────────────────────────────────────────────────────
   Live Status Strip (ATT-78)
   Sits above the tab content Turbo Frame, persists across
   tab navigation. State-specific colors are scoped via the
   .ls-strip--in_progress / --completed / --failed modifier.
   ───────────────────────────────────────────────────────── */

.ls-strip {
  display: flex;
  flex-direction: column;
  margin-bottom: 16px;
  border: 1px solid var(--border-primary);
  border-radius: 12px;
  background: var(--bg-elevated);
  font-size: 0.875rem;
  transition: opacity 250ms ease-out;
}

/* The strip's top row — fixed 42px, horizontal flex. When the history list
   is hidden the strip collapses to just this row; when shown, the strip
   grows below. */
.ls-strip-row {
  display: flex;
  align-items: center;
  gap: 12px;
  height: 42px;
  padding: 0 14px;
}

/* Auto-hide state for the Live Status strip after a `done` broadcast.
   The Stimulus controller adds this class after the 5s delay, waits for
   the opacity transition to finish, then removes the strip from the DOM.
   pointer-events: none prevents the fading strip from intercepting clicks. */
.ls-strip--hiding {
  opacity: 0;
  pointer-events: none;
}

/* Per Figma node 1:1852, all three states share the same neutral background
   and border. State is communicated via the logo color (handled in ATT-84)
   and, for failed, the red text color applied to .ls-phase / .ls-current-step
   below. The completed state has no special chrome — just the logo color. */
.ls-strip--in_progress,
.ls-strip--completed,
.ls-strip--failed {
  /* no per-state background/border overrides */
}

.ls-logo {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  flex-shrink: 0;
}

/* ── Live Status — Animated Logo (ATT-84) ─────────────────────────────
   Pure-CSS animation, opacity only. Three pairs of equalizer-style waves
   pulse on a staggered 3s cycle while in_progress; a soft glow pulses
   behind the central star. Completed/failed states freeze in a quiet
   pose. The star itself is always solid brand purple.
   Note on blur: the Figma Make source animated `filter: blur()` from 0
   to 15px. Animating blur is the one expensive property here and
   behaves inconsistently in Safari. Instead we set a fixed 15px blur on
   .ls-glow and animate only its opacity — a blurred shape fading in/out
   reads visually the same as one whose blur ramps with opacity.
*/
.ls-logo-mark {
  display: block;
  width: 28px;
  height: 28px;
  flex-shrink: 0;
}

.ls-wave,
.ls-glow,
.ls-star {
  fill: var(--brand-primary);
}

/* Completed state shifts the mark to brand secondary (green) per Figma
   node 14:1113. Failed stays brand primary (purple) — only success gets
   the color change, since failed already communicates state via the
   red text. */
.ls-strip--completed .ls-wave,
.ls-strip--completed .ls-glow,
.ls-strip--completed .ls-star {
  fill: var(--brand-secondary);
}

.ls-wave {
  opacity: 0.1;
}

.ls-glow {
  opacity: 0;
  filter: blur(15px);
}

/* In-progress: pulsing waves (staggered) + pulsing glow. */
.ls-strip--in_progress .ls-wave {
  animation: ls-wave-pulse 3s ease-in-out infinite;
}

.ls-strip--in_progress .ls-wave--outer  { animation-delay: 0s;   }
.ls-strip--in_progress .ls-wave--middle { animation-delay: 0.5s; }
.ls-strip--in_progress .ls-wave--inner  { animation-delay: 1s;   }

.ls-strip--in_progress .ls-glow {
  animation: ls-glow-pulse 3s ease-in-out infinite;
  animation-delay: 1.5s;
}

/* Terminal states: freeze the mark at full opacity, no glow. Color comes
   from the fill rules — completed shifts to brand secondary (green),
   failed stays brand primary (purple). */
.ls-strip--completed .ls-wave,
.ls-strip--failed    .ls-wave {
  opacity: 1;
}

.ls-strip--completed .ls-glow,
.ls-strip--failed    .ls-glow {
  opacity: 0;
}

@keyframes ls-wave-pulse {
  0%, 100% { opacity: 0.1; }
  50%      { opacity: 0.8; }
}

@keyframes ls-glow-pulse {
  0%, 100% { opacity: 0; }
  50%      { opacity: 0.4; }
}

.ls-content {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 4px;
}

/* Bold phase prefix on the lead line. Rendered in all states:
   "Analyzing sources:" / "Analysis completed:" / "Analysis failed:".
   Solid (no shimmer) — only the detail text shimmers in in-progress.
   flex-shrink: 0 so the prefix stays whole and the detail span absorbs
   all ellipsis truncation. */
.ls-phase {
  flex-shrink: 0;
  font-weight: 600;
  color: var(--text-primary);
}

/* Failed state colors both prefix and detail in error red, per Figma. */
.ls-strip--failed .ls-phase,
.ls-strip--failed .ls-current-step {
  color: var(--status-error-text);
}

.ls-current-step {
  min-width: 0;
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Shimmer animation on the in_progress step text. Communicates "something
   is happening" passively during long-running steps. Targets only the text
   span (.ls-current-step), only when the strip is in the in_progress state.
   Completed and failed states set their own color on .ls-strip and have no
   shimmer. Entirely opt-in via prefers-reduced-motion — users with reduced
   motion enabled see static text with no gradient applied.
   Note: Safari may render bold text with slightly reduced fidelity when
   clipped via background-clip: text. Known browser quirk, no clean workaround. */
@media (prefers-reduced-motion: no-preference) {
  .ls-strip--in_progress .ls-current-step {
    background-image: linear-gradient(
      90deg,
      var(--text-primary) 0%,
      var(--text-primary) 45%,
      var(--text-muted)   50%,
      var(--text-primary) 55%,
      var(--text-primary) 100%
    );
    background-size: 200% 100%;
    background-clip: text;
    -webkit-background-clip: text;
    color: transparent;
    animation: ls-shimmer 2s linear infinite;
  }
}

@keyframes ls-shimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

.ls-actions {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-shrink: 0;
  cursor: pointer;
  color: var(--text-muted);
  font-size: 0.8125rem;
  background: transparent;
  border: 0;
  padding: 0;
  font-family: inherit;
}

.ls-details-label {
  font-weight: 500;
}

.ls-chevron {
  width: 18px;
  height: 18px;
  stroke-width: 1.5;
}

/* SVG-namespaced elements don't reliably honor the HTML `hidden` attribute
   via the UA stylesheet (per the HTML spec, `hidden` is defined for HTML
   elements, not SVG). Use an explicit class to toggle chevron visibility
   from the Stimulus controller. */
.ls-chevron.is-hidden {
  display: none;
}

/* History timeline — full step list, rendered inside the strip and toggled
   via the Details button. Scrolls internally past 200px so the strip never
   pushes the tab content arbitrarily far down on long runs. No separator
   border — Figma node 1:1852 shows the expanded panel as one continuous
   neutral surface. Left padding aligns the history items with the content
   column (logo gutter 14 + 28 + gap 12 = 54). */
.ls-history {
  padding: 0 14px 12px 54px;
  max-height: 200px;
  overflow-y: auto;
  color: var(--text-muted);
}

.ls-history-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 0;
  font-size: 0.8125rem;
}

.ls-history-icon {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  stroke-width: 1.5;
  color: var(--text-muted);
}

/* ── Setup / Dashboard mode ── */
.setup-complete-badge {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 12px;
  background: var(--cyan-dim); border: 1px solid var(--cyan);
  border-radius: 8px;
  font-size: 0.8rem; font-weight: 600; color: #0a6b52;
}
.setup-complete-icon {
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--cyan); color: var(--text-2);
  display: flex; align-items: center; justify-content: center;
  font-size: 0.6rem; font-weight: 800; flex-shrink: 0;
}
.setup-complete-text {
  font-size: 0.78rem; font-weight: 600;
}

/* In setup mode, make the checklist more prominent */
.setup-mode .checklist {
  background: var(--surface);
  border: 1.5px solid var(--border);
  border-radius: 10px;
  padding: 8px;
}

/* ── Modal ── */
.modal-backdrop {
  position: fixed; top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(0, 0, 0, 0.4);
  display: flex; align-items: center; justify-content: center;
  z-index: 200;
}
.modal-card {
  background: var(--surface); border-radius: 12px;
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
  width: 100%; max-width: 440px;
}
.modal-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 16px 20px;
  border-bottom: 1px solid var(--border);
}
.modal-title {
  font-size: 0.92rem; font-weight: 700; color: var(--text-2);
}
.modal-close {
  background: none; border: none; font-size: 0.92rem;
  color: var(--text-muted); cursor: pointer; padding: 4px;
  line-height: 1; text-decoration: none;
}
.modal-close:hover { color: var(--text); }
.modal-body {
  padding: 20px;
}
.modal-radio-option {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 12px; border: 1.5px solid var(--border);
  border-radius: 8px; margin-bottom: 6px; cursor: pointer;
  font-size: 0.84rem; color: var(--text-2);
}
.modal-radio-option:hover { border-color: var(--border-dark); }
.modal-radio-meta {
  margin-left: auto; font-size: 0.72rem; color: var(--text-muted);
  font-family: var(--mono);
}

/* ══════════════════════════════════════════
   Shell Layout (Primary Nav + Content Rail)
   ATT-66
   ══════════════════════════════════════════ */

.shell-body {
  background: var(--bg-page);
  font-family: var(--font-sans);
  color: var(--text-primary);
  height: 100vh;
  overflow: hidden;
}

.shell {
  display: flex;
  gap: 10px;
  padding: 12px 12px 28px 16px;
  height: 100vh;
}

.shell-nav {
  width: 200px;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  gap: 20px;
  padding: 16px 12px;
  height: 100%;
}

.shell-content {
  flex: 1;
  min-width: 0;
  height: 100%;
  background: var(--bg-surface);
  border: 0.5px solid var(--palette-neutrals-100);
  border-radius: 12px;
  box-shadow: var(--shadow-container-elevated-bg);
  overflow-y: auto;
  overflow-x: hidden;
}

/* ─── Nav: merchant switcher ─── */

.nav-merchant-wrap {
  position: relative;
  width: 100%;
}

.nav-merchant {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  width: 100%;
  padding: 6px;
  background: transparent;
  border: 0;
  border-radius: var(--radius-small);
  cursor: pointer;
  text-align: left;
  color: var(--text-secondary);
  font-family: var(--font-sans);
  transition: background 0.12s;
}
.nav-merchant:hover { background: var(--bg-elevated); }
.nav-merchant-wrap-open .nav-merchant { background: var(--brand-subtle); }
.nav-merchant-wrap-open .nav-merchant-text { color: var(--brand-pressed); }

.nav-merchant-name {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}

.nav-avatar {
  flex-shrink: 0;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--palette-purple-200);
  color: var(--text-on-brand);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  font-size: 11px;
  line-height: 1;
  letter-spacing: 0.005em;
}

/* ATT-256 chunk 2 — the same circle, now holding a photo instead of initials.
   The element is already round and already 26px; an image inside it needs to
   fill it and be cropped rather than sit letterboxed, which is the one thing
   the text version never needed. overflow lives on the parent, which has the
   radius. */
.nav-avatar { overflow: hidden; }
.nav-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.nav-merchant-text {
  font-weight: 500;
  font-size: 14px;
  line-height: 22px;
  color: var(--text-secondary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.nav-chevron {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  color: var(--text-secondary);
}

/* ─── Nav: merchant dropdown menu ─── */

/* ─── Shared: popover transition ───
   Reusable show/hide for dropdowns, menus, and popovers. Pair with the
   `menu` Stimulus controller (or any class toggle) — combine with a
   component class that owns position/size/background. */
.popover {
  opacity: 0;
  /* visibility, not just opacity: an opacity-0 element is still FOCUSABLE, so a
     closed menu kept every one of its items in the tab ring — tabbing past a
     category select walked through 20+ invisible options before reaching the
     next field. pointer-events fixed the mouse but not the keyboard.
     visibility:hidden removes them; it is transitioned alongside opacity so the
     fade is unchanged (it flips instantly on open, and at the END of the fade on
     close). Applies to every popover: select, multiselect, nav menu, kebabs. */
  visibility: hidden;
  transform: translateY(-4px);
  pointer-events: none;
  transition: opacity 0.14s ease, transform 0.14s ease, visibility 0.14s;
}
.popover-open {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  pointer-events: auto;
}

.nav-menu {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  width: 188px;
  background: var(--bg-surface);
  border: 0.5px solid var(--border-primary);
  border-radius: var(--radius-small);
  box-shadow: var(--shadow-container-elevated-bg);
  /* 6px / 2px, matching .ps-menu-dropdown. This was 12px / 10px, which suited
     flat unpadded rows; the items are padded now, so the container gives back
     the space the item took on. */
  padding: 6px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  z-index: 50;
}

.nav-menu-section {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-bottom: 6px;
  border-bottom: 0.5px solid var(--border-primary);
  width: 100%;
}
.nav-menu-section-last {
  padding-bottom: 0;
  border-bottom: 0;
}

/* Same padded row as .ps-menu-item: 8px 10px in an 8px-radius box, 8px between
   icon and label. It was a bare text row, which is why this menu read as a
   different component from every kebab in the app. Type, weight and colour were
   already identical. */
.nav-menu-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  border-radius: 8px;
  width: 100%;
  font-weight: 500;
  font-size: 12px;
  line-height: 18px;
  color: var(--text-secondary);
  text-decoration: none;
  transition: color 0.12s;
}
/* Hover → --text-link, active → --brand-pressed: the same shift the source-row
   menu uses, which is the Figma-derived one (nodes 1:1666 / 1:1686 — see
   .ps-menu-dropdown--grouped). This read as having NO hover at all, because
   --text-secondary → --text-primary is a barely-visible darkening on a menu that
   is already dark text on white. */
.nav-menu-item:hover  { color: var(--text-link); }
.nav-menu-item:active { color: var(--brand-pressed); }
.nav-menu-icon { width: 14px; height: 14px; flex-shrink: 0; color: currentColor; }

/* ─── Nav: items list ─── */

.nav-items-wrap {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 12px;
  min-height: 0;
}

.nav-items {
  display: flex;
  flex-direction: column;
  gap: 8px;
  list-style: none;
  padding: 0;
  margin: 0;
}

/* ATT-256 — a group boundary inside one nav list. The settings nav puts Profile
   (personal) above Brand / Brand voice / Manage members / Billing (the brand and
   its administration), separated by space rather than by a heading: a section
   header over a single item is worse than none, and there is only one personal
   item until a second arrives.

   ON THE ITEM THAT ENDS THE GROUP, not the one that starts the next, because
   what starts the next differs by role — an Admin's is Brand, a Member's is
   Manage members. Hanging it off the always-present first item makes the gap
   role-independent.

   16px ON TOP OF .nav-items' own 8px gap = 24px, which is the separation the
   approved mock drew.

   NOT admin/_nav's shape: that nav groups with two <ul>s and an .adm-navsection
   heading between them (.nav-items + .adm-navsection carries its spacing). Two
   lists cannot work here — .nav-items-wrap is justify-content: space-between, so
   a second <ul> would be pushed to the bottom of the rail rather than sitting
   under the first. */
.nav-group-end { margin-bottom: 16px; }

.nav-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  height: 30px;
  padding: 4px 12px;
  border-radius: 8px;
  border: 0.5px solid transparent;
  font-family:    var(--text-ui-nav-550-font-family);
  font-weight:    var(--text-ui-nav-550-font-weight);
  font-size:      var(--text-ui-nav-550-font-size);
  line-height:    var(--text-ui-nav-550-line-height);
  letter-spacing: var(--text-ui-nav-550-letter-spacing);
  color: var(--text-secondary);
  text-decoration: none;
  transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.nav-item:hover { background: var(--bg-elevated); }

.nav-item-active {
  background: var(--brand-subtle);
  border-color: var(--palette-purple-200);
  color: var(--brand-primary);
  box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.03), 0 3px 6px -2px rgba(0, 0, 0, 0.05);
}
.nav-item-active:hover { background: var(--brand-subtle); }

.nav-icon {
  width: 13px;
  height: 13px;
  flex-shrink: 0;
  color: currentColor;
}

/* ─── Nav: admin console ─── */

.nav-admin {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px;
  border-radius: var(--radius-small);
  background: var(--palette-neutrals-50);
  color: var(--brand-primary);
  text-decoration: none;
  font-family:    var(--text-ui-nav-550-font-family);
  font-weight:    var(--text-ui-nav-550-font-weight);
  font-size:      var(--text-ui-nav-550-font-size);
  line-height:    var(--text-ui-nav-550-line-height);
  letter-spacing: var(--text-ui-nav-550-letter-spacing);
  transition: background 0.12s;
}
.nav-admin:hover { background: var(--palette-neutrals-100); }
.nav-admin-icon { width: 16px; height: 16px; flex-shrink: 0; color: currentColor; }

/* ─── Nav: wordmark ─── */

.nav-wordmark {
  display: flex;
  justify-content: center;
  color: var(--brand-primary);
  padding: 24px 0;
}
.nav-wordmark svg {
  width: 136px;
  height: auto;
  display: block;
}

/* ─── Flash banner ─── */

.banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  min-height: 48px;
  padding: 6px 14px;
  border-bottom: 1px solid transparent;
  font-family: var(--font-sans);
  transition: opacity 0.3s ease;
}

.banner-text {
  display: flex;
  align-items: center;
  gap: 4px;
  font-weight: 400;
  font-size: 14px;
  line-height: 22px;
}
.banner-title { font-weight: 600; }
.banner-description { font-weight: 400; }

.banner-error {
  background: var(--status-error-default);
  color: var(--status-error-text);
  border-bottom-color: var(--status-error-subtle);
}
.banner-warning {
  background: var(--status-warning-default);
  color: var(--status-warning-subtle);
  border-bottom-color: var(--status-warning-subtle);
}
.banner-info {
  background: var(--status-info-default);
  color: var(--status-info-subtle);
  border-bottom-color: var(--status-info-subtle);
}
.banner-success {
  background: var(--status-success-default);
  color: var(--status-success-subtle);
  border-bottom-color: var(--status-success-subtle);
}

.banner-fading { opacity: 0; }

.banner-action {
  flex-shrink: 0;
  height: 32px;
  padding: 8px 18px;
  background: var(--palette-neutrals-white);
  border: 1px solid var(--palette-neutrals-200);
  border-radius: 14px;
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: 12px;
  line-height: 18px;
  color: var(--text-secondary);
  cursor: pointer;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.banner-action-wrap { display: flex; align-items: center; flex-shrink: 0; }
.banner-action-wrap form { margin: 0; }

/* ATT-105 — page-level error banner (products/_product_error_banner). The slot
   is always in the DOM so job errors can broadcast in/out; display: contents
   means the empty slot adds no box (no layout gap), and when populated the
   .banner is the direct flex child of .ps-content. Full-bleed: negative margins
   cancel .ps-content's 32px padding at top/left/right so the banner is flush to
   the panel edges (the panel's border-radius + overflow round its top corners);
   .ps-content's 20px flex gap spaces it from the header below. */
.ps-error-banner-slot { display: contents; }
/* width: auto (not the base .banner's width: 100%) so the flex item stretches
   and the negative margins extend it to BOTH panel edges; with width: 100% the
   box stays content-box-width and only shifts left, leaving a right-side gap. */
.ps-error-banner-slot .banner { width: auto; margin: -32px -32px 0; }

/* Fade-in on appearance (ATT-105), disabled under reduced-motion. */
@keyframes banner-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}
.banner { animation: banner-in 0.2s ease-out; }
@media (prefers-reduced-motion: reduce) {
  .banner { animation: none; }
}

/* ══════════════════════════════════════════
   Design System Components — Shared
   Source: Figma Design System (MZJ1sKIdafvU8UD1Wc08Xz)
   These map to Figma components:
     - Button - Label, Elevated  (node 162:166)  → .btn-elevated
     - Pill - Label              (node 164:472)  → .pill-label
     - Pill - Label w/ count     (node 234:1134) → .pill-label + .pill-count-badge
   ══════════════════════════════════════════ */

/* ─── Button: Elevated ─── */

.btn-elevated {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 36px;
  padding: 0 20px;
  border-radius: 14px;
  border: 0;
  font-family:    var(--text-btn-font-family);
  font-weight:    var(--text-btn-font-weight);
  font-size:      var(--text-btn-font-size);
  line-height:    var(--text-btn-line-height);
  letter-spacing: var(--text-btn-letter-spacing);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  box-shadow: var(--shadow-elevated-sm);
  transition: background 0.12s, border-color 0.12s, color 0.12s;
}

/* Sizes */
.btn-elevated-sm {
  height: 32px;
  padding: 0 20px;
  font-size: 12px;
  line-height: 18px;
}
.btn-elevated-xs {
  height: 24px;
  padding: 0 12px;
  border-radius: 10px;
  font-size: 11px;
  line-height: 16px;
  letter-spacing: 0.005em;
}

/* Label PLUS icon. ATT-242 — the filter trigger needed one and found the DS had
   no rule for it: .btn-icon-elevated is icon-ONLY, so a lucide dropped into a
   24px button rendered at its natural 24px and swallowed the label.

   Three surfaces had already each solved it locally and identically — the
   audiences pagination, the settings-billing pagination (byte-identical rules)
   and the OAuth stub. This is that rule, once, where a fourth caller will find
   it. The two pagination duplicates were deleted when it landed; their measured
   geometry is unchanged (14px icon, 8px to the label, same button width).

   CHILD COMBINATOR, DELIBERATELY. .ob-oauth's Google mark is a GRANDCHILD inside
   .ob-gmark and is sized 16px by its own rule; `>` leaves it alone. A descendant
   selector would have resized a third-party logo as a side effect of styling a
   filter button.

   Margins rather than a gap on the button, so the 300-odd label-only callers are
   untouched: a `gap` would have applied to any button that happens to have two
   element children, and the icon-bearing ones are the only ones this is for. */
.btn-elevated > svg { width: 16px; height: 16px; flex-shrink: 0; }
.btn-elevated > svg:first-child { margin-right: 8px; }
.btn-elevated > svg:last-child  { margin-left: 8px; }
/* Scaled to the label beside it. Equal specificity to the base rule, so these
   must stay BELOW it. */
.btn-elevated-sm > svg { width: 14px; height: 14px; }
.btn-elevated-xs > svg { width: 12px; height: 12px; }
.btn-elevated-xs > svg:first-child { margin-right: 4px; }
.btn-elevated-xs > svg:last-child  { margin-left: 4px; }

/* Destructive text link — button styled as a plain text link in the
   status-error color. Used by journey-row Delete (ATT-120) and any
   future inline destructive action. */
/* ── ATT-257: the generic text link ────────────────────────────────────────
   A LINK-LOOKING CONTROL, for an <a> OR a <button>. The button-reset half
   (display/padding/border/background/font) is inert on an anchor, so one class
   serves both — which matters because half the text links in this app are
   button_to forms rather than anchors: the ELEMENT is decided by whether the
   action is a GET or a POST, not by how it should look.

   GENERIC, NOT PREFIXED, and that is a deliberate reading of the prefix rule.
   CLAUDE.md's isolation rule is about LAYOUT clusters — index rows, page shells,
   "each index surface owns its own .prefix-* cluster". A text link is a generic
   component in the same family as .btn-*, .pill-* and .form-*, all of which are
   shared. Prefixing it produced four near-duplicates instead:

       .cr-banner-link    button reset + underline   (this rule, legacy name)
       .set-inline-link   color + underline          settings/billing/show
       .ob-link           color, NO underline        3 auth screens
       .e404-link         color, NO underline        BYTE-IDENTICAL to .ob-link

   .cr-banner-link IS KEPT AS A SELECTOR HERE rather than migrated: its six call
   sites (products/_product_error_banner, product_collections/_brief_alert_banner)
   keep working untouched, and there is one rule rather than two copies. Its
   declarations were already exactly these — nothing changed but the name it also
   answers to. Nothing else in the app targets .cr-banner-link (no hover rule),
   so moving it earlier in the cascade cannot lose a fight it was winning.

   NOT MIGRATED, deliberately: .set-inline-link is this minus the reset; .ob-link
   and .e404-link are this without the underline and are duplicates of each
   other. Three shipped surfaces in one sweep is a batch rewrite — do them when a
   feature next touches them.

   THE DESTRUCTIVE SIBLING is .btn-link-destructive below: same idea, error
   colour, its own font sizing, used as a button label rather than inline prose.
   Not folded in. */
.text-link,
.cr-banner-link {
  display: inline;
  padding: 0;
  border: 0;
  background: transparent;
  font: inherit;
  color: var(--text-link);
  text-decoration: underline;
  cursor: pointer;
}

/* HOVER REMOVES THE UNDERLINE, which is the inverse of the four other link
   classes in this file (.evidence-src-link, .cpt-thin-banner-link,
   .set-inv-link and .btn-link-destructive all ADD one on hover). Deliberate:
   those four are undecorated at rest, so the underline is what announces they
   are interactive at all. This one is underlined at rest, so the affordance is
   already made and hover has to change something else — dropping it is the
   change. Do not "align" this with the four; they are answering a different
   question. */
.text-link:hover,
.cr-banner-link:hover { text-decoration: none; }

.btn-link-destructive {
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  font-family:    var(--text-btn-font-family);
  font-weight:    var(--text-btn-font-weight);
  font-size:      12px;
  line-height:    18px;
  color: var(--status-error-text);
  white-space: nowrap;
  text-decoration: none;
}
/* Keeps its underline — that is this component's own affordance — and now takes
   the same colour shift as every other destructive control. */
.btn-link-destructive:hover { text-decoration: underline; color: var(--status-error-hover); }

/* Fill variants */
.btn-elevated-primary {
  background: var(--brand-primary);
  color: var(--palette-neutrals-white);
}
.btn-elevated-primary:hover   { background: var(--brand-hover); }
.btn-elevated-primary:active  { background: var(--brand-pressed); }

.btn-elevated-secondary {
  background: var(--brand-secondary);
  color: var(--palette-neutrals-white);
}
.btn-elevated-secondary:hover  { background: var(--palette-emerald-800); }
.btn-elevated-secondary:active { background: var(--palette-emerald-900); }

.btn-elevated-outline {
  background: var(--palette-neutrals-white);
  color: var(--text-secondary);
  border: 1px solid var(--palette-neutrals-300);
}
.btn-elevated-outline:hover  { border-color: var(--brand-hover); }
.btn-elevated-outline:active { border-color: var(--brand-pressed); }

/* ── Destructive ──
   Figma DS MZJ1sKIdafvU8UD1Wc08Xz, nodes 574:87 (Style=Filled, Fill=Red) and
   574:194 (Style=Outline, Fill=White), both Type=destructive.

   COLOUR ONLY, deliberately. Both nodes are Size=Default, and that size's
   geometry — 36px tall, 0 20px, 14px radius, 14/20 Medium, elevated shadow — is
   .btn-elevated's base value for value. So these compose with .btn-elevated-sm
   and -xs exactly as -primary and -outline do, instead of restating a size scale
   that would then have two places to drift.

   Every colour is an existing token whose repo value matches the node's:
     status/error/subtle   #dd524c  → --status-error-subtle
     palette/neutrals/white #ffffff → --palette-neutrals-white
     palette/neutrals/300  #d1d5db  → --palette-neutrals-300
   Nothing was transcribed; the hex above is only the audit trail.

   HOVER, NOT PRESSED. Both nodes are State=Default; --status-error-hover was
   added afterwards for the hover. There is still no pressed red, and :active is
   left unset rather than guessed — the same reason the hover waited. */
.btn-elevated-destructive {
  background: var(--status-error-subtle);
  color: var(--palette-neutrals-white);
}
.btn-elevated-destructive:hover { background: var(--status-error-hover); }
/* Identical to .btn-elevated-outline but for the label — same white fill, same
   neutrals-300 stroke. Kept as its own class rather than a modifier on that one
   so a caller states which it wants, and so a future hover state can differ. */
.btn-elevated-destructive-outline {
  background: var(--palette-neutrals-white);
  color: var(--status-error-text);
  border: 1px solid var(--palette-neutrals-300);
}
/* Here the red is the LABEL, so the hover moves the text rather than a fill.
   The border follows the same neutrals-300 → brand path .btn-elevated-outline
   uses, unchanged. */
.btn-elevated-destructive-outline:hover { color: var(--status-error-hover); }

.btn-elevated:disabled,
.btn-elevated[aria-disabled="true"] {
  background: var(--palette-neutrals-100);
  color: var(--palette-neutrals-400);
  border-color: var(--palette-neutrals-200);
  cursor: not-allowed;
  pointer-events: none;
}

/* ─── Button: Icon Elevated ─── */
/* Shared icon-button component. Figma "Button - Icon, Small, Elevated"
   (application instance 1:1639). Color tokens mirror the verified
   .btn-elevated-outline pattern from DS 162:166 (white bg, neutrals-300
   default border, brand hover/pressed). Default 24×24 matches the "Small"
   DS variant — introduce size modifiers (.btn-icon-elevated-md, etc.)
   when a second size appears. */
.btn-icon-elevated {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width:  24px;
  height: 24px;
  padding: 0;
  border-radius: 9999px;
  border: 1px solid var(--palette-neutrals-300);
  background: var(--palette-neutrals-white);
  color: var(--text-secondary);
  box-shadow: var(--shadow-elevated-sm);
  cursor: pointer;
  transition: border-color 0.12s, color 0.12s;
}
.btn-icon-elevated:hover  { border-color: var(--brand-hover);   color: var(--brand-hover);   }
.btn-icon-elevated:active { border-color: var(--brand-pressed); color: var(--brand-pressed); }
.btn-icon-elevated:disabled,
.btn-icon-elevated[aria-disabled="true"] {
  background: var(--palette-neutrals-100);
  color: var(--palette-neutrals-400);
  border-color: var(--palette-neutrals-200);
  cursor: not-allowed;
  pointer-events: none;
}
/* Lucide icon inside — uses currentColor so it follows the button text
   color across default/hover/pressed/disabled states. */
.btn-icon-elevated svg {
  width: 14px;
  height: 14px;
}

/* ─── Pill: Label (status) ─── */

.pill-label {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 22px;
  padding: 0 14px;
  border-radius: var(--radius-default);
  font-family:    var(--text-ui-badge-font-family);
  font-weight:    var(--text-ui-badge-font-weight);
  font-size:      var(--text-ui-badge-font-size);
  line-height:    var(--text-ui-badge-line-height);
  letter-spacing: var(--text-ui-badge-letter-spacing);
  white-space: nowrap;
  flex-shrink: 0;
}

.pill-label-brand   { background: var(--brand-subtle);           color: var(--brand-primary); }
.pill-label-warning { background: var(--status-warning-default); color: var(--status-warning-subtle); }
.pill-label-success { background: var(--status-success-default); color: var(--status-success-subtle); }
.pill-label-error   { background: var(--status-error-default);   color: var(--status-error-text); }
.pill-label-info    { background: var(--status-info-default);    color: var(--status-info-subtle); }
.pill-label-gray    { background: var(--palette-neutrals-200);   color: var(--palette-neutrals-500); }
.pill-label-muted   { background: var(--palette-neutrals-100);   color: var(--text-muted); }
/* ATT-141 — Products-tab priority pills (Figma 358:15555). Primary = solid
   brand; mentioned = light neutral. (Secondary reuses .pill-label-brand.) */
.pill-label-brand-solid { background: var(--brand-primary);       color: var(--brand-subtle); }
.pill-label-neutral     { background: var(--palette-neutrals-50); color: var(--palette-neutrals-700); }
/* ATT-137 — Outline gray pill variant for the Collections Index
   product pills (Figma 341:12311 / Pill instance status="GrayOutline").
   White surface, neutrals-300 border, neutrals-700 text.
   box-sizing: border-box keeps the 22px height consistent with
   the borderless pill variants in the cluster. */
.pill-label-gray-outline {
  background: var(--bg-surface);
  color: var(--palette-neutrals-700);
  border: 1px solid var(--palette-neutrals-300);
  box-sizing: border-box;
}

/* ATT-155 — Dimension key/value pill (Audiences Index, Figma 572:4097).
   Extends the .pill-label base (h22 / pad 0 14 / gap 8 / radius-default /
   UI-Badge type — all already matching the node) and adds the two-tone
   treatment the single-tone .pill-label-* variants don't cover: brand-subtle
   fill, key in secondary, value in brand. Used as
   `.pill-label.pill-kv > .pill-kv-key + .pill-kv-value`. */
.pill-kv       { background: var(--brand-subtle); }
.pill-kv-key   { color: var(--text-secondary); }
.pill-kv-value { color: var(--brand-primary); }

/* Elevated-background flag pills — used in node source-info rows for
   review-status indicators (Suggestion / Auto-Fixed / generic flag) and
   for the cta/reply prompt on check-in + handoff cards (ATT-125). */
.pill-flag {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 22px;
  padding: 0 14px;
  border-radius: var(--radius-default);
  background: var(--bg-elevated);
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: 11px;
  line-height: 16px;
  letter-spacing: 0.055px;
  white-space: nowrap;
}
.pill-flag-info      { color: var(--status-info-subtle); }
.pill-flag-success   { color: var(--status-success-subtle); }
.pill-flag-muted     { color: var(--text-muted); }
.pill-flag svg { width: 12px; height: 12px; flex-shrink: 0; }

/* Borderless condition pill — branch-card condition label
   ("Customer replied HELP"). No background, muted text, reply icon. */
.pill-condition {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 22px;
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: 11px;
  line-height: 16px;
  letter-spacing: 0.055px;
  color: var(--text-muted);
  white-space: nowrap;
}
.pill-condition svg { width: 16px; height: 16px; flex-shrink: 0; }
/* Brand-purple variant — the interactive reply/contact cue on check-in +
   handoff cards (branch condition keeps the default muted color). */
.pill-condition-brand { color: var(--brand-primary); }

/* ─── Pill: count badge (lives inside .pill-label) ─── */

.pill-count-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 6px;
  border-radius: 9999px;
  background: var(--palette-neutrals-white);
  border: 1px solid var(--palette-neutrals-200);
  font-family: var(--font-mono);
  font-weight: 400;
  font-size: 11px;
  line-height: 16px;
  color: var(--palette-neutrals-500);
}

/* ══════════════════════════════════════════
   Shared Tab Navigation (Linear-style segmented control).
   DS Figma MZJ1sKIdafvU8UD1Wc08Xz, node 255:2675.
   Consumers:
     - Product Show tab bar (default, content-width tabs).
     - Journey Show right rail (with .tab-nav--stretch, equal-width). [ATT-126]
   Active-state class is .is-active.
   ══════════════════════════════════════════ */
.tab-nav {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 6px 4px;
  background: var(--palette-neutrals-100);
  border-radius: var(--radius-small);
  /* No overflow:hidden here. The tab items are inset within the rounded
     corners so nothing bleeds; clipping made Blink drop the active pill's
     top/bottom border+shadow (leaving only L/R "tick" edges) once an
     ancestor scroll container (.ps-content) promotes to a composited
     layer — visible on long lists. ATT-157. */
}
.tab-nav-item {
  display: inline-flex;
  align-items: center;
  justify-content: flex-start;
  gap: 4px;
  height: 28px;
  padding: 0 14px;
  border-radius: var(--radius-small);
  background: transparent;
  border: 1px solid transparent;
  cursor: pointer;
  flex-shrink: 0;
  font-family:    var(--font-sans);
  font-weight:    500;
  font-size:      13px;
  line-height:    18px;
  color: var(--text-muted);
  text-decoration: none;
  transition: background-color 0.12s, color 0.12s, border-color 0.12s;
}
.tab-nav-item:hover { color: var(--text-secondary); }
.tab-nav-item.is-active {
  background: var(--bg-surface);
  border-color: var(--palette-neutrals-300);
  color: var(--text-primary);
  box-shadow: var(--shadow-container-elevated-bg);
}

/* Count badge inside a tab — colors shift with active state (overrides the
   .pill-count-badge defaults when nested in a tab). */
.tab-nav-item .pill-count-badge {
  background: var(--palette-neutrals-200);
  color: var(--palette-neutrals-500);
}
.tab-nav-item.is-active .pill-count-badge {
  background: var(--brand-subtle);
  color: var(--brand-primary);
}

/* Equal-width modifier — narrow contexts (rail, drawers). [ATT-126] */
.tab-nav--stretch              { display: flex; }
.tab-nav--stretch .tab-nav-item {
  flex: 1 1 0;
  justify-content: center;
}

/* ══════════════════════════════════════════
   Products Index (.pi-*)
   Page layout only; visual components above are shared.
   ATT-67
   ══════════════════════════════════════════ */

.pi-page {
  display: flex;
  flex-direction: column;
  gap: 40px;
  padding: 32px;
  min-height: 100%;
}

.pi-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding-bottom: 20px;
  border-bottom: 1px solid var(--border-primary);
}

.pi-title {
  color: var(--text-primary);
  margin: 0;
}

.pi-header-actions {
  display: flex;
  align-items: center;
  gap: 14px;
}

/* ATT-133 — the Index renders .brief-empty-card inside the flex-column
   .pi-page. The card's own `margin: 0 auto` (auto cross-axis margins) means
   flexbox does NOT stretch it, so it would shrink to its content width. Give
   it a definite width so it fills to its 584px max-width and centers — matching
   Figma 341:12667. Mirrors the .jli-page fix from ATT-121. Does not modify
   .brief-empty-card. */
.pi-page > .brief-empty-card { width: 100%; }

.pi-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.pi-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  width: 100%;
  padding: 16px;
  border-radius: 12px;
  border: 1px solid var(--border-primary);
  background: var(--bg-surface);
  transition: border-color 0.12s, box-shadow 0.12s;
}
.pi-row:hover {
  border-color: var(--palette-purple-300);
  box-shadow: var(--shadow-container-elevated-bg);
}
.pi-row:hover .btn-elevated-outline {
  border-color: var(--brand-hover);
}

.pi-row-main {
  display: flex;
  flex-direction: column;
  gap: 4px;
  flex: 1 1 0;
  min-width: 0;
}
.pi-row-title {
  color: var(--text-secondary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pi-row-category {
  color: var(--text-secondary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.pi-status-cell {
  display: flex;
  align-items: center;
  min-width: 160px;
  flex-shrink: 0;
}

.pi-row-date {
  min-width: 130px;
  flex-shrink: 0;
  color: var(--text-secondary);
}

/* ── Audiences Index (ATT-155) ─────────────────────────────────────────────
   Mirrors the Collections Index (.pc-*) layout: the page carries VERTICAL
   padding only, so the header's bottom border is full-bleed edge-to-edge; the
   32px horizontal gutter lives inside .aud-header and .aud-body. Own prefix
   (surface isolation); reuses generic .brief-empty-card / .pill-label /
   .btn-* / .text-* utilities. The stacked row (name / description / wrapping
   KV pills) + route-meta cell is the drill-proven gap vs the single-line
   .pc-row/.pi-row. */
.aud-page { padding: 32px 0; }
.aud-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 0 32px 20px;
  border-bottom: 1px solid var(--border-primary);
}
.aud-header h1 { color: var(--text-primary); margin: 0; }
.aud-header-actions { display: flex; align-items: center; gap: 14px; }
.aud-body { padding: 32px 32px 0; }
/* Square 48px audiences glyph in the empty state (Figma 522:4525) — overrides
   the collections box icon's 37×40; keeps .brief-empty-card-icon's color +
   flex-shrink. */
.brief-empty-card-icon.aud-empty-icon { width: 48px; height: 48px; }

.aud-list { display: flex; flex-direction: column; gap: 12px; }
.aud-row {
  display: flex;
  align-items: center;
  gap: 24px;
  padding: 20px 16px;
  border: 1px solid var(--palette-neutrals-200);
  border-radius: var(--radius-small);
  background: var(--bg-surface);
  text-decoration: none;
  color: inherit;
  transition: border-color 0.12s;
}
.aud-row:hover { border-color: var(--palette-purple-300); }
.aud-row-main {
  display: flex;
  flex-direction: column;
  gap: 6px;
  flex: 1 1 auto;
  min-width: 0;
}
.aud-row-name { color: var(--text-secondary); }
.aud-row-desc {
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.aud-row-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.aud-row-meta {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
  color: var(--text-muted);
}
.aud-row-meta-icon { width: 18px; height: 18px; }
.aud-row-count { color: var(--text-muted); white-space: nowrap; }
.aud-row-chevron { width: 24px; height: 24px; flex-shrink: 0; color: var(--text-muted); }

.pi-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 12px;
  min-width: 120px;
  flex-shrink: 0;
}

.pi-delete-form {
  display: inline-flex;
  margin: 0;
}
.pi-link-delete {
  background: transparent;
  border: 0;
  padding: 0;
  font-family:    var(--text-btn-small-font-family);
  font-weight:    var(--text-btn-small-font-weight);
  font-size: 12px;
  line-height: 18px;
  color: var(--status-error-text);
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
}
.pi-link-delete:hover {
  text-decoration: underline;
}

/* ══════════════════════════════════════════
   Lucide icon defaults
   ══════════════════════════════════════════ */

/* ATT-240 — the stroke weight moved to config/initializers/lucide.rb.
   It used to live here as `svg.lucide { stroke-width: 1.5 }` and keyed on a class
   the helper does not emit, so it reached 12 of 230 call sites and the other ~218
   drew at Lucide's default of 2. It is now a default option on the helper, which
   every call site gets and none can omit. Nothing needs re-adding here: CSS still
   beats the attribute, so a surface that genuinely needs a different weight can
   still say so, and .ls-chevron / .ls-history-icon already do. */

/* ══════════════════════════════════════════
   Product Show (.ps-*)
   ATT-76
   ══════════════════════════════════════════ */

body:has(.ps-page) {
  background: var(--bg-page);
  height: 100vh;
  overflow: hidden;
}

.ps-page {
  display: flex;
  gap: 10px;
  padding: 12px 12px 28px 16px;
  height: 100vh;
  font-family: var(--font-sans);
  color: var(--text-primary);
}

/* ─── Left rail ─── */

.ps-rail {
  width: 200px;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 16px 12px;
  height: 100%;
  overflow-y: auto;
}

.ps-rail-inner {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-height: 0;
}

.ps-rail-section {
  border-bottom: 1px solid var(--border-primary);
  padding: 8px 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.ps-rail-section:last-child { border-bottom: 0; }
/* ATT-114 — the insights slot is always rendered (so discovery can replace it
   live); when it is empty (pre-discovery) drop the preceding section's
   separator so the rail looks unchanged. */
.ps-rail-section:has(+ .ps-insights-slot--empty) { border-bottom: 0; }

.ps-rail-section-header {
  font-family:    var(--text-caption-bold-font-family);
  font-weight:    600;
  font-size:      12px;
  line-height:    18px;
  letter-spacing: 0;
  color: var(--palette-neutrals-500);
  text-transform: uppercase;
}
/* ATT-139 — a status pill placed in a rail section (Collections Show) hugs
   its content; the section is a stretch column, so without this the pill
   would stretch full-width. Minimal shared tweak, not a new cluster. */
.ps-rail-section > .pill-label { align-self: flex-start; }
/* 20px between the back link and the status pill (the default 8px section
   gap reads too tight). Scoped via :has to the section that contains a
   pill, so other rail sections are unaffected. */
.ps-rail-section:has(> .pill-label) { gap: 20px; }

/* Back link */
.ps-back {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 4px 0;
  color: var(--text-link);
  text-decoration: none;
  font-family:    var(--text-ui-nav-font-family);
  font-weight:    500;
  font-size:      13px;
  line-height:    18px;
}
.ps-back:hover { color: var(--brand-hover); }
.ps-back-icon { width: 18px; height: 18px; flex-shrink: 0; }

/* ─── Setup Progress checklist ─── */

.ps-checklist {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.ps-checklist-step {
  padding: 4px 0;
}

.ps-checklist-link {
  display: flex;
  gap: 6px;
  align-items: flex-start;
  padding: 6px 8px;
  border-radius: 12px;
  border: 1px solid transparent;
  text-decoration: none;
  color: inherit;
  transition: background 0.12s, border-color 0.12s, box-shadow 0.12s;
}

/* Default (None) styling per status — only :current renders as a card */
.ps-checklist-step-current .ps-checklist-link {
  background: var(--bg-surface);
  border-color: var(--palette-neutrals-300);
}

/* Hover states per status (matches Figma component 151:514).
   :done steps are non-interactive (rendered as a plain div, not a link —
   see _setup_checklist.html.erb), so they get no :hover rule.
   :ready is a dead state — the model only emits :done / :current / :pending
   since ATT-59 — so no :ready rule either. */
.ps-checklist-step-pending .ps-checklist-link:hover {
  background: var(--bg-elevated);
  border-color: var(--border-primary);
  box-shadow: var(--shadow-container-elevated-bg);
}
.ps-checklist-step-current .ps-checklist-link:hover {
  background: var(--bg-surface);
  border-color: var(--palette-purple-200);
}

.ps-checklist-status {
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 2px;
}
.ps-checklist-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 1px solid transparent;
  font-family: var(--font-sans);
  font-weight: 700;
  font-size: 9px;
  line-height: 14px;
}
/* :current shows the brand-tinted indicator circle (:ready was dropped — the
   model only emits :done / :current / :pending since ATT-59). */
.ps-checklist-step-current .ps-checklist-num {
  background: var(--brand-subtle);
  border-color: var(--brand-primary);
  color: var(--brand-primary);
}
.ps-checklist-step-pending .ps-checklist-num {
  background: var(--palette-neutrals-100);
  border-color: var(--palette-neutrals-400);
  color: var(--text-muted);
}
.ps-checklist-check {
  width: 16px;
  height: 16px;
  display: block;
}

.ps-checklist-body {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.ps-checklist-title {
  font-family:    var(--text-ui-nav-font-family);
  font-weight:    var(--text-ui-nav-font-weight);
  font-size:      var(--text-ui-nav-font-size);
  line-height:    var(--text-ui-nav-line-height);
  letter-spacing: var(--text-ui-nav-letter-spacing);
  color: var(--text-secondary);
}
.ps-checklist-step-pending .ps-checklist-title { color: var(--text-muted); }
.ps-checklist-sub {
  font-family:    var(--text-ui-label-small-font-family);
  font-weight:    var(--text-ui-label-small-font-weight);
  font-size:      var(--text-ui-label-small-font-size);
  line-height:    var(--text-ui-label-small-line-height);
  letter-spacing: var(--text-ui-label-small-letter-spacing);
  color: var(--text-muted);
}

/* ─── Insights summary ─── */

.ps-insights-summary {
  display: flex;
  flex-direction: column;
  width: 100%;
}
.ps-insights-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px 0;
  border-bottom: 1px solid var(--border-primary);
}
.ps-insights-row:last-child { border-bottom: 0; }
.ps-insights-label {
  font-family:    var(--text-ui-label-font-family);
  font-weight:    var(--text-ui-label-font-weight);
  font-size:      var(--text-ui-label-font-size);
  line-height:    var(--text-ui-label-line-height);
  letter-spacing: var(--text-ui-label-letter-spacing);
  color: var(--text-muted);
}
/* ATT-79 — plain count value (no circular .pill-count-badge per Figma
   5:1074). Tokens lifted from count node 125:2814: SF Mono Semibold 10/16,
   --palette-neutrals-500. No mono type-scale token exists, so size/weight are
   literal (consistent with .brief-data-value and the old badge). */
.ps-insights-count {
  font-family: var(--mono);
  font-weight: 600;
  font-size: 10px;
  line-height: 16px;
  color: var(--palette-neutrals-500);
}

/* ─── Wordmark ─── */

.ps-rail-wordmark {
  display: flex;
  justify-content: center;
  padding: 24px 0;
  flex-shrink: 0;
}
.ps-rail-wordmark svg {
  width: 136px;
  height: auto;
  display: block;
  color: var(--brand-primary);
}

/* ─── Content rail ─── */

.ps-content {
  flex: 1 1 0;
  min-width: 0;
  height: 100%;
  background: var(--bg-surface);
  border: 0.5px solid var(--palette-neutrals-100);
  border-radius: 12px;
  box-shadow: var(--shadow-container-elevated-bg);
  padding: 32px;
  display: flex;
  flex-direction: column;
  gap: 20px;
  overflow-y: auto;
}

/* ─── Product header ─── */

.ps-header {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex-shrink: 0;
  /* ATT-152 — full-bleed bottom divider, per the Figma "Product Header"
     (504:3993): border-b var(--border-primary) + pb-14, spanning the full
     content rail. Negative L/R margins cancel the .ps-content 32px padding so
     the stroke reaches the panel edges; the 32px L/R padding keeps the header
     content aligned. Shared with products (same Figma header component); the
     stale banner butts flush under this stroke. */
  margin: 0 -32px;
  padding: 0 32px 14px;
  border-bottom: 1px solid var(--border-primary);
}
.ps-header-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  min-height: 40px;
}
.ps-header-title-group {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  flex: 1 1 0;
}
.ps-header-title {
  font-family:    var(--text-h1-font-family);
  font-weight:    var(--text-h1-font-weight);
  font-size:      var(--text-h1-font-size);
  line-height:    var(--text-h1-line-height);
  letter-spacing: var(--text-h1-letter-spacing);
  color: var(--text-primary);
  margin: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}

/* Edit icon */
.ps-edit-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 6px;
  color: var(--text-secondary);
  background: transparent;
  border: 0;
  cursor: pointer;
  text-decoration: none;
  flex-shrink: 0;
}
.ps-edit-icon:hover {
  background: var(--bg-elevated);
  color: var(--text-primary);
}
.ps-edit-icon svg { width: 16px; height: 16px; }

/* Three-dot menu */
.ps-menu-wrap {
  position: relative;
  flex-shrink: 0;
}
.ps-menu-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 6px;
  border: 0;
  background: transparent;
  color: var(--text-secondary);
  cursor: pointer;
}
.ps-menu-button:hover {
  background: var(--bg-elevated);
  color: var(--text-primary);
}
.ps-menu-button[aria-expanded="true"] {
  background: var(--brand-subtle);
  border-radius: 50%;
  color: var(--text-secondary);
}
.ps-menu-button svg { width: 20px; height: 20px; }

.ps-menu-dropdown {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  min-width: 200px;
  background: var(--bg-surface);
  border: 0.5px solid var(--border-primary);
  border-radius: var(--radius-small);
  box-shadow: var(--shadow-container-elevated-bg);
  padding: 6px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  z-index: 50;
}

.ps-menu-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  border: 0;
  background: transparent;
  border-radius: 8px;
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: 12px;
  line-height: 18px;
  color: var(--text-secondary);
  text-decoration: none;
  cursor: pointer;
  text-align: left;
  width: 100%;
}
/* Lock the resting colour regardless of element type. A menu item is sometimes a
   <button> and sometimes an <a> (link_to), and the <a> would otherwise pick up
   the UA's link/visited colour. This lived on the --grouped variant until the
   base gained link items too. */
.ps-menu-item:visited { color: var(--text-secondary); }

/* One hover language across every menu in the app. This used to fill the row
   with --bg-elevated while the source-row menu (the Figma-derived one, nodes
   1:1666 / 1:1686) shifted colour instead — so the two kebabs a user meets
   minutes apart behaved differently. Colour shift wins because it is the one
   with a node behind it.
   The padding and radius stay: they set the hit target, which a colour-only
   hover still needs. */
.ps-menu-item:hover  { color: var(--text-link); }
.ps-menu-item:active { color: var(--brand-pressed); }

/* Destructive: --status-error-subtle at rest, --status-error-hover on hover.
   :active is deliberately unset — there is a hover token now but no pressed one,
   and guessing a third red is what this token was added to stop. */
.ps-menu-item-danger        { color: var(--status-error-text); }
.ps-menu-item-danger:hover  { color: var(--status-error-hover); }
.ps-menu-icon {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  color: currentColor;
}

/* button_to inside menu wraps in a form — neutralize */
.ps-menu-dropdown form { margin: 0; display: contents; }

/* ATT-198 — right-anchored variant. The base opens RIGHTWARD from the trigger
   (left: 0), which is correct where the kebab sits beside an H1 on the left of
   the page — the audience / collection / journey headers. On an INDEX ROW the
   kebab is flush against the content's right edge, so a rightward menu runs off
   the viewport: the admin brand rows clipped "View" and "Delete" entirely.

   Anchoring to the trigger's right edge makes it open leftward, back into the
   page. Same trick .ps-menu-dropdown--grouped already uses for the same reason;
   this is it without the grouped layout. Any future index row with a kebab wants
   this modifier, not a per-surface override. */
.ps-menu-dropdown--right {
  left: auto;
  right: 0;
}

/* ─── ATT-77 Chunk 4: grouped variant ───
   Opt-in modifier .ps-menu-dropdown--grouped switches the dropdown to the
   Figma 1:1587 "source menu" layout — three visual groups (Open/Copy,
   Edit/etc., Remove) separated by 0.5px horizontal divider lines, items
   flush-padded inside a 12px container with 12px inter-item gap. Existing
   product-header three-dot menu does NOT use this modifier and keeps its
   current flat-list styling.

   Composition:
     <div class="popover ps-menu-dropdown ps-menu-dropdown--grouped">
       <div class="ps-menu-group ps-menu-group--separated"> … items … </div>
       <div class="ps-menu-group ps-menu-group--separated"> … items … </div>
       <div class="ps-menu-group">                          … items … </div>
     </div>
   The --separated modifier draws the divider line + adds the 12px padding
   below the group; omit it on the last group. */
.ps-menu-dropdown--grouped {
  /* Container padding and gap now come from the base (6px / 2px). They were
     12px / 10px here to give flat, unpadded rows room to breathe; with padded
     rows that spacing stacks on top of the item's own and the menu grows huge. */
  /* Anchor to the kebab's right edge so the menu opens leftward and stays
     on-screen when the row sits near the viewport right boundary. Overrides
     the .ps-menu-dropdown base's `left: 0`. */
  left: auto;
  right: 0;
}
.ps-menu-group {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.ps-menu-group--separated {
  padding-bottom: 6px;
  border-bottom: 0.5px solid var(--border-primary);
}
/* The variant no longer restyles the ITEM at all. It used to flatten it
   (padding: 0) and redeclare four state rules to undo the base's background
   fill; now every menu in the app shares one padded item and one hover
   language, so this modifier is down to what it is actually for — dividing
   items into groups. */
/* Inert (deferred) items render identically to wired items per Figma —
   there is no disabled-state styling. `aria-disabled="true"` remains the
   accessibility signal; clicks are simply no-ops until later chunks wire
   the handlers / routes. */

/* Source-row kebab — give the trigger a visible "open" state to match
   .ps-menu-button[aria-expanded="true"] in the product header menu. */
.src-row-kebab[aria-expanded="true"] {
  background: var(--brand-subtle);
  color: var(--text-secondary);
}
.src-row-kebab-wrap {
  position: relative;
  flex-shrink: 0;
}

/* Header actions container */
.ps-header-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-shrink: 0;
}
.ps-header-actions form { margin: 0; }

/* Subrow (category + URL) */
.ps-subrow {
  display: flex;
  align-items: center;
  gap: 10px;
}
.ps-subrow-category {
  font-family:    var(--text-ui-label-font-family);
  font-weight:    var(--text-ui-label-font-weight);
  font-size:      var(--text-ui-label-font-size);
  line-height:    var(--text-ui-label-line-height);
  letter-spacing: var(--text-ui-label-letter-spacing);
  color: var(--text-muted);
}
.ps-subrow-url {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family:    var(--text-ui-label-font-family);
  font-weight:    var(--text-ui-label-font-weight);
  font-size:      var(--text-ui-label-font-size);
  line-height:    var(--text-ui-label-line-height);
  letter-spacing: var(--text-ui-label-letter-spacing);
  color: var(--brand-primary);
  text-decoration: none;
}
.ps-subrow-url:hover { text-decoration: underline; }
.ps-subrow-url-icon { width: 10px; height: 10px; flex-shrink: 0; }

/* ─── Tab bar ─── */

/* Tab content lets the existing tab partials live underneath unchanged */
.ps-tab-content {
  flex: 1 1 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

/* ─── Modal (shared shell) ─── */

.ps-modal-backdrop {
  position: fixed;
  inset: 0;
  background: var(--bg-tint);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 200;
  padding: 24px;
}
.ps-modal {
  background: var(--bg-surface);
  border: 0.5px solid var(--border-primary);
  border-radius: 14px;
  box-shadow: var(--shadow-container-elevated-bg);
  width: 100%;
  max-width: 600px;
  max-height: 90vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
/* Opt-in width modifier, the same shape as .focus-content-wide and
   .ob-card-wide: the default measure stands, and a surface that genuinely needs
   more asks for it. 880px is not a new number — it is .ob-card-wide's, arrived
   at in ATT-205 for exactly this content (a teammate on one line: two name
   fields, an email, a role and a remove). Reuse it rather than picking a second
   "wide".

   Passed through shared/_modal as `wide: true`; default false, so no existing
   modal moves. */
.ps-modal-wide { max-width: 880px; }
.ps-modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 40px 60px 20px;
}
.ps-modal-title {
  font-family:    var(--text-h1-font-family);
  font-weight:    var(--text-h1-font-weight);
  font-size:      var(--text-h1-font-size);
  line-height:    var(--text-h1-line-height);
  letter-spacing: var(--text-h1-letter-spacing);
  color: var(--text-primary);
  margin: 0;
}
.ps-modal-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  background: transparent;
  border-radius: 6px;
  color: var(--text-muted);
  text-decoration: none;
  cursor: pointer;
}
.ps-modal-close:hover {
  background: var(--bg-elevated);
  color: var(--text-secondary);
}
.ps-modal-close svg { width: 20px; height: 20px; }
.ps-modal-body {
  padding: 0 60px 40px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.ps-modal-footer {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  padding-top: 4px;
}

/* Source Details modal — read-only label/value list. Two new classes only;
   row values reuse .disco-title / .disco-url / .pill-label-* / .disco-score
   for typography & visual treatment. The default <dd> rule below provides
   body-text styling for any value (e.g. the rationale paragraph) that
   doesn't carry its own typography class — so the modal doesn't need a
   third class for the rationale row. */
.source-details-list {
  display: flex;
  flex-direction: column;
  gap: 16px;
  margin: 0;
}
.source-details-list dd {
  margin: 0;
  font-family:    var(--text-body-font-family);
  font-weight:    var(--text-body-font-weight);
  font-size:      var(--text-body-font-size);
  line-height:    var(--text-body-line-height);
  letter-spacing: var(--text-body-letter-spacing);
  color: var(--text-primary);
}
.source-details-label {
  font-family:    var(--text-caption-font-family);
  font-weight:    var(--text-caption-font-weight);
  font-size:      var(--text-caption-font-size);
  line-height:    var(--text-caption-line-height);
  letter-spacing: var(--text-caption-letter-spacing);
  color: var(--text-muted);
  margin-bottom: 6px;
}

.ps-modal-desc {
  font-family: var(--font-sans);
  font-size: 13px;
  line-height: 20px;
  color: var(--text-muted);
  margin: 0;
}

.ps-modal-form {
  display: flex;
  flex-direction: column;
  gap: 16px;
  margin: 0;
}
.ps-modal-radio-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.ps-modal-radio-option {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border: 1.5px solid var(--border-primary);
  border-radius: 10px;
  cursor: pointer;
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: 13px;
  line-height: 20px;
  color: var(--text-secondary);
}
.ps-modal-radio-option:hover { border-color: var(--palette-neutrals-300); }
.ps-modal-radio-label { flex: 1 1 0; }
.ps-modal-radio-meta {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-muted);
}

/* Reusable custom radio button (Figma 28:3911 / node 28:4334). Apply
   class="radio-custom" to an <input type="radio"> — replaces the native
   accent-color radio app-wide. Unselected: neutrals-300 outline. Selected:
   purple-400 ring (Figma "border/focus" #967fee) + a brand-primary center dot
   rendered via a radial-gradient background. Tokens only. */
.radio-custom {
  appearance: none;
  -webkit-appearance: none;
  width: 18px;
  height: 18px;
  margin: 0;
  flex-shrink: 0;
  border: 1.5px solid var(--palette-neutrals-300);
  border-radius: 50%;
  background: var(--bg-surface);
  cursor: pointer;
  transition: border-color 0.12s, background 0.12s;
}
.radio-custom:checked {
  border-color: var(--palette-purple-400);
  background: radial-gradient(circle at center, var(--brand-primary) 0 4px, var(--bg-surface) 5px 100%);
}
.radio-custom:focus-visible {
  outline: 2px solid var(--palette-purple-400);
  outline-offset: 2px;
}

/* ══════════════════════════════════════════
   Focused single-rail page (.focus-*)
   ATT-104 — reusable shell: bg-page backdrop, centered elevated white rail
   with a header back link. See shared/_focused_rail.html.erb.
   ══════════════════════════════════════════ */
body:has(.focus-page) { background: var(--bg-page); }

.focus-page {
  display: flex;
  align-items: stretch;
  min-height: 100vh;
  padding: 12px 12px 28px 16px;
  font-family: var(--font-sans);
  color: var(--text-primary);
}
.focus-rail {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
  padding-bottom: 32px;
  background: var(--bg-surface);
  border: 0.5px solid var(--palette-neutrals-100);
  border-radius: var(--radius-small);
  box-shadow: var(--shadow-container-elevated-bg);
}
.focus-rail-header {
  width: 100%;
  display: flex;
  align-items: center;
  padding: 14px 32px;
  border-bottom: 1px solid var(--border-primary);
}
.focus-rail-back {
  display: flex;
  align-items: center;
  gap: 4px;
  color: var(--text-link);
  text-decoration: none;
  font-family: var(--text-ui-nav-font-family);
  font-weight: 500;
  font-size: 13px;
  line-height: 18px;
}
.focus-rail-back:hover { color: var(--brand-hover); }
.focus-rail-back-icon { width: 18px; height: 18px; flex-shrink: 0; }
.focus-content {
  width: 540px;
  display: flex;
  flex-direction: column;
  gap: 32px;
}
/* Opt-in wide variant (ATT-156): the audiences/new form needs 600px to fit the
   two-column dimension rows. Reusable via focused_rail's content_class local;
   collections (pins its own 540) and products/new (inherits 540) don't opt in,
   so they're unaffected. */
.focus-content.focus-content-wide { width: 600px; }

/* ── Audiences new/edit form (ATT-156) ─────────────────────────────────────
   Isolated aud-form-* scaffolding mirroring the collections focused-rail form
   treatment (.pcf-card / .pcf-section-eyebrow / .pcf-actions) — own prefix so
   audiences doesn't depend on the collections form. Fields reuse the generic
   .form-* / .optional / .btn-* components (no new input CSS). The genuinely-new
   UI is the repeatable dimension row. */
.aud-form { display: flex; flex-direction: column; gap: 32px; }
.aud-form-title { margin: 0; }
.aud-form-card {
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-small);
  background: var(--bg-surface);
  padding: 32px 20px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.aud-form-eyebrow {
  padding-bottom: 14px;
  font-family: var(--font-sans);
  font-weight: 600;
  font-size: 12px;
  line-height: 18px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}
.aud-form-actions {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 10px;
}
/* Repeatable dimension rows — the new UI (Stimulus `dimensions`). Two flex
   .form-input columns + a subtle X remove; "Add dimension" is the compact
   elevated-outline button. */
.aud-dim-rows { display: flex; flex-direction: column; gap: 8px; }
.aud-dim-row { display: flex; align-items: center; gap: 8px; }
.aud-dim-row .form-input { flex: 1 1 0; min-width: 0; }
.aud-dim-remove {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
}
.aud-dim-remove:hover { color: var(--status-error-subtle); }
.aud-dim-remove svg { width: 16px; height: 16px; }
.aud-dim-add { margin-top: 8px; align-self: flex-start; }

/* ── Audiences Show (ATT-157) ─────────────────────────────────────────────
   Two-rail show reuses .ps-* shell + .ps-menu-* kebab + .tab-nav + .pill-kv +
   .pill-label + .input-affix + .btn-elevated*. Genuinely new (drill-proven,
   Figma 566:5539): the left-rail stat/sources blocks, the content eyebrow +
   description, the dimensions row, the consumer table, the journey-sentiment
   trait pill, and the pagination footer. Token-only, except email/phone use
   the mono family token at 13/18 (no 13px --text-data token exists). */

/* Left rail — stat block + membership sources (inside reused .ps-rail-section) */
.aud-rail-stat { display: flex; flex-direction: column; }
.aud-rail-count {
  font-family:    var(--text-h1-font-family);
  font-weight:    var(--text-h1-font-weight);
  font-size:      var(--text-h1-font-size);
  line-height:    var(--text-h1-line-height);
  letter-spacing: var(--text-h1-letter-spacing);
  color: var(--text-primary);
}
.aud-rail-count-label { font-family: var(--font-sans); font-size: 12px; line-height: 18px; color: var(--text-muted); }
.aud-rail-meta { font-family: var(--font-sans); font-weight: 500; font-size: 12px; line-height: 18px; color: var(--text-primary); }
.aud-source-row {
  display: flex; align-items: center; justify-content: space-between;
  padding: 8px 0; border-bottom: 1px solid var(--border-primary);
}
.aud-source-row:last-child { border-bottom: 0; }
.aud-source-label { font-family: var(--font-sans); font-weight: 500; font-size: 12px; line-height: 16px; color: var(--text-muted); }
.aud-source-count { font-family: var(--font-mono); font-weight: 600; font-size: 10px; line-height: 16px; color: var(--palette-neutrals-500); }

/* Content — eyebrow, description, dimensions row */
.aud-block { display: flex; flex-direction: column; gap: 8px; }
.aud-eyebrow {
  font-family: var(--font-sans); font-weight: 600; font-size: 12px; line-height: 18px;
  text-transform: uppercase; color: var(--text-muted);
}
.aud-desc {
  font-family:    var(--text-body-small-font-family);
  font-weight:    var(--text-body-small-font-weight);
  font-size:      var(--text-body-small-font-size);
  line-height:    var(--text-body-small-line-height);
  color: var(--text-primary); margin: 0;
}
.aud-dims-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }

/* Consumers tab — toolbar + table + pagination */
.aud-consumers { display: flex; flex-direction: column; }
.aud-import-form { display: contents; }
.aud-consumers-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding-bottom: 10px; }
.aud-showing, .aud-page-info { font-family: var(--font-sans); font-weight: 400; font-size: 12px; line-height: 18px; color: var(--text-muted); }
.aud-toolbar-actions { display: flex; align-items: center; gap: 12px; }
.aud-search { width: 260px; }

.aud-ct { width: 100%; border-collapse: collapse; table-layout: fixed; }
.aud-ct col.c-consumer { width: 228px; }
.aud-ct col.c-email    { width: 228px; }
.aud-ct col.c-phone    { width: 200px; }
.aud-ct col.c-source   { width: 110px; }
.aud-ct col.c-x        { width: 50px; }
.aud-ct th {
  font-family: var(--font-sans); font-weight: 600; font-size: 12px; line-height: 18px;
  text-transform: uppercase; color: var(--text-muted);
  text-align: left; padding: 10px; vertical-align: middle;
  border-bottom: 1px solid var(--border-primary);
}
.aud-ct td { padding: 12px 10px; vertical-align: middle; border-bottom: 1px solid var(--palette-neutrals-100); transition: background-color 0.12s; }
.aud-ct tr:last-child td { border-bottom: 0; }
/* Row hover — bg/elevated fill (Figma 573:4602 "row - hover"). Set on the
   cells rather than the <tr> because border-collapse hides a <tr> background
   behind the collapsed borders. */
.aud-ct tbody tr:hover td { background: var(--bg-elevated); }
.aud-c-name { font-family: var(--font-sans); font-weight: 600; font-size: 13px; line-height: 20px; color: var(--text-primary); }
.aud-c-mono {
  display: block; font-family: var(--font-mono); font-weight: 500; font-size: 13px; line-height: 18px;
  color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.aud-c-email { color: var(--brand-primary); }
.aud-c-empty { font-family: var(--font-mono); font-size: 13px; color: var(--text-muted); }
.aud-trait-pill {
  display: inline-flex; align-items: center; height: 22px; padding: 0 8px; border-radius: 8px;
  background: var(--palette-neutrals-100); color: var(--text-muted); white-space: nowrap;
  font-family: var(--font-sans); font-weight: 500; font-size: 11px; line-height: 16px; letter-spacing: 0.005em;
}
.aud-c-x { text-align: right; }
.aud-remove-btn {
  display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px;
  padding: 0; border: 0; background: none; color: var(--text-muted); cursor: pointer;
}
.aud-remove-btn:hover { color: var(--status-error-subtle); }
.aud-remove-btn svg { width: 16px; height: 16px; }

.aud-pagination { display: flex; align-items: center; justify-content: flex-end; gap: 16px; padding-top: 8px; }
.aud-page-info { flex: 1 1 0; }
/* ATT-242 — .aud-page-btn's `gap: 8px` + `svg { 14px }` moved onto
   .btn-elevated-sm, where the settings pagination had an identical copy. */

.aud-tab-stub {
  padding: 40px 0; text-align: center;
  font-family: var(--text-body-small-font-family); font-size: var(--text-body-small-font-size);
  line-height: var(--text-body-small-line-height); color: var(--text-muted);
}

/* ══════════════════════════════════════════
   Add Brand page (.ab-*)
   ATT-166 — renders inside the .focus-* shell, mirroring the Add Product
   (.ap-*) / audiences (.aud-form-*) focused-rail form treatment. Own prefix so
   the Brand form doesn't depend on the Products one (same reasoning as the
   aud-form-* cluster above).

   This is layout scaffolding ONLY — page header, section cards, action row.
   Every field reuses the generic .form-field / .form-label / .form-input /
   .form-select / .form-textarea / .form-hint components and the .btn-elevated
   buttons; no new input or button CSS. Values mirror the existing clusters
   rather than introducing new spacing.

   Known DS gap: there is no file-input component anywhere in the design system,
   so the brand-guidelines upload renders as a bare browser file input inside a
   .form-field. Flagged rather than invented — see ATT-166 report.
   ══════════════════════════════════════════ */
.ab-header { display: flex; flex-direction: column; gap: 2px; }
.ab-title {
  font-family:    var(--text-h1-font-family);
  font-weight:    var(--text-h1-font-weight);
  font-size:      var(--text-h1-font-size);
  line-height:    var(--text-h1-line-height);
  letter-spacing: var(--text-h1-letter-spacing);
  color: var(--text-primary);
  margin: 0;
}
.ab-subhead {
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: 12px;
  line-height: 16px;
  letter-spacing: 0.5px;
  color: var(--text-muted);
  margin: 0;
}
.ab-form { display: flex; flex-direction: column; gap: 32px; }
.ab-section {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 18px;
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-small);
}
.ab-section-eyebrow {
  font-family: var(--font-sans);
  font-weight: 600;
  font-size: 12px;
  line-height: 18px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}
.ab-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
  padding-bottom: 42px;
}

/* ══════════════════════════════════════════
   Add Product page (.ap-*)
   ATT-104 — Figma 181:2824. Renders inside the .focus-* shell.
   ══════════════════════════════════════════ */
.ap-header { display: flex; flex-direction: column; gap: 2px; }
.ap-title {
  font-family:    var(--text-h1-font-family);
  font-weight:    var(--text-h1-font-weight);
  font-size:      var(--text-h1-font-size);
  line-height:    var(--text-h1-line-height);
  letter-spacing: var(--text-h1-letter-spacing);
  color: var(--text-primary);
  margin: 0;
}
.ap-subhead {
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: 12px;
  line-height: 16px;
  letter-spacing: 0.5px;
  color: var(--text-muted);
  margin: 0;
}
.ap-form-wrap { display: flex; flex-direction: column; gap: 32px; }
.ap-form {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 18px;
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-small);
}
.ap-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 14px 0 42px;
}
.ap-actions-caption {
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: 13px;
  line-height: 20px;
  color: var(--text-secondary);
  margin: 0;
}
.ap-actions-caption strong { font-weight: 700; }
.ap-actions-row {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
}
.ap-next {
  display: flex;
  flex-direction: column;
  padding: 18px;
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-default);
}
.ap-next-title {
  font-family: var(--font-sans);
  font-weight: 600;
  font-size: 12px;
  line-height: 18px;
  color: var(--text-muted);
  text-transform: uppercase;
  padding-bottom: 10px;
}
.ap-next-list { display: flex; flex-direction: column; gap: 4px; }
.ap-next-step { display: flex; align-items: flex-start; gap: 6px; padding: 4px 0; }
.ap-next-num {
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  margin-top: 2px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--brand-primary);
  border-radius: 9999px;
  font-family: var(--font-sans);
  font-weight: 700;
  font-size: 9px;
  line-height: 14px;
  color: var(--brand-primary);
}
.ap-next-body { display: flex; flex-direction: column; justify-content: center; }
.ap-next-step-title {
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: 12px;
  line-height: 16px;
  color: var(--text-secondary);
}
.ap-next-step-desc {
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: 10px;
  line-height: 12px;
  color: var(--text-muted);
}

/* ══════════════════════════════════════════
   DS form controls — custom select / multiselect (.control, .ctl-*)
   ATT-119. Visual source: attune-form-components.html.
   Menu = .popover (transition) + .select-menu (position/surface).
   Open state on the trigger's [aria-expanded="true"].
   ══════════════════════════════════════════ */
.control { position: relative; }
.ctl-trigger {
  width: 100%; height: 36px; display: flex; align-items: center; gap: 8px;
  padding: 0 10px 0 12px; border: 1px solid var(--border-primary);
  border-radius: var(--radius-small); background: var(--bg-elevated);
  font-family: var(--font-sans); font-size: 14px; color: var(--text-primary);
  cursor: pointer; text-align: left;
}
.ctl-trigger .val { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ctl-trigger .val.placeholder { color: var(--text-muted); }
.ctl-trigger .chev { color: var(--text-muted); display: flex; transition: transform 0.15s; }
.ctl-trigger .chev svg { width: 14px; height: 14px; }
.ctl-trigger[aria-expanded="true"] {
  border-color: var(--border-focus); background: var(--bg-surface);
  box-shadow: 0 0 0 3px var(--brand-subtle);
}
.ctl-trigger[aria-expanded="true"] .chev { transform: rotate(180deg); }
.ctl-trigger:focus-visible { outline: 2px solid var(--brand-primary); outline-offset: 2px; }
.ctl-compact .ctl-trigger { height: 32px; font-size: 13px; }

.select-menu {
  position: absolute; top: calc(100% + 6px); left: 0; right: 0;
  background: var(--bg-surface); border: 0.5px solid var(--border-primary);
  border-radius: var(--radius-small); box-shadow: var(--shadow-container-elevated-bg);
  padding: 6px; z-index: 50; max-height: 260px; overflow: auto;
}
.opt {
  display: flex; align-items: center; gap: 8px; width: 100%; padding: 8px 10px;
  border: 0; background: transparent; border-radius: 8px;
  font-family: var(--font-sans); font-size: 14px; color: var(--text-secondary);
  cursor: pointer; text-align: left;
}
.opt[hidden] { display: none; }
.opt:hover, .opt.active { background: var(--bg-elevated); color: var(--text-primary); }
.opt .check { width: 16px; height: 16px; flex-shrink: 0; color: var(--brand-primary); opacity: 0; }
.opt[aria-selected="true"] .check { opacity: 1; }
.opt[aria-selected="true"] { color: var(--text-primary); font-weight: 500; }
.opt .sub { display: block; font-size: 12px; color: var(--text-muted); font-weight: 400; }
.opt-empty { padding: 10px; font-size: 13px; color: var(--text-muted); text-align: center; }

.pop-search { display: flex; align-items: center; gap: 8px; padding: 6px 8px; margin-bottom: 4px; border-bottom: 0.5px solid var(--border-primary); }
.pop-search svg { width: 15px; height: 15px; color: var(--text-muted); flex-shrink: 0; }
.pop-search input { flex: 1; border: 0; outline: none; background: transparent; font-family: var(--font-sans); font-size: 14px; color: var(--text-primary); }

.pop-foot { display: flex; gap: 10px; padding: 8px 8px 4px; margin-top: 4px; border-top: 0.5px solid var(--border-primary); }
.kbd { font-family: var(--mono); font-size: 11px; color: var(--text-muted); }
.kbd b { display: inline-block; border: 1px solid var(--border-primary); border-radius: 4px; padding: 0 4px; color: var(--text-secondary); font-weight: 500; }

.ctl-multi { min-height: 36px; height: auto; flex-wrap: wrap; padding: 5px 10px 5px 6px; gap: 5px; }
.chip { display: inline-flex; align-items: center; gap: 5px; height: 24px; padding: 0 4px 0 9px; border-radius: 8px; background: var(--brand-subtle); color: var(--brand-primary); font-size: 12px; font-weight: 500; }
.chip button { border: 0; background: none; cursor: pointer; color: var(--brand-primary); display: flex; padding: 2px; border-radius: 5px; opacity: 0.7; }
.chip button:hover { opacity: 1; background: rgba(76, 2, 232, 0.12); }
.chip button svg { width: 12px; height: 12px; }
.ctl-multi .ms-placeholder { color: var(--text-muted); font-size: 14px; align-self: center; padding-left: 6px; }

/* ══════════════════════════════════════════
   DS form controls — char count, switch, segmented, listbox, optional suffix
   ATT-119 Chunk 2. Visual source: attune-form-components.html.
   Switch / segmented / listbox use native :checked selectors so no JS runs.
   ══════════════════════════════════════════ */

/* Field head row (label + meta/count side by side) */
.form-field .field-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}
.field-count {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.field-count.over { color: var(--status-error-text); }

/* Optional-label suffix (per Decision Log 2026-06-11: muted "Optional",
   no asterisks for required) */
.form-label .optional {
  font-weight: 400;
  letter-spacing: 0;
  color: var(--text-muted);
  margin-left: 6px;
}

/* ── Switch — native <input type="checkbox"> styled as a switch ── */
.switch-wrap {
  display: inline-flex;
  align-items: center;
  cursor: pointer;
}
.switch-input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
  pointer-events: none;
}
.switch {
  position: relative;
  flex-shrink: 0;
  width: 40px;
  height: 24px;
  border-radius: 999px;
  background: var(--palette-neutrals-300, #D7DAE0);
  transition: background 0.18s ease;
  display: inline-block;
}
.switch::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
  transition: transform 0.18s ease;
}
.switch-input:checked + .switch { background: var(--brand-primary); }
.switch-input:checked + .switch::after { transform: translateX(16px); }
.switch-input:disabled + .switch { opacity: 0.5; cursor: not-allowed; }
.switch-input:focus-visible + .switch {
  outline: 2px solid var(--brand-primary);
  outline-offset: 2px;
}
.switch-sm { width: 34px; height: 20px; }
.switch-sm::after { width: 16px; height: 16px; }
.switch-input:checked + .switch-sm::after { transform: translateX(14px); }

/* ── Segmented control — native radios ── */
.segmented {
  display: inline-flex;
  gap: 3px;
  padding: 3px;
  background: var(--bg-elevated);
  border-radius: 10px;
  border: 1px solid var(--border-primary);
}
.seg {
  position: relative;
  display: inline-flex;
  align-items: center;
  cursor: pointer;
}
.seg-input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
  pointer-events: none;
}
.seg-label {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 7px;
  height: 28px;
  padding: 0 14px;
  font-family: var(--font-sans);
  font-size: 13px;
  font-weight: 500;
  color: var(--text-muted);
  transition: background 0.12s, color 0.12s;
}
.seg-input:checked + .seg-label {
  background: var(--bg-surface);
  color: var(--text-primary);
  box-shadow: var(--shadow-elevated, var(--shadow-container-elevated-bg));
}
.seg-input:focus-visible + .seg-label {
  outline: 2px solid var(--brand-primary);
  outline-offset: 2px;
}
.seg-input:disabled + .seg-label { opacity: 0.55; cursor: not-allowed; }

/* ── Inline listbox — native radios with descriptions ── */
.listbox {
  display: flex;
  flex-direction: column;
  gap: 2px;
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-small);
  padding: 6px;
  background: var(--bg-surface);
}
.lb-item {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px;
  border-radius: 8px;
  cursor: pointer;
}
.lb-item:hover { background: var(--bg-elevated); }
.lb-input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
  pointer-events: none;
}
.lb-radio {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 1.5px solid var(--border-primary);
  flex-shrink: 0;
  margin-top: 1px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
}
.lb-input:checked + .lb-radio { border-color: var(--brand-primary); }
.lb-input:checked + .lb-radio::after {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--brand-primary);
}
.lb-input:focus-visible + .lb-radio {
  outline: 2px solid var(--brand-primary);
  outline-offset: 2px;
}
.lb-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.lb-title {
  font-size: 14px;
  font-weight: 500;
  color: var(--text-primary);
  line-height: 18px;
}
.lb-sub {
  font-size: 12px;
  color: var(--text-muted);
  line-height: 16px;
}

/* ══════════════════════════════════════════
   Shared Journey List Item (.jli-*)
   ATT-120. Figma 282:9716. Used by:
     - app/views/journeys/index.html.erb
     - app/views/products/tabs/_journeys_content.html.erb
     - app/views/product_collections/show.html.erb
   Reuses .pill-label*, .btn-elevated-xs, .btn-link-destructive.
   ══════════════════════════════════════════ */
.jli-page {
  display: flex;
  flex-direction: column;
  gap: 32px;
  padding: 32px;
  min-height: 100%;
}
.jli-page-header {
  padding-bottom: 20px;
  border-bottom: 1px solid var(--border-primary);
}
.jli-page-header .text-h1 { color: var(--text-primary); margin: 0; }

.jli-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* ATT-121 — the Index renders .brief-empty-card inside the flex-column
   .jli-page. The card's own `margin: 0 auto` (auto cross-axis margins) means
   flexbox does NOT stretch it, so it would shrink to its content width. Give
   it a definite width so it fills to its 584px max-width and centers — matching
   Figma 288:10804 (centered 584px card). Does not modify .brief-empty-card. */
.jli-page > .brief-empty-card { width: 100%; }

/* One fixed track per metadata column + a flexible title track. Because every
   row shares the same template, each column lines up vertically down the list
   no matter how wide an individual cell's content is (the old flex layout let
   variable node-count / audience-name widths shift the columns per row). Track
   widths come from Figma 282:9838 (target 200, date 130, actions 120), widened
   where real data is longer than the spec's single example (nodes fit "26
   Nodes"; audience fits "Amazon New Buyers"; status fits "Published"). */
.jli-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 200px 90px 150px 96px 130px 120px;
  align-items: center;
  column-gap: 20px;
  padding: 16px 20px;
  background: var(--bg-surface);
  border: 1px solid var(--palette-neutrals-200);
  border-radius: 12px;
  transition: border-color 0.12s;
}
.jli-row:hover { border-color: var(--palette-purple-300); }
.jli-row:hover .btn-elevated-outline { border-color: var(--brand-hover); }

/* ATT-161 — Audiences Journeys tab row. The audience row drops the target +
   audience columns (Figma 573:4824: name · nodes · status · date · View), so
   this compact grid replaces those two fixed tracks. The list_item partial omits
   both cells when `compact: true`; every other surface keeps the 7-track grid. */
.jli-row-compact {
  grid-template-columns: minmax(0, 1fr) 90px 96px 130px 120px;
}

.jli-cell-title { min-width: 0; }
.jli-title {
  display: block;
  font-family:    var(--text-body-bold-font-family);
  font-weight:    var(--text-body-bold-font-weight);
  font-size:      var(--text-body-bold-font-size);
  line-height:    var(--text-body-bold-line-height);
  letter-spacing: var(--text-body-bold-letter-spacing);
  color: var(--text-secondary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.jli-cell-target {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.jli-target-icon {
  width: 13px;
  height: 13px;
  flex-shrink: 0;
  color: var(--text-muted);
  stroke: currentColor;
}
.jli-target-icon-filled { fill: currentColor; stroke: none; }
.jli-target-name {
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: 13px;
  line-height: 18px;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Nodes / audience / status: flex so the inline-flex content (route+count, or a
   content-hugging pill) sits at the column's left edge within its fixed track. */
.jli-cell-nodes,
.jli-cell-audience,
.jli-cell-status {
  display: flex;
  align-items: center;
  min-width: 0;
}
.jli-nodes {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-family: var(--mono);
  font-size: 12px;
  line-height: 16px;
  color: var(--text-muted);
}
.jli-nodes svg { width: 18px; height: 18px; }

.jli-cell-date {
  text-align: right;
  font-family: var(--mono);
  font-size: 12px;
  line-height: 16px;
  color: var(--text-muted);
}

.jli-cell-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 12px;
}
.jli-delete-form { display: inline-flex; margin: 0; }

/* ══════════════════════════════════════════
   DS form controls — text input variants + inline label/tip helpers
   ATT-122. Visual source: attune-form-components-2.html.
   Pairs with the ATT-85 .form-* base; reuses tokens already in :root.
   ══════════════════════════════════════════ */

/* Leading-icon input. Wrap a .form-input with .input-affix and place
   the icon in .lead; the rule shifts the input's left padding to clear
   the icon. */
.input-affix {
  position: relative;
  display: flex;
  align-items: center;
}
.input-affix .lead {
  position: absolute;
  left: 11px;
  display: flex;
  color: var(--text-muted);
  pointer-events: none;
}
.input-affix .lead svg { width: 16px; height: 16px; }
.input-affix .form-input { padding-left: 34px; }

/* Static focus-state demo — mirrors .form-input:focus so the styleguide can
   show the focus treatment without interaction. Dev-preview-only sugar:
   .is-focus is used solely inside the styleguide. */
.is-focus .form-input {
  outline: none;
  border-color: var(--border-focus);
  background: var(--bg-surface);
  box-shadow: 0 0 0 3px var(--brand-subtle);
}

/* URL / mono-prefix input. The wrapper carries the border + focus ring
   (via :focus-within) so the prefix and input read as one control; the
   inner <input> is unstyled. */
.input-prefix {
  display: flex;
  align-items: center;
  height: 36px;
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-small);
  background: var(--bg-elevated);
  overflow: hidden;
}
.input-prefix:focus-within {
  border-color: var(--border-focus);
  background: var(--bg-surface);
  box-shadow: 0 0 0 3px var(--brand-subtle);
}
.input-prefix .pfx {
  padding: 0 10px;
  font-family: var(--mono);
  font-size: 13px;
  color: var(--text-muted);
  border-right: 1px solid var(--border-primary);
  height: 100%;
  display: flex;
  align-items: center;
}
.input-prefix input {
  flex: 1;
  border: 0;
  background: transparent;
  outline: none;
  padding: 0 12px;
  font-family: var(--font-sans);
  font-size: 14px;
  color: var(--text-primary);
  min-width: 0;
}
/* The inner input is unstyled (no .form-input), so it needs its own empty-state
   placeholder color to match the muted treatment of the base inputs. */
.input-prefix input::placeholder { color: var(--text-muted); }

/* Form tip — short icon-prefixed helper text below a field. Brand-primary
   icon; muted text. Use sparingly for actionable guidance (not for
   validation — that's .form-hint with .form-field-error). */
.form-tip {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  font-family: var(--font-sans);
  font-size: 12px;
  line-height: 16px;
  color: var(--text-muted);
}
.form-tip svg {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  margin-top: 1px;
  color: var(--brand-primary);
}

/* Field meta — mono inline suffix next to the label (node IDs, status). */
.field-meta {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0;
  color: var(--text-muted);
}

/* ══════════════════════════════════════════
   Journey Show header (.jh-*)
   ATT-124. Figma 295:11576. Renders inside .shell-content.
   Kebab reuses the .popover / .popover-open + menu_controller pattern.
   ══════════════════════════════════════════ */
.jh-header {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 14px 32px;
  border-bottom: 1px solid var(--border-primary);
}
.jh-eyebrow { display: flex; align-items: center; gap: 4px; }
.jh-back {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: var(--brand-primary);
  text-decoration: none;
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: 13px;
  line-height: 18px;
}
.jh-back:hover { color: var(--brand-hover); }
.jh-back-icon { width: 18px; height: 18px; flex-shrink: 0; }

.jh-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}

.jh-title-group {
  display: flex;
  align-items: center;
  gap: 16px;
  min-width: 0;
}
.jh-title-with-kebab {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.jh-title {
  font-family:    var(--text-h1-font-family);
  font-weight:    var(--text-h1-font-weight);
  font-size:      var(--text-h1-font-size);
  line-height:    var(--text-h1-line-height);
  letter-spacing: var(--text-h1-letter-spacing);
  color: var(--text-primary);
  margin: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.jh-kebab-wrap { position: relative; display: inline-flex; }
.jh-kebab-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  background: transparent;
  border-radius: 9999px;
  color: var(--text-secondary);
  cursor: pointer;
  transition: background-color 0.12s, color 0.12s;
}
.jh-kebab-btn:hover { background: var(--bg-elevated); color: var(--text-primary); }
.jh-kebab-btn[aria-expanded="true"] {
  background: var(--brand-subtle);
  color: var(--text-secondary);
}
.jh-kebab-icon { width: 20px; height: 20px; }

/* The collections + journeys kebab DROPDOWN + items now reuse the shared
   Product Show menu (.ps-menu-dropdown / .ps-menu-item / .ps-menu-item-danger /
   .ps-menu-icon) so their hover states match products/show. Only the kebab
   trigger (.jh-kebab*), the divider, and the form reset remain jh-scoped. */
.jh-menu-divider {
  border-bottom: 1px solid var(--border-primary);
  width: 100%;
  /* small margin since the divider now lives in the tighter .ps-menu-dropdown
     (gap 2px) rather than the old .jh-menu (gap 10px). */
  margin: 4px 0;
}
.jh-menu-form { margin: 0; display: block; width: 100%; }

.jh-controls {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-shrink: 0;
}

/* ══════════════════════════════════════════
   Journey Node cards (.jn-*)
   ATT-125. Figma 239:7216. Replaces the old .jc-node* / .jc-day /
   .jc-goal-badge / .jc-content / .jc-cta / .jc-branch-tag /
   .jc-review-badge family.
   ══════════════════════════════════════════ */

.jn-stack {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  width: 100%;
}

/* Day pill — sits above the card and overlaps the top edge by 11px. */
.jn-day-wrap {
  display: flex;
  justify-content: center;
  margin-bottom: -11px;
  position: relative;
  z-index: 2;
}
.jn-day {
  display: inline-flex;
  align-items: center;
  height: 22px;
  padding: 0 14px;
  background: var(--palette-neutrals-500);
  color: var(--text-on-primary, #fff);
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: 11px;
  line-height: 16px;
  letter-spacing: 0.055px;
  border-radius: var(--radius-default);
  white-space: nowrap;
}

/* Card base — white surface, 12px radius, 6px left border + 1px elsewhere.
   Default border color is neutrals-200 (muted). */
.jn-card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--palette-neutrals-white);
  border: 1px solid var(--palette-neutrals-200);
  border-left-width: 6px;
  border-radius: var(--radius-small);
  box-shadow: var(--shadow-container-elevated-bg);
  cursor: pointer;
  transition: border-color 0.12s, box-shadow 0.15s;
  user-select: none;
}
.jn-card:hover  { border-color: var(--palette-purple-200); }
.jn-card.selected {
  border-color: var(--border-focus);
  box-shadow: 0 0 0 3px var(--brand-subtle), var(--shadow-container-elevated-bg);
}

/* Per-type left border (muted defaults: message + check-in + handoff use
   neutrals-200; only branch and continue carry type-specific colors). */
.jn-card-branch   { border-color: var(--node-branch, #e9a23b); }
.jn-card-continue {
  border-color: var(--palette-emerald-100);
  background: var(--bg-elevated);
}
/* Branch + continue share a grid row — make both cards fill the row height so
   their bottoms align and the merge connector meets each cleanly (no break). */
.jc-branch-grid .jn-card { flex: 1 1 auto; }
/* Continue card centers its content vertically within the (stretched) card. */
.jn-card-continue .jn-inner { flex: 1; justify-content: center; }

.jn-inner {
  display: flex;
  flex-direction: column;
  padding: 16px;
  gap: 0;
}

/* Eyebrow ("MESSAGE", "CHECK IN", "BRANCH", "HANDOFF"). */
.jn-header {
  font-family:    var(--text-caption-bold-font-family, var(--font-sans));
  font-weight:    600;
  font-size:      12px;
  line-height:    18px;
  letter-spacing: 0;
  color: var(--text-muted);
  text-transform: uppercase;
  margin-bottom: 0;
}

/* Body text — Body/Default (14/22) on every node type. */
.jn-body {
  font-family:    var(--text-body-font-family);
  font-weight:    var(--text-body-font-weight);
  font-size:      var(--text-body-font-size);
  line-height:    var(--text-body-line-height);
  letter-spacing: var(--text-body-letter-spacing);
  color: var(--text-primary);
  margin: 0;
  padding-top: 0;
  padding-bottom: 12px;
}

/* Source-info pill row at the bottom of the card (rows can stack). */
.jn-source-info {
  display: flex;
  align-items: center;
  gap: 20px;
}
/* Condition row sits ABOVE the body (branch only). */
.jn-condition-row { padding-bottom: 4px; }
/* Stacked source-info rows (check-in reply / handoff contact → pills row). */
.jn-source-info + .jn-source-info { padding-top: 12px; }

/* Continue node — centered icon + text block. */
.jn-continue-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 42px;
  margin: 0 auto 8px;
  border-radius: 9999px;
  background: var(--palette-emerald-100);
  color: var(--text-on-accent, #065f46);
}
.jn-continue-icon svg { width: 24px; height: 24px; }
.jn-continue-title {
  font-family:    var(--text-caption-bold-font-family);
  font-weight:    var(--text-caption-bold-font-weight);
  font-size:      var(--text-caption-bold-font-size);
  line-height:    var(--text-caption-bold-line-height);
  letter-spacing: var(--text-caption-bold-letter-spacing);
  text-transform: uppercase;
  text-align: center;
  color: var(--text-on-accent, #065f46);
}
.jn-continue-sub {
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: 14px;
  line-height: 22px;
  text-align: center;
  color: var(--text-primary);
}
.jn-continue-condition {
  display: flex;
  justify-content: center;
  margin-bottom: 4px;
}

/* .jc-stage hosts a flex row: canvas-area (left) + the always-visible rail
   (right) — ATT-126. Dot-grid background carried from ATT-125 (Figma 295:11385). */
.jc-stage {
  display: flex;
  align-items: stretch;
  justify-content: space-between;
  padding: 20px 32px 32px;
  flex: 1 1 0;
  min-height: 0;
  background-image: radial-gradient(circle, var(--palette-neutrals-200) 1px, transparent 1px);
  background-size: 24px 24px;
  background-position: top left;
}
.jc-canvas-area {
  flex: 1 1 0;
  min-width: 0;
  padding-right: 16px;
  display: flex;
  flex-direction: column;
  align-items: center;
  overflow-y: auto;
}
.jc-canvas { padding: 0; }
/* Connector stroke color (SVG paths use stroke="currentColor", 2px). */
.jc-svg-connector { color: var(--palette-neutrals-500); }

/* ══════════════════════════════════════════
   Focused layout — single-rail full-bleed surface.
   Used for deep edit / canvas views (journey show is the first
   consumer). Pairs with app/views/layouts/focused.html.erb.
   ══════════════════════════════════════════ */
body.focused-layout {
  min-height: 100vh;
  margin: 0;
  background: var(--bg-page);
  padding: 12px 12px 28px 16px;
  display: flex;
  flex-direction: column;
  font-family: var(--font-sans);
}
.focused-rail {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--bg-surface);
  border: 0.5px solid var(--palette-neutrals-100);
  border-radius: var(--radius-small);
  box-shadow: var(--shadow-container-elevated-bg);
  /* ATT-126 wired inner scroll (.jc-canvas-area + .jc-rail-panel each scroll
     independently), so the rail itself no longer scrolls. */
  overflow: hidden;
}

/* ══════════════════════════════════════════
   Journey Show contextual rail (.jc-rail / .jc-suggestion*)
   ATT-126. Figma 237:6048. Always-visible 380px rail with
   Node Details | Suggestions tabs. Pairs with journey_canvas_controller.
   Tab nav uses the shared .tab-nav family from ATT-129.
   ══════════════════════════════════════════ */
.jc-rail {
  width: 380px;
  flex-shrink: 0;
  padding-left: 16px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.jc-rail-panel {
  flex: 1 1 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 24px;
  padding: 16px 14px;
  background: var(--bg-surface);
  border-radius: var(--radius-small);
  /* 1px border drawn as an inset ring rather than a layout border: under
     border-box a real border would subtract 2px from the content, leaving
     cards at 334px. The panel is 364px and Figma expects 336px content/cards
     (364 - 14px gutters * 2), so the ring keeps the full 336px. */
  box-shadow: var(--shadow-container-elevated-bg), inset 0 0 0 1px var(--border-primary);
  /* The panel itself does not scroll — the tab nav stays anchored at the top
     while only the tab panels scroll (see .jc-tab-panels). */
  overflow: hidden;
}

/* Scroll container — the tab nav sits above this and stays fixed. */
.jc-tab-panels { display: flex; flex-direction: column; flex: 1 1 0; min-height: 0; overflow-y: auto; }
/* flex 1 0 auto: fills the panel when content is short (so the Node Details
   empty state can center), but grows to content height when tall so the
   .jc-tab-panels scroll kicks in. */
.jc-tab-panel  { display: flex; flex-direction: column; flex: 1 0 auto; min-height: 0; }
.jc-tab-panel[hidden] { display: none; }

.jc-rail-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 48px 24px;
  gap: 12px;
  flex: 1 1 0;
}
/* The element's own display:flex would otherwise win over the `hidden`
   attribute's UA display:none — keep the hidden empty state actually hidden
   (server-rendered for the "all suggestions dismissed" case, revealed by JS). */
.jc-rail-empty[hidden] { display: none; }
.jc-rail-empty-icon  { width: 42px; height: 42px; }
.jc-rail-empty-title {
  font-family:    var(--text-body-bold-font-family);
  font-weight:    var(--text-body-bold-font-weight);
  font-size:      var(--text-body-bold-font-size);
  line-height:    var(--text-body-bold-line-height);
  letter-spacing: var(--text-body-bold-letter-spacing);
  color: var(--text-primary);
  margin: 0;
}
.jc-rail-empty-desc {
  font-family:    var(--text-body-small-font-family);
  font-weight:    var(--text-body-small-font-weight);
  font-size:      var(--text-body-small-font-size);
  line-height:    var(--text-body-small-line-height);
  letter-spacing: var(--text-body-small-letter-spacing);
  color: var(--text-muted);
  margin: 0;
}

.jc-suggestions-list { display: flex; flex-direction: column; }

/* Suggestion item (Figma 227:3776 → item 232:4017). */
.jc-suggestion {
  border-bottom: 1px solid var(--border-primary);
  padding: 20px 0;
  display: flex;
  flex-direction: column;
}
.jc-suggestion:first-child { padding-top: 10px; }
.jc-suggestion:last-child  { border-bottom: 0; }
.jc-suggestion[hidden]     { display: none; }

.jc-suggestion-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-bottom: 10px;
}
.jc-suggestion-dismiss {
  width: 16px; height: 16px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.jc-suggestion-dismiss:hover { color: var(--text-primary); }
.jc-suggestion-dismiss svg   { width: 11px; height: 11px; }

.jc-suggestion-body {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding-bottom: 4px;
}
.jc-suggestion-title {
  font-family:    var(--text-h3-font-family);
  font-weight:    var(--text-h3-font-weight);
  font-size:      var(--text-h3-font-size);
  line-height:    var(--text-h3-line-height);
  letter-spacing: var(--text-h3-letter-spacing);
  color: var(--text-primary);
  margin: 0;
}
.jc-suggestion-desc {
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: 13px;
  line-height: 20px;
  color: var(--text-primary);
  margin: 0;
}

.jc-suggestion-nodes {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  padding: 4px 0 10px;
}
/* Clickable node pill — button-reset so .pill-label-gray styles apply. */
.jc-node-pill {
  border: 0;
  cursor: pointer;
  font-family: inherit;
  transition: background-color 0.12s;
}
.jc-node-pill:hover {
  background: var(--palette-neutrals-300);
}

/* Suggested action (informational; no click). Figma 320:12112 — a
   "Recommendation" header (icon + label) above the full-width copy, so long
   recommendation text wraps cleanly on its own line. */
.jc-suggestion-action {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  padding-top: 8px;
}
.jc-suggestion-action-header {
  display: flex;
  align-items: center;
  width: 100%;
}
.jc-suggestion-action-icon  { width: 20px; height: 20px; flex-shrink: 0; }
.jc-suggestion-action-label {
  flex: 1 1 0;
  min-width: 0;
  font-family:    var(--text-h4-font-family);
  font-weight:    var(--text-h4-font-weight);
  font-size:      var(--text-h4-font-size);
  line-height:    var(--text-h4-line-height);
  letter-spacing: var(--text-h4-letter-spacing);
  color: var(--text-secondary);
}
.jc-suggestion-action-text {
  font-family:    var(--text-body-small-font-family);
  font-weight:    var(--text-body-small-font-weight);
  font-size:      var(--text-body-small-font-size);
  line-height:    var(--text-body-small-line-height);
  letter-spacing: var(--text-body-small-letter-spacing);
  color: var(--text-primary);
  margin: 0;
}

/* ══════════════════════════════════════════
   Collections form (.pcf-*) — ATT-138 (Figma 377:4822).
   Focused-layout collections new/edit form. Sits inside
   .focus-content (yielded by shared/_focused_rail). Fixed
   540px content column per Figma. Uses canonical .form-*
   + .btn-elevated* + .input-affix DS components. The
   product picker rows reuse .form-select for the priority
   dropdown, with a .pcf-priority modifier for the compact
   size (132x32 vs base 36).
   ══════════════════════════════════════════ */
.pcf-header {
  width: 540px;
  margin: 0 auto;
  padding-top: 32px;
  padding-bottom: 0;
}
.pcf-title {
  margin: 0;
  font-family:    var(--text-h1-font-family);
  font-weight:    var(--text-h1-font-weight);
  font-size:      var(--text-h1-font-size);
  line-height:    var(--text-h1-line-height);
  letter-spacing: var(--text-h1-letter-spacing);
  color: var(--text-primary);
}
#collection-form {
  width: 540px;
  margin: 0 auto;
  /* No top padding — the shared .focus-content gap (32px) already provides
     the title → first-section spacing (Figma 377:4829: 32px). A top padding
     here would double it to 64px. */
  padding-bottom: 32px;
  display: flex;
  flex-direction: column;
  gap: 32px;
}
/* Product-selection errors — inline .form-hint-error lines under the
   SELECT PRODUCTS eyebrow (Figma 413:3433). No banner; stacked tight. */
.pcf-selection-errors {
  display: flex;
  flex-direction: column;
}
.pcf-card {
  border: 1px solid var(--palette-neutrals-200);
  border-radius: var(--radius-small);
  background: var(--bg-surface);
  padding: 32px 20px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.pcf-section {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.pcf-section-eyebrow {
  padding-bottom: 14px;
  font-family: var(--font-sans);
  font-weight: 600;
  font-size: 12px;
  line-height: 18px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}
.pcf-picker-search { margin-bottom: 0; }
/* Cap the rows area and scroll within it so the page height stays stable
   with many products (the search above stays pinned; the footer stays
   reachable). ~6 rows visible before scrolling. max-height is tunable. */
.pcf-rows {
  display: flex;
  flex-direction: column;
  gap: 8px;
  max-height: 400px;
  overflow-y: auto;
}
/* ATT-142 — keep the Update-products overlay a constant height while filtering.
   The picker frame's contents swap on every search (rows ↔ fewer rows ↔ empty
   state); sizing to content would resize and re-center the modal (jarring). In
   the modal, make the frame a fixed-height scroll area (~7 rows per Figma
   368:233) and defer the inner .pcf-rows scroll to it, so the overlay height
   never changes. Only scoped to the modal — the focused new/edit form keeps its
   natural-height picker. */
.ps-modal-body turbo-frame#collection_product_picker {
  display: block;
  height: 340px;
  overflow-y: auto;
}
.ps-modal-body turbo-frame#collection_product_picker .pcf-rows {
  max-height: none;
  overflow: visible;
}
.pcf-empty {
  padding: 20px 16px;
  border: 1px dashed var(--palette-neutrals-200);
  border-radius: var(--radius-small);
  color: var(--text-muted);
  font-size: 13px;
  text-align: center;
}
.pcf-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 16px;
  border: 1px solid var(--palette-neutrals-200);
  border-radius: var(--radius-small);
  background: var(--bg-surface);
  transition: border-color 0.12s;
}
.pcf-row:hover { border-color: var(--palette-purple-300); }
.pcf-row.pcf-row-checked { border-color: var(--brand-primary); }
.pcf-row-left {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 250px;
  min-width: 0;
  cursor: pointer;
}
/* Product name + category stacked vertically (Figma 377:5182):
   name on top, category below. */
.pcf-row-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.pcf-row-title {
  min-width: 0;
  font-family:    var(--text-body-bold-font-family);
  font-weight:    var(--text-body-bold-font-weight);
  font-size:      var(--text-body-bold-font-size);
  line-height:    var(--text-body-bold-line-height);
  color: var(--text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pcf-row-right {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-shrink: 0;
}
.pcf-row-category {
  font-size: 12px;
  line-height: 18px;
  color: var(--palette-neutrals-500);
  letter-spacing: 0.01em;
}

/* Custom checkbox rendered as a 16px circle. Native input
   visually hidden but kept in the DOM for focus + a11y. The
   adjacent .pcf-check-visual paints the circle; .pcf-check-icon
   is the lucide check, hidden by default and revealed when the
   checkbox is :checked. */
.pcf-check {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  border: 0;
}
.pcf-check-visual {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 1px solid var(--palette-neutrals-300);
  background: var(--bg-surface);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  transition: background-color 0.12s, border-color 0.12s;
}
.pcf-check:checked + .pcf-check-visual {
  background: var(--brand-primary);
  border-color: var(--brand-primary);
}
.pcf-check-icon {
  width: 10px;
  height: 10px;
  color: var(--bg-surface);
  opacity: 0;
  transition: opacity 0.08s;
}
.pcf-check:checked + .pcf-check-visual .pcf-check-icon {
  opacity: 1;
}
.pcf-check:focus-visible + .pcf-check-visual {
  outline: 2px solid var(--border-focus);
  outline-offset: 2px;
}

/* Compact priority select — Figma is 132x32, smaller than the
   base .form-select (h-36). Same neutrals-100 fill, rounded-12,
   chevron background image. Inherits other properties from
   .form-select. */
.pcf-priority {
  width: 132px;
  height: 32px;
  font-size: 12px;
  padding: 0 32px 0 13px;
}

.pcf-actions {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 10px;
  padding-top: 14px;
}

/* ══════════════════════════════════════════
   Product Status — left-rail progress component (ATT-139, Figma 427:3581;
   states 427:3535 incomplete / 427:3496 complete). The ONLY new component
   cluster this chunk — Collections Show otherwise reuses .ps-* (shell),
   .jh-* (kebab), .tab-nav (tabs), .pill-label, and .btn-elevated. Neutral
   prefix (.rail-progress*) so it can be reused on other collection/group
   rail surfaces. Sits inside a .ps-rail-section. Tokens only.
   ══════════════════════════════════════════ */
.rail-progress-summary {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 7px 0;
}
.rail-progress-count {
  color: var(--text-primary);
  letter-spacing: -0.24px;
}
/* "N of M": the counts are H1-scale (24/32 semibold), the "of" is 14px medium
   (Figma 427:3541). */
.rail-progress-count-num {
  font-family: var(--font-sans);
  font-weight: 600;
  font-size: 24px;
  line-height: 32px;
}
.rail-progress-count-of {
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: 14px;
  line-height: 20px;
}
.rail-progress-label {
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: 12px;
  line-height: 18px;
  letter-spacing: 0.12px;
  color: var(--text-muted);
}
.rail-progress-bar {
  width: 100%;
  height: 6px;
  background: var(--palette-neutrals-200);
  border-radius: var(--radius-default);
  overflow: hidden;
}
.rail-progress-bar-fill {
  height: 100%;
  /* Runtime percent arrives as the --rail-progress-pct custom property set on
     the element (data only); color/height/radius stay here. */
  width: var(--rail-progress-pct, 0%);
  border-radius: var(--radius-default);
}
.rail-progress-bar-fill--complete { background: var(--status-success-subtle); }
.rail-progress-bar-fill--partial  { background: var(--status-warning-subtle); }

.rail-progress-list {
  display: flex;
  flex-direction: column;
}
.rail-progress-row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 7px 0;
  border-bottom: 1px solid var(--border-primary);
}
.rail-progress-row:last-child { border-bottom: 0; }
/* 10px dot; nudged to align with the name's first line (18px line-height). */
.rail-progress-dot {
  width: 10px;
  height: 10px;
  margin-top: 4px;
  border-radius: 50%;
  flex-shrink: 0;
}
.rail-progress-dot--ready  { background: var(--status-success-subtle); }
.rail-progress-dot--needed {
  background: transparent;
  border: 1.5px solid var(--status-warning-subtle);
}
.rail-progress-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.rail-progress-name {
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: 12px;
  line-height: 18px;
  letter-spacing: 0.12px;
  color: var(--text-primary);
}
.rail-progress-sub {
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: 12px;
  line-height: 18px;
  letter-spacing: 0.12px;
  color: var(--text-muted);
}
.rail-progress-sub--needed {
  font-weight: 500;
  color: var(--status-warning-subtle);
}

/* ══════════════════════════════════════════
   Collections Show — Products tab (.cpt-*) — ATT-141 (Figma 353:15201 happy
   / 358:15555 unhappy). The Index .pc-row lives inline in
   product_collections/index.html.erb (not global), so this row is its own
   cluster; it REUSES .pill-label*, .pill-count-badge, the .text-* type
   utilities, and .pcf-section-eyebrow. Genuinely new: the row layout + the
   thin-evidence warning treatment. Tokens only.
   ══════════════════════════════════════════ */
.cpt-products-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  /* Space between the PRODUCTS COLLECTION header and the list (Figma 377:6694
     header pb-10 + the section container gap-10 ≈ 20px). */
  padding-bottom: 20px;
}
.cpt-products-header .pcf-section-eyebrow { padding-bottom: 0; }

.cpt-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.cpt-empty {
  padding: 24px 0;
  text-align: center;
  color: var(--text-muted);
  font-size: 13px;
}

.cpt-row {
  display: flex;
  align-items: center;
  gap: 24px;
  min-height: 62px;            /* Figma 370:3012 row height */
  padding: 8px 16px;
  border: 1px solid var(--palette-neutrals-200);
  border-radius: var(--radius-small);
  background: var(--bg-surface);
  box-shadow: var(--shadow-container-elevated-bg);  /* elevated card (Figma drop-shadow) */
  transition: border-color 0.12s;
}
.cpt-row:hover { border-color: var(--palette-purple-300); }
.cpt-row-main {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 24px;
}
.cpt-row-name {
  flex: 0 1 260px;
  min-width: 0;
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cpt-row-category {
  flex: 1 1 auto;
  min-width: 0;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Fixed-width pill columns — DIRECT flex children of .cpt-row (flat layout,
   like the Index .pc-row). A nested flex wrapper (flex-shrink:0 holding
   fixed-basis cells) overflowed in Safari; direct fixed-basis children do not.
   Widths per Figma 370:3021/3023/3025 (130/120/110); the row's gap-24 spaces
   them and .cpt-row-main (flex:1) pushes them to the right. Pills left-aligned
   within each cell, which also keeps the priority pill off the row's edge. */
.cpt-cell { display: flex; align-items: center; }
.cpt-cell-sources  { flex: 0 0 130px; }
.cpt-cell-brief    { flex: 0 0 120px; }
.cpt-cell-priority { flex: 0 0 110px; }

/* Thin-evidence treatment (Figma 370:3005). The amber warning banner sits
   BEHIND the product row and peeks out above it — the row (a normal card with
   its own neutral border + shadow) overlaps and covers the banner's lower edge,
   so the amber box "tucks under" the row. Dismiss removes the banner + strips
   .cpt-thin-wrap (dismissable_controller), reverting to a plain row. The banner
   fill is the Figma's bespoke near-white warm tint #fffdf6 (no DS token); the
   border + warning text use --status-warning-subtle, and the dismiss X uses
   --text-muted (Figma 370:3010) — NOT amber. */
.cpt-thin-wrap {
  position: relative;
  padding-top: 28px;   /* Figma pt-28: how far the banner peeks above the row */
}
/* Row on top, covering the lower edge of the banner behind it. */
.cpt-thin-wrap .cpt-row {
  position: relative;
  z-index: 1;
}
.cpt-thin-banner {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 0;          /* behind the row — the amber box tucks UNDER it */
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 8px 16px 16px;   /* pt-8 px-16 pb-16; the pb tucks under the row */
  background: #fffdf6;
  border: 1px solid var(--status-warning-subtle);
  border-bottom: 0;
  border-radius: var(--radius-small) var(--radius-small) 0 0;
}
.cpt-thin-banner-text {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.cpt-thin-banner-icon {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  color: var(--status-warning-subtle);
}
.cpt-thin-banner-link {
  min-width: 0;
  font-family:    var(--text-ui-badge-font-family);
  font-weight:    var(--text-ui-badge-font-weight);
  font-size:      var(--text-ui-badge-font-size);
  line-height:    var(--text-ui-badge-line-height);
  letter-spacing: var(--text-ui-badge-letter-spacing);
  color: var(--status-warning-subtle);
  text-decoration: none;
}
.cpt-thin-banner-link:hover { text-decoration: underline; }
.cpt-thin-banner-dismiss {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text-muted);   /* Figma 370:3010: the X is text/muted (#9da3ae), not amber */
  cursor: pointer;
}
.cpt-thin-banner-dismiss svg { width: 14px; height: 14px; }

/* ATT-152 — shared content-rail banner. A FLOW banner that full-bleeds the
   .ps-content 32px padding and tucks FLUSH under the header stroke (square top,
   no top border, 12px bottom corners) — NOT the absolute row-tuck of
   .cpt-thin-banner. The slot is display:contents (like .ps-error-banner-slot) so
   the banner is the .ps-content flex child the negative margins act on. Two
   variants: --warning (stale group brief, Figma 504:4005) and --error (a
   generation/job failure). Used by the collection brief banner + the products
   error banner. Text reuses --text-ui-badge. */
.cr-banner-slot { display: contents; }
.cr-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin: -20px -32px 0;     /* full-bleed L/R; -20px top cancels the .ps-content
                                gap so the banner butts FLUSH under the header
                                stroke (its open top = the header's divider) */
  padding: 10px 32px;       /* content aligns with the 32px-inset header */
  border: 1px solid;
  border-top: 0;
  border-radius: 0 0 var(--radius-small) var(--radius-small);
}
.cr-banner--warning { background: #fffdf6; border-color: var(--status-warning-subtle); }
.cr-banner--warning .cr-banner-icon,
.cr-banner--warning .cr-banner-copy { color: var(--status-warning-subtle); }
.cr-banner--error { background: var(--status-error-default); border-color: var(--status-error-subtle); }
.cr-banner--error .cr-banner-icon,
.cr-banner--error .cr-banner-copy { color: var(--status-error-text); }

/* Takes the available width so a long copy (e.g. a product error message) wraps
   instead of overflowing; the short collection-brief copy stays one line. */
.cr-banner-text { display: flex; align-items: center; gap: 8px; flex: 1 1 auto; min-width: 0; }
.cr-banner-icon { width: 14px; height: 14px; flex-shrink: 0; }
.cr-banner-copy {
  min-width: 0;
  font-family:    var(--text-ui-badge-font-family);
  font-weight:    var(--text-ui-badge-font-weight);
  font-size:      var(--text-ui-badge-font-size);
  line-height:    var(--text-ui-badge-line-height);
  letter-spacing: var(--text-ui-badge-letter-spacing);
}
.cr-banner-copy strong { font-weight: 600; }   /* the lead statement is semibold */
.cr-banner-muted { color: var(--text-muted); } /* the trailing detail run is muted */
/* Inline action affordance (Regenerate / Try again / Retry). button_to POSTs;
   the form + button reset makes it read as a text link mid-sentence. */
.cr-banner-form { display: inline; margin: 0; }
/* .cr-banner-link is now a selector on the generic .text-link, defined above
   .btn-link-destructive. Its declarations are unchanged — only the rule it lives
   in moved. */
.cr-banner-dismiss {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
}
.cr-banner-dismiss svg { width: 14px; height: 14px; }


/* ─── ATT-168: generic 404 (.e404-*) ───────────────────────────────────────
   The one not-found page. A bad ID and a cross-tenant ID render identically —
   nothing here varies by what was missing.

   Two treatments, chosen ONLY by the viewer's own session state:
     - signed in WITH a brand -> "shell" layout; .e404-canvas centres the message
       inside .shell-content, which already supplies the surface/radius/shadow.
     - logged out, or signed in with no brand -> "application" layout, no chrome,
       so .e404-standalone carries the brand itself.

   Typography comes from the existing .text-* utilities (.text-display-large /
   .text-display-small / .text-h2 / .text-body / .text-caption) — the only rules
   below are layout, brand colour and the numeral's optical tracking. */

.e404-standalone {
  min-height: 100vh;
  background: var(--bg-page);
  padding: 72px 20px 84px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: var(--text-primary);
}

.e404-canvas {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 24px;
  color: var(--text-primary);
}

/* 600px column + 32px gutter — the same measure as the auth screens. */
.e404-wrap { width: 100%; max-width: 600px; padding: 0 32px; text-align: center; }

.e404-mark { color: var(--brand-primary); width: 44px; margin-bottom: 14px; }
.e404-wordmark { color: var(--brand-primary); width: 104px; margin-bottom: 40px; }
.e404-mark svg,
.e404-wordmark svg { width: 100%; height: auto; display: block; }

/* Type is .text-display-*; only colour + optical tracking at display size. */
.e404-numeral { color: var(--brand-primary); margin: 0 0 8px; letter-spacing: -0.02em; }

.e404-title { margin: 0 0 10px; }
.e404-body  { color: var(--text-muted); margin: 0 auto; max-width: 420px; }

.e404-actions { display: flex; justify-content: center; gap: 10px; margin-top: 28px; }

.e404-fineprint { color: var(--text-muted); margin: 24px 0 0; }
.e404-link { color: var(--text-link); text-decoration: none; }

/* ─── ATT-169: acting-as banner (.aab-*) ───────────────────────────────────
   The persistent bar an Attune staff user sees while operating inside a brand's
   tenant. Rendered by shared/_acting_as_banner on Current.acting_as?, above the
   chrome in every authenticated layout — a merchant user never sees it.

   Full width and saturated on purpose: this is a safety rail against an admin
   mistaking a real brand's live data for their own test brand, so it spans the
   nav AND the content and cannot be scrolled away from.

   Everything inside it is existing DS — .pill-label-brand, .btn-elevated-outline
   .btn-elevated-xs (Figma DS 226:5704), shared/_attune_mark. The rules below are
   only the bar's own layout and fill.

   Text on --brand-primary (#4c02e8, dark) is --text-on-primary (#fff). NOT
   --text-on-brand (#2a0182), which is dark purple for the LIGHT purple fills. */

:root { --aab-height: 44px; }

.aab {
  height: var(--aab-height);
  background: var(--brand-primary);
  color: var(--text-on-primary);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 0 20px;
  font-family: var(--font-sans);
}
.aab-left { display: flex; align-items: center; gap: 10px; min-width: 0; }

/* The logomark fills with currentColor, so it inherits the bar's white. */
.aab-mark { width: 18px; flex-shrink: 0; display: inline-flex; }
.aab-mark svg { width: 100%; height: auto; display: block; }

.aab-copy {
  font-size: 13px;
  line-height: 18px;
  color: inherit;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.aab-copy strong { font-weight: 600; }

/* button_to wraps the Exit control in a <form>; zero its margin so it does not
   add height to the 44px bar or push the button out of vertical centre. */
.aab-exit-form { margin: 0; flex-shrink: 0; }

/* The two layouts need OPPOSITE treatment, because the bar sits in a different
   place relative to each one's box.

   shell / application: <body> carries no padding or layout of its own, so the
   bar is already a flush, full-bleed sibling. .shell is height:100vh, so it is
   the one that must shrink or its last row is pushed past the fold.

   focused: body.focused-layout IS the layout box — padding 12/12/28/16, flex
   column — so the bar lands INSIDE the padding as a flex child. Two consequences:
     1. it must escape the padding to reach the viewport edge. Negative margins,
        the same full-bleed trick .cr-banner and .ps-error-banner-slot already
        use; the bottom 12px restores the gap the rail expects above itself.
     2. the body must NOT shrink by the bar height — the bar is inside its flex
        column, so 100vh already covers both. Subtracting would double-count and
        leave the page 44px short.
   Scoped to .is-acting-as so nothing changes when the bar is absent. */
.is-acting-as .shell { height: calc(100vh - var(--aab-height)); }
.focused-layout > .aab { margin: -12px -12px 12px -16px; }

/* ─── ATT-237: the billing pause bar (.bpb-*) ─────────────────────────────
   Promoted from the /styleguide/billing-banner mock's page-scoped .sgbb-*, with
   the mock's own values: geometry lifted from .aab so the two bars are
   interchangeable in the layout (44px, flex, space-between, 0 20px, 13/18 copy),
   and only the fill and contents differ.

   ONE FILL, not three. The mock draws a warning bar for a trial that is merely
   ENDING — that state is ATT-209's and nothing is paused in it. Every state this
   bar renders has already stopped something, so all three are --error. */
:root { --bpb-height: 44px; }

.bpb {
  height: var(--bpb-height);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 0 20px;
  font-family: var(--font-sans);
}

.bpb--error { background: var(--status-error-default); }

.bpb-left { display: flex; align-items: center; gap: 10px; min-width: 0; }

/* 14px to match .cr-banner-icon rather than .aab-mark's 18px: this is a banner
   icon. Stroke weight arrives from the helper (ATT-240), never from a class. */
.bpb-icon { width: 14px; height: 14px; flex-shrink: 0; display: inline-flex; color: var(--text-secondary); }
.bpb-icon svg { width: 100%; height: 100%; display: block; }

.bpb-copy {
  min-width: 0;
  font-size: 13px;
  line-height: 18px;
  color: var(--text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.bpb-copy strong { font-weight: 600; color: var(--status-error-subtle); }

/* THE HEIGHT OFFSETS, and .ps-page is here where .aab's equivalent rule covers
   only .shell. Product Show is its own 100vh box under `body:has(.ps-page)`
   with overflow:hidden, so an un-offset bar clips the foot of the page rather
   than scrolling it. (.aab does not offset .ps-page today — pre-existing, and
   left alone rather than fixed sideways in this ticket.)

   BOTH BARS CAN RENDER AT ONCE: Attune staff acting inside a paused brand see
   the acting-as bar and this one, so the combined case subtracts both. */
.is-billing-paused .shell,
.is-billing-paused .ps-page { height: calc(100vh - var(--bpb-height)); }

.is-acting-as.is-billing-paused .shell,
.is-acting-as.is-billing-paused .ps-page { height: calc(100vh - var(--aab-height) - var(--bpb-height)); }

/* focused: body IS the layout box, so the bar escapes its padding to reach the
   viewport edge and the body does NOT shrink — the bar is inside its flex
   column, so 100vh already covers both. Mirrors the .aab treatment exactly. */
.focused-layout > .bpb { margin: -12px -12px 12px -16px; }

/* ─── ATT-186: self-service onboarding wizard (.ob-*) ──────────────────────
   Promoted from the /styleguide/onboarding mock's page-scoped .sgo-*. The mock
   stays as the north-star reference; these are the real classes.

   DARK MODE. The wizard is the one surface with a light/dark toggle today
   (theme_controller.js, localStorage-backed). It works by overriding SEMANTIC
   TOKENS for the .ob-root subtree — the global token block at the top of this
   file is untouched. Everything token-based re-themes for free; the handful of
   rules that hardcode a colour need explicit help and are listed at the bottom.

   To lift this app-wide later (authenticated theme preference): move data-theme
   onto <html> and change these selectors from `.ob-root[data-theme="dark"]` to
   `[data-theme="dark"]`. Nothing else changes.

   PREREQUISITE, already done: merchants/onboarding/show.html.erb used to ship a
   <style> block redefining :root with hardcoded hex (--text, --border, --purple,
   …). Those names are the DS backwards-compat ALIASES, which normally resolve to
   semantic tokens (--text -> --text-primary). Hardcoding them at :root did not
   just cause drift — it made dark mode impossible, because no token swap could
   reach through them. */

.ob-root {
  min-height: 100vh;
  background: var(--bg-page);
  color: var(--text-primary);
  font-family: var(--font-sans);
  display: flex;
  flex-direction: column;
}

/* ─── ATT-198: admin console nav (.adm-*) ──────────────────────────────────
   Promoted from the approved /styleguide/admin-console proposal's page-scoped
   .sga-*. Only three pieces are genuinely new — the nav header, the section
   label, and one override — because the console reuses the brand console's
   .shell / .shell-nav / .nav-item / .nav-menu wholesale. A second page
   architecture would be a permanent tax; this is a second MODE. */

/* The identity block. Same slot the brand nav gives its merchant switcher, but
   admin surfaces do not participate in acting-as, so there is no brand to
   switch: it says who you are and where you are. */
.adm-navhead-wrap { position: relative; margin-bottom: 8px; }
.adm-navhead {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 8px 10px;
  border: 0;
  background: transparent;
  border-radius: 8px;
  cursor: pointer;
  text-align: left;
}
.adm-navhead:hover { background: var(--bg-elevated); }
.adm-navid { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.adm-navtitle {
  font-family: var(--font-sans);
  font-size: 13px;
  font-weight: 600;
  color: var(--text-primary);
  line-height: 18px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.adm-navsub {
  font-family: var(--font-sans);
  font-size: 11px;
  color: var(--text-muted);
  line-height: 14px;
}

/* .nav-items-wrap is justify-content: space-between, which is right for the
   brand nav (one list pushed apart from the Admin Console link at the bottom)
   but spreads labelled sections across the whole rail. Group them at the top. */
.adm-root .nav-items-wrap { justify-content: flex-start; gap: 0; }

/* Section label — the eyebrow pattern, the one place uppercase is sanctioned
   (CLAUDE.md capitalization). Tight to its own list; the space belongs BETWEEN
   sections, which is why the margin is on the following label rather than
   inside one. */
.adm-navsection {
  font-family: var(--font-sans);
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  padding: 0 10px;
  margin: 0 0 4px;
}
.nav-items + .adm-navsection { margin-top: 18px; }

/* ─── ATT-242: index filter bar (.fb-*) ───────────────────────────────────
   Promoted verbatim from the approved /styleguide/filters page, renamed .sgf-*
   → .fb-* on the way in. NOT prefixed to a surface, unlike .pi-* / .pc-* / .adm-*:
   this is the ONE cluster the prefix-isolation rule exempts, because a filter
   bar sitting differently on Brands than on Products is the failure it exists to
   prevent. Surfaces contribute FACETS (a Filtering::FilterSet subclass), never
   layout.

   Only the admin brands list is wired today; a second caller is a config object
   and these same partials.

   What it reuses rather than restates: .popover + .ps-menu-dropdown (the DS
   popover), .pill-kv (the two-tone key/value pill from the audiences index — a
   filter chip is exactly that shape), .pill-count-badge, .pcf-check* (the DS
   circular check), .btn-elevated-xs. What is genuinely new is below: the two-row
   bar, the chip's dismiss button, and the panel's value row. */

/* ROW 1 lives in the host surface's own toolbar (.adm-toolbar here) — the bar
   does not own the search box, because search is not a filter and every surface
   already has one. The trigger sits outboard of the count, so the two things
   that never move anchor the ends. */
.fb-trigger-wrap { position: relative; display: inline-flex; flex: 0 0 auto; }

/* ROW 2 — the applied filters, on their own line, wrapping. Rendered ONLY when
   something is applied: an empty row reserving height would make the list jump
   every time the last chip is removed.

   One hairline, no fill. A tinted band puts fill behind fill and costs the chips
   their edges, and it implies a container that persists when this row does not
   exist until a filter is applied. The chips are already the coloured thing on
   the row; the rule just says "this appeared". */
.fb-applied {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--palette-neutrals-200);
}
/* Pushed right, so it lands under the trigger that adds them. */
.fb-applied .fb-clear { margin-left: auto; }

/* ── the chip ── */
.fb-chip { display: inline-flex; align-items: center; gap: 0; position: relative; }
.fb-chip-label {
  border: 0;
  background: transparent;
  padding: 0;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font: inherit;
  color: inherit;
  text-decoration: none;
}
.fb-chip-chevron { width: 12px; height: 12px; opacity: .6; }
.fb-chip-x {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  background: transparent;
  cursor: pointer;
  padding: 0 0 0 8px;
  color: var(--brand-primary);
  opacity: .55;
}
.fb-chip-x:hover { opacity: 1; }
.fb-chip-x svg { width: 13px; height: 13px; }

/* ── the panel ──
   .popover + .ps-menu-dropdown are the DS popover; these only size it and lay
   out a value row. */
.fb-panel { min-width: 232px; padding: 6px; }
/* Anchored right: the trigger sits at the content's right edge, and a menu
   opening leftward from there would run off the page. */
.fb-panel--right { left: auto; right: 0; }
.fb-value {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 7px 8px;
  border: 0;
  background: transparent;
  cursor: pointer;
  border-radius: 8px;
  text-align: left;
  font-family: var(--font-sans);
  font-size: var(--text-body-small-font-size);
  color: var(--text-secondary);
  text-decoration: none;
}
.fb-value:hover { background: var(--bg-elevated); }
.fb-value-name { flex: 1 1 auto; }
.fb-value-chevron { width: 13px; height: 13px; opacity: .5; }
.fb-panel-foot {
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-top: 1px solid var(--palette-neutrals-200);
  margin-top: 6px;
  padding: 8px 8px 2px;
}
.fb-linkbtn {
  border: 0;
  background: transparent;
  padding: 0;
  cursor: pointer;
  font-family: var(--font-sans);
  font-size: var(--text-caption-font-size);
  color: var(--text-muted);
  text-decoration: none;
}
.fb-linkbtn:hover { color: var(--text-primary); }

/* The back row: this panel replaced the facet list, and that has to be undoable
   without closing and reopening. */
.fb-back {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 6px 8px;
  border: 0;
  background: transparent;
  cursor: pointer;
  border-radius: 8px;
  font-family: var(--font-sans);
  font-size: var(--text-caption-font-size);
  font-weight: 500;
  color: var(--text-muted);
  text-decoration: none;
}
.fb-back:hover { background: var(--bg-elevated); color: var(--text-primary); }
.fb-back svg { width: 13px; height: 13px; }

/* The count pushed right, so the trigger that follows it anchors the row's end.
   .adm-toolbar is space-between, which would otherwise centre it. */
.adm-toolbar .adm-showing { margin-left: auto; }

/* ─── ATT-198 phase 3: Brand Management (.adm-brand-* / .adm-page*) ────────
   Promoted from the approved /styleguide/brand-switcher row and
   /styleguide/admin-console brand detail. Its own cluster rather than reusing
   another surface's rows (CLAUDE.md prefix isolation) — .pi-* and .pc-* are the
   products and collections index rows and are not this shape.

   NOT reusing .ab-*: that cluster is the deleted Add Brand PAGE's scaffolding
   (header/form/section/actions). It went unreferenced when ATT-208 removed its
   only view; nothing here is the same thing wearing a different name, so it is
   left for ATT-185's sweep rather than repurposed. */

/* ── Page chrome ── */
/* Page / full-bleed header / body — the shape every other index and show view in
   the app already uses (.pc-* from ATT-134, .aud-* from ATT-155), with their
   values rather than near-misses.
   The trick, in collections' own words: the horizontal padding moves INSIDE each
   section rather than sitting on the page, so the header's bottom border spans
   the content rail edge to edge. The admin console shipped with page-level
   padding and no border at all, which is why its headers read as a different
   app next to Audiences or Collections. */
.adm-page { padding: 32px 0; }
.adm-page-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 0 32px 20px;
  border-bottom: 1px solid var(--border-primary);
}
.adm-body { padding: 32px 32px 0; }
/* Brand DETAIL constrains its content to the same 750px measure the brand-side
   Billing page uses (.set-col), so the two read alike when support has them open
   together. The index deliberately does NOT: a list of brands wants the width.
   The header band and back link sit outside this and stay full width. */
.adm-col { max-width: 750px; margin: 0 auto; }
/* A back link above the header band, on the same grid. .adm-back carries its own
   margin-bottom for the old layout; the wrapper owns the spacing here. */
.adm-page-back { padding: 0 32px; }
.adm-page-back .adm-back { margin-bottom: 10px; }
.adm-title-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.adm-h1 {
  font-family:    var(--text-h1-font-family);
  font-weight:    var(--text-h1-font-weight);
  font-size:      var(--text-h1-font-size);
  line-height:    var(--text-h1-line-height);
  letter-spacing: var(--text-h1-letter-spacing);
  color: var(--text-primary);
  margin: 0;
}
.adm-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 14px;
}
/* The search input is wrapped in a real GET form, so the FORM is the flex child
   and has to carry the width the bare input used to. .form-input is width:100%,
   so the input fills it. */
.adm-search-form { flex: 0 1 280px; }
.adm-search { max-width: 280px; }
.adm-showing {
  font-family: var(--font-sans);
  font-size: var(--text-caption-font-size);
  color: var(--text-muted);
  white-space: nowrap;
}
/* Search matched nothing. Mirrors .aud-tab-stub — the same treatment the
   Consumers panel uses for the same state — under this surface's own prefix. */
.adm-search-empty {
  padding: 40px 0;
  text-align: center;
  font-family: var(--text-body-small-font-family);
  font-size: var(--text-body-small-font-size);
  line-height: var(--text-body-small-line-height);
  color: var(--text-muted);
}

/* ── The brand row ──
   Flat, with the actions as a DIRECT flex child — not nested inside an
   intermediate shrink:0 wrapper, which overflows in Safari (see .pc-row). */
.adm-brand-list { display: flex; flex-direction: column; gap: 8px; }
.adm-brand-row {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 14px 16px;
  border: 1px solid var(--palette-neutrals-200);
  border-radius: var(--radius-small);
  background: var(--bg-surface);
  transition: border-color 0.12s;
}
.adm-brand-row:hover { border-color: var(--palette-purple-300); }
/* The brand you are currently inside. Fill + stroke carry it; the Active pill
   beside the name says it in words. */
.adm-brand-row.is-active { border-color: var(--brand-primary); background: var(--brand-subtle); }

.adm-brand-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 5px; }
.adm-brand-name-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.adm-brand-name {
  font-family: var(--text-body-bold-font-family);
  font-weight: var(--text-body-bold-font-weight);
  font-size:   var(--text-body-bold-font-size);
  color: var(--text-primary);
}
.adm-brand-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-family: var(--font-sans);
  font-size: var(--text-caption-font-size);
  color: var(--text-muted);
}
.adm-brand-dot { color: var(--palette-neutrals-300); }
.adm-brand-site { color: var(--text-muted); text-decoration: none; }
.adm-brand-site:hover { color: var(--brand-primary); text-decoration: underline; }

/* ATT-233 — the status cluster, right-aligned against the actions rather than
   trailing the brand name, so the states line up in one column down the list
   instead of starting at a different x per row.

   A DIRECT flex child of .adm-brand-row (never nested in another shrink:0
   wrapper — that overflows in Safari, see .pc-row). margin-left:auto is what
   pushes it right; align-items centres it against the two-line name/meta block
   beside it. */
.adm-brand-pills {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: 8px;
  margin-left: auto;
  flex-shrink: 0;
}

.adm-brand-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
/* The kebab is hover-revealed; the context control beside it is not. Keeping the
   quiet action quiet is what stops every row shouting two buttons. */
.adm-brand-quiet { opacity: 0; transition: opacity 0.12s; }
.adm-brand-row:hover .adm-brand-quiet,
.adm-brand-row:focus-within .adm-brand-quiet { opacity: 1; }

/* button_to renders its own <form>; without this the context control breaks the
   row's flex line. (The same trap ATT-211 hit putting a form inside a <p>.) */
.adm-brand-actions form { display: inline-flex; margin: 0; }

/* ── Brand detail ── */
.adm-back {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 14px;
  font-family: var(--font-sans);
  font-size: var(--text-body-small-font-size);
  color: var(--text-muted);
  text-decoration: none;
  transition: color 0.12s;
}
/* The same hover as .set-back, so a back link behaves the same in the admin
   console as it does in the brand app. */
.adm-back:hover { color: var(--brand-primary); }
/* ── svg, NOT .lucide ──
   `lucide_icon "arrow-left"` renders NO CLASS unless one is passed, so
   `.adm-back .lucide` never matched anything and this arrow had been drawing at
   its raw 24px width attribute the whole time — visibly bigger than every other
   back arrow. .set-back's rule is a bare `svg` selector, which is why the two
   settings links were always correct. 14px matches them. */
.adm-back svg { width: 14px; height: 14px; flex-shrink: 0; }

/* ATT-199 — right-aligned form actions, .set-form-actions' values under the
   console's own prefix. Form buttons are right-aligned everywhere in this app. */
.adm-form-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px; }
.adm-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin: 18px 0; }
.adm-card {
  border: 1px solid var(--palette-neutrals-200);
  border-radius: var(--radius-small);
  background: var(--bg-surface);
  padding: 14px 16px;
}
.adm-card-label {
  font-family: var(--font-sans);
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  margin-bottom: 6px;
}
.adm-card-value {
  font-family: var(--font-sans);
  font-size: 18px;
  font-weight: 600;
  color: var(--text-primary);
}

.adm-section-label {
  font-family: var(--font-sans);
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}
.adm-detail-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin: 22px 0 8px;
}
/* Key/value details with ONE edit action in the header, rather than a row you
   click Edit on to reach a form — the app's established Show-page pattern. */
.adm-dl {
  margin: 0;
  border: 1px solid var(--palette-neutrals-200);
  border-radius: var(--radius-small);
  background: var(--bg-surface);
  overflow: hidden;
}
.adm-dl-row {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 11px 16px;
  border-top: 1px solid var(--palette-neutrals-200);
}
.adm-dl-row:first-child { border-top: 0; }
.adm-dl dt {
  flex: 0 0 140px;
  font-family: var(--font-sans);
  font-size: var(--text-caption-font-size);
  color: var(--text-muted);
}
.adm-dl dd {
  margin: 0;
  min-width: 0;
  font-family: var(--font-sans);
  font-size: var(--text-body-font-size);
  color: var(--text-primary);
}

/* ── ATT-233: the Billing section ──
   The pill and its one-line gloss share a row. Wraps rather than truncating —
   the paused explanation carries a date staff need to act on, so it is the one
   string on this page that must not be cut off. */
.adm-billing-status {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.adm-billing-note {
  font-size: var(--text-body-small-font-size);
  color: var(--text-muted);
}

/* Identifiers, compared character by character against another screen — so
   mono, where every other value on this page is sans. */
.adm-stripe-id {
  font-family: var(--font-mono);
  font-size: var(--text-body-small-font-size);
  color: var(--brand-primary);
  text-decoration: none;
}
.adm-stripe-id:hover { text-decoration: underline; }
.adm-dl-empty { color: var(--text-muted); }
/* ── Invoices (staff view) ──
   Its own cluster rather than .set-inv reused: each surface owns its layout, and
   this one carries four cells where the brand's carries six (no Pay link, no
   PDF — the Stripe link in the section header covers both). */
.adm-invs {
  border: 1px solid var(--palette-neutrals-200);
  border-radius: var(--radius-small);
  background: var(--bg-surface);
  overflow: hidden;
}
.adm-inv {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 10px 16px;
  font-size: var(--text-body-small-font-size);
  line-height: 18px;
  color: var(--text-primary);
}
.adm-inv + .adm-inv { border-top: 1px solid var(--palette-neutrals-200); }
/* DIRECT children, fixed basis. See the Safari note on .pc-row. */
.adm-inv-date   { flex: 0 0 100px; }
.adm-inv-number { flex: 0 0 150px; color: var(--text-muted); font-family: var(--font-mono); font-size: 12px; }
.adm-inv-amount { flex: 0 0 90px; font-weight: 500; }
.adm-inv-status { flex: 1 1 auto; }
.adm-inv-note {
  margin: 10px 0 0;
  font-size: var(--text-body-small-font-size);
  line-height: 18px;
  color: var(--text-muted);
}

/* An identifier that is not a link. Same mono treatment as .adm-stripe-id, minus
   the colour and hover that would promise somewhere to go. */
.adm-id {
  font-family: var(--font-mono);
  font-size: var(--text-body-small-font-size);
  color: var(--text-primary);
}

/* ── ATT-208: admin mode ──
   The wizard serves a public signup visitor AND Attune staff creating a brand for
   a client. In admin mode the logomark/wordmark header is REPLACED by this bar —
   the wordmark welcomes someone arriving cold, which staff are not, and it costs
   ~110px before any content.

   Not .aab: that bar means "you are inside this brand's data", the opposite of
   what an admin surface claims (ATT-201 suppressed it here for exactly that
   reason). Exit sits left, matching focused_rail's back link. */
.ob-adminbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 16px;
  background: var(--bg-elevated);
  border-bottom: 1px solid var(--border-primary);
  font-family: var(--font-sans);
  font-size: 12px;
  color: var(--text-secondary);
}
.ob-adminbar-mode { display: flex; align-items: center; gap: 8px; min-width: 0; }
.ob-adminbar-exit {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--text-muted);
  text-decoration: none;
  white-space: nowrap;
}
.ob-adminbar-exit:hover { color: var(--brand-primary); }
.ob-adminbar-exit svg { width: 15px; height: 15px; }
/* With no .ob-header above it, the content column needs its own lead. */
.ob-adminbar + .ob-main { padding-top: 28px; }

/* ── Header: logomark ABOVE wordmark, both centred, per the mock. No nav —
   onboarding is a focused arc. Sizes are the mock's: 44px mark, 104px wordmark. ── */
.ob-header { display: flex; flex-direction: column; align-items: center; padding: 56px 20px 0; }
.ob-mark { width: 44px; color: var(--brand-primary); margin-bottom: 14px; }
.ob-wordmark { width: 104px; color: var(--brand-primary); margin-bottom: 24px; }
.ob-mark svg,
.ob-wordmark svg { width: 100%; height: auto; display: block; }

/* Screen title. Sits INSIDE the content column, ABOVE the stepper — the mock's
   order is mark -> wordmark -> h2 -> stepper -> card. */
.ob-title {
  font-family:    var(--text-h2-font-family);
  font-weight:    var(--text-h2-font-weight);
  font-size:      var(--text-h2-font-size);
  line-height:    var(--text-h2-line-height);
  letter-spacing: var(--text-h2-letter-spacing);
  color: var(--text-primary);
  text-align: center;
  margin: 0 0 24px;
}

/* ── Stepper ──
   The mock's progress-BAR styling, but stepper SEMANTICS: each phase is simply
   done / current / upcoming, with no partial fill. That is what removes the
   mock's inline style="width:N%" — a bar is either filled or it isn't, so the
   state is a class.

   Geometry is the mock's .sgo-phase* verbatim: 4px bar, 2px radius, 8px gap to
   the label, phases flexed equally with an 8px gutter. Colours are the mock's
   too — done is brand-secondary (green), current is brand-primary. */
.ob-stepper {
  list-style: none;
  margin: 0 0 28px;
  padding: 0;
  display: flex;
  gap: 8px;
}
.ob-step { flex: 1; }
/* display:block is load-bearing — the bar is a <span>, and an inline element
   ignores height, so without it the 4px bar renders as nothing at all. */
.ob-step-bar {
  display: block;
  height: 4px;
  border-radius: 2px;
  background: var(--palette-neutrals-200);
  margin-bottom: 8px;
}
.ob-step.is-done .ob-step-bar    { background: var(--brand-secondary); }
.ob-step.is-current .ob-step-bar { background: var(--brand-primary); }
/* The mock hardcoded 11px/500/0.02em; --text-ui-badge-* is the DS token group
   holding exactly that scale (11px/500/16px), so use it rather than literals.
   Only the letter-spacing differs (token 0.005em vs the mock's 0.02em) — the
   token wins, per the DS typography convention. */
.ob-step-label {
  display: block;
  font-family:    var(--text-ui-badge-font-family);
  font-weight:    var(--text-ui-badge-font-weight);
  font-size:      var(--text-ui-badge-font-size);
  line-height:    var(--text-ui-badge-line-height);
  letter-spacing: var(--text-ui-badge-letter-spacing);
  color: var(--text-muted);
}
.ob-step.is-current .ob-step-label { color: var(--text-primary); font-weight: 600; }
.ob-step.is-done .ob-step-label    { color: var(--brand-secondary); }

/* ── Content column ── */
.ob-main { flex: 1 1 auto; display: flex; justify-content: center; padding: 0 20px 64px; }
/* 32px L/R gutter = the DS page standard (focused-rail header padding). */
.ob-wrap { width: 100%; max-width: 600px; padding: 0 32px; }

/* ── Per-screen scaffolding ──
   Used by EVERY screen in the arc, so it belongs to the shell rather than to any
   one of them: .ob-card is the inset content column (deliberately NOT a bordered
   card — the mock's .sgo-card is padding only), .ob-sub the description under the
   title, .ob-actions the button row. All token-based, so they re-theme with the
   dark scope for free. */
.ob-card { width: 100%; padding: 0 24px; }

/* Opt-in wide CONTENT column, for screens whose body needs more room than the
   600px form measure — the plan chooser's three tier columns are squeezed at
   the default width. (Same idea as .focus-content-wide on the focused-rail
   layout: a width modifier the screen asks for, not a new layout.)

   It widens the CONTENT ONLY. The title and stepper live in .ob-wrap, outside
   this element, and deliberately stay at the standard measure — a stepper
   stretched to match the cards reads as a different page.

   Mechanism: .ob-wrap caps at 600px, so the column has to bleed back out of its
   own parent. Margins resolve against the parent's width, so a target wider than
   the parent yields the negative inset that centres it. Clamped to the viewport
   so the bleed can never produce a horizontal scrollbar. */
.ob-card-wide {
  --ob-content-width: min(880px, calc(100vw - 48px));
  width: var(--ob-content-width);
  margin-left:  calc((100% - var(--ob-content-width)) / 2);
  margin-right: calc((100% - var(--ob-content-width)) / 2);
}
/* ATT-246 — .set-card joins the selector rather than getting a rule of its own.
   .form-field carries NO margin (only an internal 6px gap), so stacked fields
   collapse together anywhere this selector does not reach; drilled before adding
   anything, and the wizard card held the ONLY such rule in the stylesheet. The
   other settings form has a single field, which is why the gap went unnoticed.
   One selector, one value — two rules could drift to two spacings. */
.ob-card .form-field + .form-field,
/* ATT-199 — .adm-card joins the list rather than getting a rule of its own: this
   IS the "fields inside a card" spacing, and a second copy is how the console's
   forms would drift from settings' by 2px in a year. */
.adm-card .form-field + .form-field,
.set-card .form-field + .form-field { margin-top: 20px; }

.ob-sub {
  font-family: var(--font-sans);
  font-size: 13px;
  line-height: 20px;
  color: var(--text-muted);
  margin: 0 0 24px;
}
.ob-sub-center { text-align: center; }

.ob-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 28px; }
.ob-actions-split  { justify-content: space-between; }
.ob-actions-center { justify-content: center; }
.ob-actions-right  { display: flex; gap: 10px; }
/* ATT-244 — the mirror of the line above, for the Back + Exit pair. Exit is a
   quiet text link rather than a second outline button: two identical buttons
   side by side read as two equal choices, and leaving is not equal to going
   back. */
.ob-actions-left   { display: flex; gap: 10px; align-items: center; }
.ob-exit {
  font-family: var(--font-sans);
  font-size: 13px;
  color: var(--text-muted);
  text-decoration: none;
}
.ob-exit:hover { color: var(--text-primary); text-decoration: underline; }

/* Two-up field row (e.g. first name / last name on Account). */
.ob-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.ob-row .form-field + .form-field { margin-top: 0; }
.ob-card .form-field + .ob-row,
.ob-card .ob-row + .form-field { margin-top: 20px; }

/* ── Billing phase: plan chooser (step 5) ──
   Tier rows are pure-CSS selectable radios, the same arrangement as .listbox:
   a visually-hidden input drives a drawn .ob-tier-radio. The listbox stops
   there; these also tint the whole row when selected, which needs :has() on the
   label (already used elsewhere in this file). */
.ob-seg-row { display: flex; justify-content: center; margin-bottom: 18px; }
/* ATT-208 — the plan error. The tiers are a radio GROUP with no .form-field
   wrapper to redden, so the message sits above them rather than under a field. */
.ob-plan-error { text-align: center; margin: 0 0 12px; }

.ob-tiers {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
  align-items: stretch;
}
@media (max-width: 720px) {
  .ob-tiers { grid-template-columns: 1fr; }
  /* The 310px floor exists to even up three side-by-side columns. Stacked, it
     only pads three cards out to ~930px of mostly empty scroll, so let each one
     size to its content. */
  .ob-tier { min-height: auto; }
  /* One column needs no extra room, and the bleed would push the cards out of
     line with everything else on the screen. */
  .ob-card-wide { width: 100%; margin-left: 0; margin-right: 0; }
}

.ob-tier {
  position: relative;              /* contains the absolutely-positioned input */
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 32px 20px;
  /* min-height rather than height: content sits top-aligned with the remainder
     as trailing space, and a long feature line that wraps in a narrow column
     grows the card instead of overflowing it. Grid `align-items: stretch` then
     matches the other two to whichever is tallest. */
  min-height: 310px;
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-small);
  background: var(--bg-surface);
  cursor: pointer;
  transition: border-color 140ms ease, background 140ms ease;
}
.ob-tier:hover { border-color: var(--palette-neutrals-300); }
/* No drawn radio — the card's own stroke and fill ARE the selection. The 2px
   inset ring keeps the chosen card from growing and nudging its neighbours. */
.ob-tier:has(.ob-tier-input:checked) {
  border-color: var(--brand-primary);
  background: var(--brand-subtle);
  box-shadow: inset 0 0 0 1px var(--brand-primary);
}
/* The input is hidden, so the focus ring has to come from the card or keyboard
   selection is invisible. */
.ob-tier:has(.ob-tier-input:focus-visible) {
  outline: 2px solid var(--brand-primary);
  outline-offset: 2px;
}

.ob-tier-input { position: absolute; opacity: 0; width: 0; height: 0; }

.ob-tier-head {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  min-height: 22px;                /* the badge's height, so heads align across */
}
.ob-tier-name {
  font-family: var(--font-sans);
  font-size: 14px;
  font-weight: 600;
  color: var(--text-primary);
}
.ob-tier-price {
  font-family: var(--font-sans);
  font-size: 13px;
  color: var(--text-muted);
}
.ob-tier-price strong {
  font-size: 22px;
  font-weight: 600;
  color: var(--text-primary);
}

/* One incentive per line, each with a check — replaces the mock's
   "a · b · c" run-on. Spans rather than a <ul>: everything inside a <label>
   must be phrasing content. */
.ob-tier-feats {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 2px;
  padding-top: 12px;
  border-top: 1px solid var(--border-primary);
}
.ob-tier-feat {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  font-family: var(--font-sans);
  font-size: 12px;
  line-height: 17px;
  color: var(--text-secondary);
}
.ob-tier-feat svg {
  width: 13px;
  height: 13px;
  flex-shrink: 0;
  margin-top: 2px;
  color: var(--brand-secondary);
}

/* Monthly/annual price swap. BOTH variants render and the checked period radio
   decides which is shown — the radios sit several levels deep inside
   .segmented, so this reads the state off the .ob-billing ancestor with :has()
   rather than a sibling combinator. No JS for a pure display swap, and the
   choice survives a re-render. */
.ob-price-annual { display: none; }
.ob-billing:has(.ob-period-annual:checked) .ob-price-monthly { display: none; }
.ob-billing:has(.ob-period-annual:checked) .ob-price-annual  { display: block; }
.ob-price-sub {
  display: block;
  font-size: 11px;
  font-weight: 400;
  color: var(--text-muted);
}

.ob-note {
  font-family: var(--font-sans);
  font-size: 12px;
  line-height: 18px;
  color: var(--text-muted);
  margin: 14px 0 0;
}

/* ── Team phase: repeatable invite rows ── */
/* ATT-205 — ONE ROW PER TEAMMATE: name, email, role, remove. This is what the
   step's .ob-card-wide measure is for (see _step7) — at the 600px default the
   email lands around 170px, which is why the names briefly took a line of their
   own. At 880px the row measures first 157 / last 157 / email 314 / role 132 /
   remove 32, and the ✕ aligns on every row. */
.ob-invite-list { display: flex; flex-direction: column; gap: 10px; }
.ob-invite-row {
  display: grid;
  grid-template-columns: 1fr 1fr 1.8fr 132px 32px;
  gap: 10px;
  align-items: center;
}
/* display:contents, so the two name inputs are direct items of the row grid above
   rather than a nested pair. The wrapper exists for the narrow fallbacks below,
   where it becomes a real grid again. */
.ob-invite-names { display: contents; }

@media (max-width: 820px) {
  /* Below the wide measure the five-up squeezes the email past usefulness — the
     card is min(880px, 100vw - 48px), so it tracks the viewport down here. Names
     take their own line; email/role/remove keep the three-up. */
  .ob-invite-row { grid-template-columns: 1fr 132px 32px; }
  .ob-invite-names {
    display: grid;
    grid-column: 1 / -1;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
  }
  /* Two-line rows need more separation than the 10px inside one, or two
     teammates read as four unrelated fields. */
  .ob-invite-list { gap: 20px; }
}
@media (max-width: 560px) {
  /* The role select and remove button keep their widths; the email takes the
     rest. Below this the three-up row squeezes the email past usefulness. */
  .ob-invite-row { grid-template-columns: 1fr 32px; }
  .ob-invite-row .control { grid-column: 1 / -1; order: 1; }
  /* Stacked too — two name fields side by side at this width are narrower than
     the email the breakpoint exists to protect. */
  .ob-invite-names { grid-template-columns: 1fr; }
}

.ob-invite-remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  transition: color 140ms ease, background 140ms ease;
}
.ob-invite-remove:hover { color: var(--text-primary); background: var(--bg-elevated); }
.ob-invite-remove svg { width: 16px; height: 16px; }

.ob-add-row {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 12px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text-link);
  font-family: var(--font-sans);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
}
.ob-add-row svg { width: 15px; height: 15px; }

/* ── Terminal screen: success mark + centred copy ── */
.ob-done-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  margin: 0 auto 20px;
  border: 1.5px solid var(--brand-secondary);
  border-radius: 50%;
  color: var(--brand-secondary);
}
.ob-done-mark svg { width: 26px; height: 26px; }
/* The terminal screen centres its title and body; every other screen is
   left-aligned under the stepper. */
.ob-sub-center { text-align: center; }

/* ── Billing phase: plan summary on the payment screen (step 6) ── */
.ob-plan {
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-small);
  background: var(--bg-surface);
  padding: 14px 16px;
  margin-bottom: 20px;
}
.ob-plan-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.ob-plan-name {
  font-family: var(--font-sans);
  font-size: 14px;
  font-weight: 600;
  color: var(--text-primary);
}
.ob-plan-meta {
  font-family: var(--font-sans);
  font-size: 12px;
  color: var(--text-muted);
  margin-top: 2px;
}
.ob-plan-price {
  font-family: var(--font-sans);
  font-size: 13px;
  white-space: nowrap;
  color: var(--text-muted);
}
.ob-plan-price strong { font-size: 18px; font-weight: 600; color: var(--text-primary); }
.ob-plan-trial {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border-primary);
  font-family: var(--font-sans);
  font-size: 12px;
  font-weight: 500;
  color: var(--brand-secondary);
}
.ob-plan-trial svg { width: 14px; height: 14px; }

/* Shared small-text treatments. */
.ob-link { color: var(--text-link); text-decoration: none; }
/* ATT-171 — the "New to Attune?" line under the login form. Its own class rather
   than .ob-fineprint: the mock draws it at 13px with a 20px lead, where fineprint
   is the 12px/16px terms treatment, and the two sit on different screens. */
.ob-alt {
  font-family: var(--font-sans);
  font-size: 13px;
  line-height: 20px;
  color: var(--text-muted);
  text-align: center;
  margin: 20px 0 0;
}
.ob-fineprint {
  font-family: var(--font-sans);
  font-size: 12px;
  line-height: 18px;
  color: var(--text-muted);
  text-align: center;
  margin: 16px 0 0;
}
.ob-note {
  font-family: var(--font-sans);
  font-size: 12px;
  line-height: 18px;
  color: var(--text-muted);
  margin: 14px 0 0;
}

/* The Analyzing screen drops the real Live Status strip straight into the card. */
.ob-card .ls-strip { margin-bottom: 0; }

/* ── Account screen: OAuth stub + "or" divider + OTP boxes ──
   Promoted from the mock's .sgo-oauth / .sgo-gmark / .sgo-or / .sgo-otp*. The
   Google button is a NON-FUNCTIONAL stub (real OAuth is deferred). */
.ob-oauth { display: flex; width: 100%; align-items: center; justify-content: center; gap: 8px; }
.ob-gmark { display: inline-flex; }
.ob-gmark svg { display: block; width: 16px; height: 16px; }

.ob-or {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 16px 0;
  font-family: var(--font-sans);
  font-size: 12px;
  color: var(--text-muted);
}
.ob-or::before,
.ob-or::after { content: ""; flex: 1; height: 1px; background: var(--border-primary); }

/* Six single-character boxes, per the mock. They carry no name — otp_controller
   mirrors them into one hidden field, so the server still reads params[:code]. */
.ob-otp { display: flex; justify-content: center; gap: 8px; }
.ob-otp-box {
  width: 46px;
  height: 52px;
  text-align: center;
  font-family: var(--font-sans);
  font-size: 18px;
  font-weight: 600;
  color: var(--text-primary);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-small);
  background: var(--bg-surface);
}
.ob-otp-box:focus {
  border-color: var(--brand-primary);
  box-shadow: 0 0 0 3px var(--brand-subtle);
  outline: none;
}
/* ATT-171 — the DS field-error stroke, extended to the OTP boxes so a rejected
   code is answered AT the input like every other field in the app, instead of by
   a page banner. Lives here rather than in the canonical .form-field-error rule
   (which lists .form-input / .form-textarea / .form-select) so the DS block stays
   free of surface-prefixed classes.

   AFTER :focus deliberately, giving the error stroke the win at equal
   specificity. That is exactly how .form-field-error .form-input behaves — it is
   declared after .form-input:focus and overrides only border-color, so a focused
   field in error keeps the red stroke and still gets its focus glow. Mirrored
   rather than reinvented. */
.form-field-error .ob-otp-box { border-color: var(--status-error-subtle); }

/* Inline confirmation for a resend: same line, same position, no layout shift —
   the answer appears where the action was. */
.ob-resend-confirm { color: var(--brand-secondary); }

/* ATT-171 — these two had NO consumer until login's verify screen. They were
   written for a button_to-shaped resend, which signup then did not need (its
   resend is a link inside the form). Login cannot use a link: its resend must
   carry the email as a form param, and a nested <form> is invalid HTML — so the
   button_to shape these were built for is exactly what it needs.

   text-decoration was `underline`, which would have made login's resend the one
   underlined link in the arc; signup's .ob-link and the /styleguide/login mock
   both draw it plain. Changed rather than overridden, since nothing else renders
   it. */
.ob-resend-form { display: inline; margin: 0; }
.ob-resend-button {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--text-link);
  cursor: pointer;
  text-decoration: none;
}
.ob-resend {
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-sans);
  font-size: 12px;
  color: var(--text-muted);
  margin-top: 14px;
}

/* ── Brand screens: logo picker, voice profile, analysis result ──
   Promoted from the mock's .sgo-logo-* / .sgo-analyzing / .sgo-voice* /
   .sgo-sample* / .sgo-adjust. Token-based, so they re-theme with the dark scope. */
.ob-logo-row { display: flex; align-items: center; gap: 12px; }
.ob-logo-tile {
  width: 56px;
  height: 56px;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-small);
  background: var(--bg-elevated);
  color: var(--text-muted);
}
.ob-logo-tile svg { width: 20px; height: 20px; }
/* ATT-255 — the tile showing an actual logo rather than the placeholder icon.
   object-fit: contain with max-width AND max-height means the image fits its
   LONGEST side and keeps its aspect ratio, so a wide wordmark stays wide inside
   the square rather than being squashed to fit. No fixed w/h on the image.

   The 2px inset stops a logo with a hard edge sitting flush against the tile's
   own border. */
.ob-logo-tile img {
  max-width: calc(100% - 4px);
  max-height: calc(100% - 4px);
  object-fit: contain;
}

/* ── ATT-256 chunk 2: the same tile, as an avatar ──────────────────────────
   A MODIFIER, NOT A SECOND TILE, so the size, the border and the background all
   still come from one place. Only two things differ between a brand mark and a
   face, and both are here.

   NOTE FOR THE .ob-* RENAME: this is the FOURTH surface to use this cluster
   (wizard step 1, wizard step 2, Settings -> Brand, Settings -> Brand voice, and
   now users/_profile_fields — which is itself rendered by two pages). ATT-255's
   report flagged that a neutral rename beats a fourth copy; it is deliberately
   NOT done here, because renaming across four surfaces inside a feature ticket
   turns a feature into a refactor, and ATT-268 reopens the wider avatar question
   one ticket later anyway. Do it there, or in its own ticket. */
.ob-logo-tile.is-round { border-radius: 50%; overflow: hidden; }

/* COVER, NOT CONTAIN, and this is the half the design mock's sketch missed.
   The rule above is object-fit: contain with a 2px inset — right for a logo,
   which is a wordmark that must fit its longest side and keep its aspect ratio.
   A face is the opposite: it should FILL the circle and be cropped, or a
   portrait photo sits as a letterboxed sliver with the tile's background showing
   through around it. Full bleed, so the 4px inset goes too. */
.ob-logo-tile.is-round img {
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  object-fit: cover;
}

/* The empty state. Initials rather than a silhouette (see the partial), so the
   tile holds text where the logo version holds a lucide glyph — which inherits
   .ob-logo-tile's own --text-muted and needs no colour of its own. */
.ob-logo-initials {
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: 18px;
  line-height: 1;
  color: var(--text-secondary);
}
/* Filename shown after picking, so the DS upload button gives the same feedback
   a browser-default file input would. */
.ob-file-name {
  font-family: var(--font-sans);
  font-size: 12px;
  line-height: 18px;
  color: var(--text-muted);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ob-analyzing {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 16px 0 10px;
  font-family: var(--font-sans);
  font-size: 13px;
  font-weight: 500;
  color: var(--brand-secondary);
}
.ob-analyzing svg { width: 14px; height: 14px; }

.ob-voice {
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-small);
  background: var(--bg-elevated);
  padding: 16px 18px;
}
/* The summary is a TEXTAREA dressed as a paragraph — edited in place, so a
   click lands the caret where it was aimed. Everything here exists to strip the
   control back to looking like body copy: the type must match .ob-sample-text's
   neighbours exactly, resize is off because JS grows it to fit (see
   #autosize — a scrollbar would give it away), and the negative inset keeps the
   text optically flush with the tones and samples below despite the padding
   that makes the hover/focus states land as a comfortable target. */
.ob-voice-summary {
  display: block;
  width: calc(100% + 16px);
  margin: -6px -8px 6px;
  padding: 6px 8px;
  border: 1px solid transparent;
  border-radius: var(--radius-small);
  background: transparent;
  font-family: var(--font-sans);
  font-size: 13px;
  line-height: 20px;
  color: var(--text-secondary);
  resize: none;
  overflow: hidden;
  cursor: text;
  transition: background 140ms ease, border-color 140ms ease;
}
/* The panel itself is --bg-elevated, so the field lifts to --bg-surface to be
   visible against it: lighter than the panel on light, recessed on dark, which
   is the conventional field treatment in each. */
.ob-voice-summary:hover { background: var(--bg-surface); }
.ob-voice-summary:focus {
  outline: none;
  background: var(--bg-surface);
  border-color: var(--brand-primary);
  box-shadow: 0 0 0 3px var(--brand-subtle);
}

/* Sits under the panel and doubles as the error slot for a rejected save. */
.ob-voice-hint {
  font-family: var(--font-sans);
  font-size: 12px;
  line-height: 18px;
  color: var(--text-muted);
  margin: 8px 0 0;
}
/* ATT-246 — was --status-error-default, which is the pale pink FILL (#f9e3e2),
   not the error TEXT colour. A rejected save's message rendered near-white on a
   white card — invisible, on the one line whose job is to explain the rejection.
   --status-error-text (#c12b25) is what every other error message in the app
   uses (.form-error-banner, the field-error line, the add-link form). */
.ob-voice-hint.is-error { color: var(--status-error-text); }
/* The confirmation, matching the completion badge's green (--brand-secondary,
   Attune Emerald) rather than introducing a second success colour. The success
   TOKENS are a trap here: --status-success-default is a pale FILL, and only
   --status-success-subtle is the readable green — which is the same #059669 the
   brand token names, so the brand token says what it is. It reverts on a timer,
   so this is never the resting state. */
.ob-voice-hint.is-success { color: var(--brand-secondary); }
.ob-voice-tones { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }

.ob-sample { border-top: 1px solid var(--border-primary); padding-top: 10px; }
.ob-sample-label {
  font-family: var(--font-sans);
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  margin-bottom: 2px;
}
.ob-sample-text {
  font-family: var(--font-sans);
  font-size: 12px;
  line-height: 18px;
  color: var(--text-muted);
  margin: 0;
}
/* The mock shows ONE sample phrase; extraction returns several. Without this
   they butt together into a single block at 18px line-height. */
.ob-sample-text + .ob-sample-text { margin-top: 6px; }

.ob-adjust {
  display: inline-block;
  margin-top: 12px;
  font-family: var(--font-sans);
  font-size: 13px;
  color: var(--text-link);
  text-decoration: none;
}

/* ── Footer: theme toggle + placeholder legal links ── */
.ob-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding: 20px 24px 28px;
  border-top: 1px solid var(--border-primary);
}
.ob-footer-links {
  display: flex;
  gap: 16px;
  font-family: var(--font-sans);
  font-size: 12px;
  line-height: 18px;
  color: var(--text-muted);
}
.ob-footer-link { color: var(--text-muted); text-decoration: none; }
.ob-footer-link:hover { color: var(--text-secondary); }
.ob-theme-label {
  margin-left: 8px;
  font-family: var(--font-sans);
  font-size: 13px;
  font-weight: 500;
  color: var(--text-secondary);
}

/* ── Dark tokens, scoped to the wizard subtree ──
   Mirrors the mock's .sgo-root:has(#sgo-theme:checked) block. --brand-subtle is
   intentionally NOT overridden: it keeps its light DS value (#f5f3ff) so the
   wordmark/logomark can use it as the light "brand/subtle" mark on dark
   (Figma DS 480:84). Dark purple backgrounds that used to lean on it get their
   own explicit tints below. */
.ob-root[data-theme="dark"] {
  --bg-page:         #121826;  /* neutrals-900 */
  --bg-surface:      #1d2531;  /* cards / tiles */
  --bg-elevated:     #262e3c;  /* inputs / strip / segmented track */
  --bg-subtle:       #1d1636;
  --brand-primary:   #6d3ce8;  /* purple-500 — lifted for contrast on dark */
  --brand-hover:     #967fee;
  --brand-pressed:   #a78bfa;
  --brand-secondary: #10b981;  /* emerald-500 */
  --text-primary:    #f3f4f6;  /* neutrals-100 */
  --text-secondary:  #d1d5db;  /* neutrals-300 */
  --text-muted:      #9da3ae;  /* neutrals-400 */
  --text-link:       #967fee;
  --border-primary:  #2f3644;
  --palette-neutrals-100: #262e3c;
  --palette-neutrals-200: #313a49;
  --palette-neutrals-300: #414b5c;
}

/* The rules that hardcode a colour and so cannot ride the token swap. Each one
   is a real DS component being corrected for a dark ground, not a new style. */
/* .btn-elevated-outline hardcodes palette-white for its fill. */
.ob-root[data-theme="dark"] .btn-elevated-outline { background: transparent; }
/* Same trap, same fix: .btn-elevated-destructive-outline fills with
   --palette-neutrals-white, a PRIMITIVE that correctly stays white on dark — so
   without this it renders a white button on a dark card. The filled variant needs
   nothing: --status-error-subtle reads on both grounds. */
.ob-root[data-theme="dark"] .btn-elevated-destructive-outline { background: transparent; }
/* Segmented control: keep the active pill lighter than its track. */
.ob-root[data-theme="dark"] .seg-input:checked + .seg-label { background: #39414f; }
/* .pill-label-brand fills with --brand-subtle, which stays light by design. */
/* Focus ring on dark. The DS focus state is
   `box-shadow: 0 0 0 3px var(--brand-subtle)`, and --brand-subtle deliberately
   stays light (#f5f3ff) so the wordmark can use it as the light mark on dark —
   which makes that ring read as a heavy near-white halo here. Swap it for the
   lifted purple at low alpha: same shape, legible against the dark ground.
   Applies to every focusable control in the wizard, not just the OTP boxes. */
.ob-root[data-theme="dark"] .form-input:focus,
.ob-root[data-theme="dark"] .form-textarea:focus,
.ob-root[data-theme="dark"] .form-select:focus,
.ob-root[data-theme="dark"] .ob-otp-box:focus,
/* Composite controls carry their OWN focus rule rather than inheriting
   .form-input's, so each needs naming here: .input-prefix (website URL,
   escalation URL) rings on :focus-within, and the shared select's trigger rings
   on [aria-expanded="true"] while its menu is open. Missing either leaves that
   one control with the light halo while its neighbours look correct. */
.ob-root[data-theme="dark"] .input-prefix:focus-within,
/* The inline voice summary is a bare textarea, so it carries its own focus rule
   too and needs naming here for the same reason. */
.ob-root[data-theme="dark"] .ob-voice-summary:focus,
.ob-root[data-theme="dark"] .ctl-trigger[aria-expanded="true"] {
  border-color: var(--brand-primary);
  box-shadow: 0 0 0 3px rgba(141, 108, 243, 0.22);
}
.ob-root[data-theme="dark"] .pill-label-brand {
  background: rgba(141, 108, 243, 0.22);
  color: var(--palette-purple-200);
}
/* .form-error-banner fills with --status-error-default (#f9e3e2), a light pink
   meant for a light ground — on dark it lands as a near-white block. The
   wizard is the first surface to render this DS component on dark; the tint
   keeps the same red hue at the same job, legible against the card. Scoped, so
   every other form using this partial is untouched. */
.ob-root[data-theme="dark"] .form-error-banner {
  background: rgba(221, 82, 76, 0.16);
  border-color: rgba(221, 82, 76, 0.45);
  color: #f2a6a2;
}

/* .form-hint-error is --status-error-text (#c12b25), a red drawn for a light
   ground. MEASURED on this wizard's dark card (#1d2531) it is 2.67:1 — under
   the 4.5:1 a body-sized message needs, and it was already shipping that way on
   step 5's plan error. The same #f2a6a2 the banner rule above uses on dark
   measures 7.90:1 against the card and 9.08:1 against the page.

   Scoped to .ob-root[data-theme="dark"], so the sixteen light-ground call sites
   keep --status-error-text unchanged. It reaches step 5's plan error as well as
   step 7's — the same latent problem, and the same fix. */
.ob-root[data-theme="dark"] .form-hint-error { color: #f2a6a2; }

/* The invite error sits between the sub-line and the Teammates label, so it
   needs the gap the banner's own margin used to provide. */
.ob-invite-error { margin: 0 0 12px; }
/* Selected tier row fills with --brand-subtle for the same reason and needs the
   same correction — otherwise the chosen plan is a near-white block on dark. */
.ob-root[data-theme="dark"] .ob-tier:has(.ob-tier-input:checked) {
  background: rgba(141, 108, 243, 0.22);
}
/* The Live Status logomark fills with --brand-primary, which on dark is the
   lifted purple — too dark against the strip. purple/100 keeps the mark legible
   while staying in the brand family. Scoped to the wizard, so Product Show and
   Collection Show strips are untouched. */
.ob-root[data-theme="dark"] .ls-wave,
.ob-root[data-theme="dark"] .ls-glow,
.ob-root[data-theme="dark"] .ls-star {
  fill: var(--palette-purple-100);
}

/* Wordmark + logomark switch to the light brand/subtle mark on dark. */
.ob-root[data-theme="dark"] .ob-mark,
.ob-root[data-theme="dark"] .ob-wordmark { color: var(--brand-subtle); }

/* ─── ATT-232: brand settings area (.set-*) ────────────────────────────────
   Promoted from the approved /styleguide/settings-billing proposal's
   page-scoped .sgs-* / .sgset-*.

   ONLY WHAT PHASE 2 RENDERS IS PROMOTED. The invoice rows and their pager, the
   payment-method row, the plan-change confirm and the cancellation block stay in
   the mock until the phase that ships them — a cluster of rules with no callers
   is indistinguishable from a cluster whose callers were deleted, and the next
   reader cannot tell which.

   Its own prefix, not .adm-* (the ADMIN console's) and not another surface's
   rows (CLAUDE.md prefix isolation). The shell is the REAL .shell / .shell-nav /
   .nav-items / .nav-item, so only two things here are genuinely new: the back
   link the settings nav adds, and a centred reading column — narrower than the
   app's full-bleed index pages because everything in this area is a sentence and
   a card rather than a table. */

/* ── The nav's back link ──
   The one structural addition the settings nav makes to the primary nav.
   .sgset-back's values, lifted rather than re-picked: an earlier pass reached for
   the rail's --text-ui-nav-550-* tokens because the link SITS in the rail, which
   is inference by category — it silently moved the weight from 500 to 550. */
.set-back {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  border-radius: var(--radius-small);
  font-size: 13px;
  line-height: 18px;
  font-weight: 500;
  color: var(--text-secondary);
  text-decoration: none;
  transition: color 0.12s;
}
/* THE LABEL TURNS BRAND ON HOVER — the dropdown MENU ITEM's treatment, not the
   kebab TRIGGER's plate fill. Every back link in the app answers the same way:
   "Back to app", "Billing" and the admin console's "Brands". The icon comes with
   it, since Lucide strokes with currentColor.

   The padding and radius stay: this sits in the settings nav above items that do
   carry plates, and the geometry is what keeps it in line with them. */
.set-back:hover { color: var(--brand-primary); }
.set-back svg { width: 14px; height: 14px; flex-shrink: 0; }

/* ── The content column ──
   ONE width for every settings page. The two mocks were drawn independently and
   landed on 720px (billing) and 787px (user management); 750 splits them, and
   sharing one value is what stops the area reading as two pages that happen to
   sit behind the same nav.

   CENTRED, while the header band above stays full width with its title at the
   page's 32px gutter. The heading and the content it names therefore do NOT
   share a left edge — that is the intended arrangement (Arash, 2026-08-14), so
   do not "fix" it by centring the header or left-aligning this. */
.set-col {
  max-width: 750px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
/* The stacked title block inside the header band: h1, plus a subtitle on the
   pages that carry one. */
.set-head { min-width: 0; }
.set-h1 {
  font-family:    var(--text-h1-font-family);
  font-weight:    var(--text-h1-font-weight);
  font-size:      var(--text-h1-font-size);
  line-height:    var(--text-h1-line-height);
  letter-spacing: var(--text-h1-letter-spacing);
  color: var(--text-primary);
  margin: 0;
}
.set-sub {
  font-family:    var(--text-body-small-font-family);
  font-weight:    var(--text-body-small-font-weight);
  font-size:      var(--text-body-small-font-size);
  line-height:    var(--text-body-small-line-height);
  letter-spacing: var(--text-body-small-letter-spacing);
  color: var(--text-muted);
  margin: 4px 0 0;
}
/* The label over a group of cards. .sgs-section-title's values. */
.set-section-title {
  font-family: var(--font-sans);
  font-size: 13px;
  font-weight: 600;
  color: var(--text-primary);
  margin: 16px 0 0;
}

/* ── Card ──
   No generic card class exists in the DS: .brief-empty-card is an empty state,
   not a container. These ARE its border and radius, so this inherits a treatment
   already in the app rather than introducing a second one. */
/* ATT-255 — A CARD LEAD. .set-card-meta already exists and is already used for
   a card's explanatory line, but only ever INSIDE .set-card-row (billing's rows,
   brand voice's regenerate card), where the row supplies the spacing.

   Used as a DIRECT CHILD it leads the card, and nothing spaces it: the card's
   field rhythm comes from `.set-card .form-field + .form-field`, an
   adjacent-sibling rule, and a paragraph before the first field breaks that
   chain rather than joining it. 20px is the value that rule already uses, so the
   lead sits on the same rhythm as the fields below it.

   `>` DELIBERATELY: every existing .set-card-meta is nested deeper, inside
   .set-card-row / .set-card-main, and none of them should move. */
.set-card > .set-card-meta { margin: 0 0 20px; }

.set-card {
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-small);
  background: var(--bg-surface);
  padding: 16px 20px;
}
.set-card-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}
.set-card-main { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.set-card-title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  font-weight: 600;
  color: var(--text-primary);
}
.set-card-meta {
  font-size: 13px;
  line-height: 18px;
  color: var(--text-muted);
}
/* The right-hand cell. A DIRECT flex child of .set-card-row, never nested in an
   intermediate shrink:0 wrapper (Safari, see .pc-row). Empty until ATT-232's
   mutations land; the card's proportions come from it either way. */
.set-card-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }

/* ── ATT-232 4a: the failed-payment alert on the card row ──
   .cpt-thin-banner's treatment, value for value, under this surface's prefix —
   the collection products tab's thin-evidence strip, which is the pattern this
   was asked to match. Promotion is a rename, as it was for .sgu-* → .set-row.

   TWO DELIBERATE DIFFERENCES:
   * NOT DISMISSABLE. No dismiss button, no `dismissable` controller. Thin
     evidence is advice a merchant may reasonably wave away; a failed payment is
     not, and a strip that can be closed on the one page that fixes it would be
     closed and forgotten.
   * The padding-top on the wrapper is 28px there because it sits in a list with
     its own gap; here the wrapper is a .set-col flex child whose 16px gap adds
     to it, which reads correctly and is why it is not re-tuned.

   #fffdf6 is copied, not chosen: the collections cluster records it as "the
   Figma's bespoke near-white warm tint, no DS token". A different amber here
   would be a second bespoke value, which is worse than one repeated with a
   pointer to its source. */
.set-alert-wrap {
  position: relative;
  padding-top: 28px;
}
/* Card on top, covering the strip's lower edge. */
.set-alert-wrap .set-card {
  position: relative;
  z-index: 1;
}
.set-alert {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 0;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px 16px;
  background: #fffdf6;
  border: 1px solid var(--status-warning-subtle);
  border-bottom: 0;
  border-radius: var(--radius-small) var(--radius-small) 0 0;
}
.set-alert-text { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
.set-alert-icon {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  color: var(--status-warning-subtle);
}
.set-alert-message {
  min-width: 0;
  font-family:    var(--text-ui-badge-font-family);
  font-weight:    var(--text-ui-badge-font-weight);
  font-size:      var(--text-ui-badge-font-size);
  line-height:    var(--text-ui-badge-line-height);
  letter-spacing: var(--text-ui-badge-letter-spacing);
  color: var(--status-warning-subtle);
}

/* ── Payment method row ──
   The glyph carries the payment TYPE, which the copy does not. A FIXED 20px
   slot, so card, bank and wallet rows align down the same left edge whatever
   glyph lands in them — and so licensed brand marks could drop in later without
   a reflow. */
.set-pm { display: flex; align-items: center; gap: 14px; min-width: 0; }
.set-pm-icon {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text-secondary);
}
.set-pm-icon svg { width: 16px; height: 16px; }

/* ── Invoice rows ──
   Fixed-basis cells as DIRECT flex children. NOT nested inside an intermediate
   flex-shrink:0 wrapper — that overflows in Safari while looking fine in Chrome,
   and is recorded in CLAUDE.md as a shipped bug. */
.set-invs {
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-small);
  overflow: hidden;
}
.set-inv {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 12px 20px;
  background: var(--bg-surface);
  font-size: 13px;
  line-height: 18px;
  color: var(--text-primary);
}
.set-inv + .set-inv { border-top: 1px solid var(--border-primary); }
.set-inv:hover { background: var(--bg-elevated); }
.set-inv-date   { flex: 0 0 110px; }
.set-inv-number { flex: 0 0 150px; color: var(--text-muted); font-family: var(--font-mono); font-size: 12px; }
.set-inv-amount { flex: 0 0 90px; font-weight: 500; }
.set-inv-status { flex: 0 0 90px; }
.set-inv-dl     { flex: 1 1 auto; display: flex; justify-content: flex-end; gap: 16px; }
/* PAY sits before the PDF link and outweighs it: on a row that owes money it is
   the reason the row is being read. Brand-coloured and semibold against the
   PDF's muted link, without becoming a button — the row is a list entry, not a
   form. */
.set-inv-pay {
  font-weight: 600;
  color: var(--brand-primary);
  text-decoration: none;
}
.set-inv-pay:hover { text-decoration: underline; }
/* The same affordance inline in a sentence, for the outstanding note. */
.set-inline-link { color: var(--text-link); text-decoration: underline; }
.set-inv-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--text-link);
  text-decoration: none;
  font-size: 13px;
}
.set-inv-link:hover { text-decoration: underline; }
.set-inv-link svg { width: 14px; height: 14px; }
.set-inv-head {
  display: flex;
  gap: 16px;
  padding: 0 20px 8px;
  font-size: 12px;
  font-weight: 500;
  color: var(--text-muted);
}
/* "This list may be out of date" — quiet, above the rows. Not a .cr-banner:
   nothing has failed for the merchant and nothing needs their attention, so the
   warning treatment would overstate it. */
.set-inv-note {
  font-family: var(--font-sans);
  font-size: var(--text-caption-font-size);
  line-height: var(--text-caption-line-height);
  color: var(--text-muted);
  margin: 0 0 10px;
}

/* ── Pager ──
   .aud-pagination's values (the audiences Consumers panel, ATT-157 — the app's
   only other pager) under this surface's prefix, so there is one pagination
   shape rather than two. */
.set-pagination {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 16px;
  padding-top: 8px;
}
.set-page-info {
  flex: 1 1 0;
  font-family: var(--font-sans);
  font-size: var(--text-caption-font-size);
  color: var(--text-muted);
}
/* ATT-242 — see .aud-page-btn: the icon sizing now comes from .btn-elevated-sm. */

/* ── Page chrome, shared by BOTH settings pages ──
   .adm-page / .adm-page-head / .adm-body / .adm-toolbar's values under this
   surface's prefix. A RENAME, not a redesign — the same relationship
   styleguide/_user_row_styles describes for the row itself. Not `.adm-*` reused
   directly: that cluster is the ADMIN console's, and a merchant surface reaching
   into it is the coupling the prefix convention exists to prevent.

   The header band is FULL WIDTH with the horizontal padding INSIDE each section,
   so its bottom stroke spans the content rail edge to edge while the body below
   is held to .set-col's measure. Billing used to be a centred column with no
   header band at all, which made the two pages read as different surfaces. */
.set-index { padding: 32px 0; }
/* FULL WIDTH AND LEFT-ALIGNED: the band spans the content rail edge to edge,
   stroke included, and its title sits at the page's 32px gutter rather than on
   .set-col's centred measure. */
.set-index-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 0 32px 20px;
  border-bottom: 1px solid var(--border-primary);
}
.set-title-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

/* ATT-257 — the email row: a value and its control on one line, inside a
   .form-field (which is a flex COLUMN, so the pair needs a row of its own).
   10px is the mock's gap.

   ITS OWN NAME rather than reusing .set-title-row above, which has the same three
   declarations: that class is the page heading's row, and a name that says
   "title" on a form field is the kind of small lie this project has renamed twice
   (admin_initials -> user_initials -> initials_for). Three declarations is a
   cheaper duplicate than a wrong name. */
.set-email-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

/* .form-input is width: 100%, so inside the row above it claims the whole line
   and wraps its neighbour onto the next one — which is what the code field did.
   Letting it SHRINK keeps the pair inline; min-width: 0 is what makes shrinking
   below the intrinsic width legal in a flex row. */
.set-email-row .form-input { flex: 1 1 auto; min-width: 0; max-width: 260px; }

/* Resend and Cancel are button_to, which wraps each in a <form> — a block
   element, so they stacked onto their own lines instead of sitting in the hint
   sentence the way the mock draws them. Scoped to .form-hint rather than given a
   class, because the fix is about where these forms SIT, not what they are.
   .cr-banner-form does the same job for the banner's inline action. */
.form-hint form { display: inline; margin: 0; }

/* TWO of them sit in the pending email hint, and a collapsed whitespace node is
   the only thing between them — so "Resend Cancel" read as one underlined run-on
   in the screenshot. A word space is not enough separation between two links
   that do opposite things. */
.form-hint form + form { margin-left: 10px; }
.set-body { padding: 32px 32px 0; }
.set-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 14px;
}
/* The search input is wrapped in a real GET form, so the FORM is the flex child
   and carries the width. .form-input is width:100%, so the input fills it. */
.set-search-form { flex: 0 1 280px; }
.set-search { max-width: 280px; }
.set-showing {
  font-family: var(--font-sans);
  font-size: var(--text-caption-font-size);
  color: var(--text-muted);
  white-space: nowrap;
}
.set-search-empty {
  padding: 40px 0;
  text-align: center;
  font-family: var(--text-body-small-font-family);
  font-size: var(--text-body-small-font-size);
  line-height: var(--text-body-small-line-height);
  color: var(--text-muted);
}

/* ── Member rows ──
   .sgu-*'s values, which are themselves .adm-brand-row's copied rather than
   re-picked. Flat, with every cell a DIRECT flex child — not nested inside an
   intermediate shrink:0 wrapper, which overflows in Safari while looking fine in
   Chrome (.pc-row, a bug already paid for once).

   The hover border IS kept even though nothing is clickable yet: it is the row
   treatment the mock approved, and ATT-187's kebab lands in the same row. Two
   rules are deliberately NOT promoted because nothing calls them —
   .sgu-row.is-deactivated (there is no deactivated column on users) and
   .sgu-quiet (no kebab yet). They arrive with the feature that needs them. */
.set-list { display: flex; flex-direction: column; gap: 8px; }
.set-row {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 14px 16px;
  border: 1px solid var(--palette-neutrals-200);
  border-radius: var(--radius-small);
  background: var(--bg-surface);
  transition: border-color 0.12s;
}
.set-row:hover { border-color: var(--palette-purple-300); }
/* ATT-259 — .um-row.is-deactivated's value (application.css:7756), promoted with
   the state it describes. A deactivated row stays READABLE but visibly out of
   service: border and fill hold so the list keeps its rhythm, and only the text
   recedes. Anything heavier (a greyed fill, a struck name) reads as an error
   rather than as a deliberate state, and the row is retained precisely so it can
   still be read and acted on.

   This is the second of the two rules application.css:7497-7499 left unpromoted
   "because nothing calls them". Chunk 2 took .sgu-quiet; this takes the other,
   and its stated reason — "there is no deactivated column on users" — has been
   false since ATT-199 added it. The verb that makes the state REACHABLE for a
   brand user is this chunk's. */
.set-row.is-deactivated .set-row-name { color: var(--text-muted); }

/* ── ATT-268: the member's avatar ──────────────────────────────────────────
   A DIRECT FLEX CHILD of .set-row, first cell. 32px because the row's text block
   is about 43px tall, so it reads at list scale without competing with the name.

   ITS OWN PREFIX, deliberately. This is the THIRD circle in the app —
   .nav-avatar 26px, this 32px, .ob-logo-tile.is-round 56px — all with the same
   fill, initials treatment and object-fit, and consolidating them into a shared
   .avatar is a decision left to a human rather than taken in passing here. The
   prefix-isolation rule says each surface owns its cluster, and .nav-avatar is
   shipped and established. Revisit if a FOURTH appears. */
.set-row-avatar {
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--palette-purple-200);
  color: var(--text-on-brand);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  font-size: 12px;
  line-height: 1;
  overflow: hidden;
}

/* cover, not contain: a face fills the circle and is cropped. Same reasoning as
   .ob-logo-tile.is-round img — a letterboxed portrait with the fill showing
   around it is what contain would give. */
.set-row-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* THE AVATAR RECEDES WITH THE NAME. The rule above quiets the name only, because
   the name was the only thing there was to quiet; a full-colour face beside grey
   text would be the brightest thing on a row that is meant to read as out of
   service. Border and fill still hold, per that rule's own reasoning — the row
   stays readable and actionable, it just stops looking live. */
.set-row.is-deactivated .set-row-avatar { opacity: 0.5; }

.set-row-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 5px; }
.set-row-name-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.set-row-name {
  font-family: var(--text-body-bold-font-family);
  font-weight: var(--text-body-bold-font-weight);
  font-size:   var(--text-body-bold-font-size);
  color: var(--text-primary);
}
/* The meta line carries email, role and activity separated by dots — the role is
   an ATTRIBUTE and lives here, while the pill on the right is STATE. Mixing the
   two in one column makes both harder to scan, which is the argument the
   approved row was drawn on. */
.set-row-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-family: var(--font-sans);
  font-size: var(--text-caption-font-size);
  color: var(--text-muted);
}
.set-row-dot { color: var(--palette-neutrals-300); }
/* ATT-259 — "You", beside your own name. A NEW RULE because the sheet has no
   generic muted-inline-text class: `grep -n "^\.text-muted\|^\.muted\b\|^\.optional\b"`
   finds none, and without a treatment this inherits body size and --text-primary,
   which reads as a second word of the name rather than an annotation on it.

   .set-row-meta's OWN THREE TOKENS, not re-picked — caption size, --text-muted,
   the sans stack. The meta line directly below is the row's quiet register, and
   this belongs to the same one. font-weight is reset because the sibling it sits
   beside is the bold name and a span inherits nothing from a sibling, but the
   name row itself is where a future bold could land. */
.set-row-you {
  font-family: var(--font-sans);
  font-size: var(--text-caption-font-size);
  font-weight: 400;
  color: var(--text-muted);
}
/* State cell: right-aligned, a DIRECT flex child. align-items:center on the row
   does the vertical centring; this only has to refuse to stretch or shrink. */
.set-row-state { flex: 0 0 auto; display: flex; align-items: center; }

/* ── ATT-259: the actions cell and its hover-revealed kebab ──
   .um-actions / .um-quiet's values (application.css:7672-7679), promoted rather
   than re-picked, with one difference forced by this surface.

   THE DIFFERENCE: A FIXED BASIS. .um-actions is `flex-shrink: 0` with no width,
   which is right on the staff list where EVERY row has a kebab. Here they do
   not — an invitation has no verb until chunk 4, and a Member has none on any
   row — so an auto-width cell would collapse to 0 on those rows and shift their
   state pill 22px right of everyone else's. 22px is .jh-kebab-btn's own width
   (application.css:4629), so the gutter is the trigger's, reserved whether or
   not the trigger is there. Same argument the design used for putting a pill on
   every row rather than only the exceptional ones: a ragged column is worse than
   a quiet one.

   NO FORM RESET HERE. .um-actions declares
   `.um-actions .ps-menu-dropdown form { margin: 0; display: contents; }` at
   :7683, and it is REDUNDANT — the base rule at :3388 is
   `.ps-menu-dropdown form { margin: 0; display: contents; }` with identical
   declarations, and it already matches any form inside the dropdown whatever the
   ancestor. The .um- copy only adds specificity. Not repeated here. (The
   redundant copy is left alone: it is .um-*'s and this chunk does not touch that
   cluster.) */
.set-row-actions {
  flex: 0 0 22px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
}
/* Hover-revealed, .um-quiet's opacity and transition. focus-within is
   load-bearing rather than thorough: it is the only way a keyboard user ever
   reaches the trigger, where hover never fires.

   `display: flex` IS THE ONE VALUE NOT COPIED FROM .um-quiet, and it is a bug
   fix rather than a preference. .um-quiet has no display, so as a flex item it
   blockifies to `block` and establishes an INLINE formatting context — and
   .jh-kebab-wrap inside it is `inline-flex` (:4624), which baseline-aligns and
   leaves descender space beneath. Measured: the wrapper is 22px, the line box it
   sits in is 24px, all 2px below it, so the trigger's centre lands 1px ABOVE the
   pill's beside it. Making this a flex container removes the inline formatting
   context entirely, and the two centres agree exactly.

   THE SAME 1px OFFSET IS LIVE ON /admin/staff TODAY — measured on .um-quiet, not
   inferred from this one. It is left alone here because .um-* is that surface's
   cluster and this chunk does not touch it; the fix there is this same line. */
.set-quiet {
  opacity: 0;
  transition: opacity 0.12s;
  display: flex;
  align-items: center;
}
.set-row:hover .set-quiet,
.set-row:focus-within .set-quiet { opacity: 1; }

/* ── ATT-259 chunk 5: the Invite modal's rows ──
   .ob-invite-*'s values (application.css:6645-6713), promoted rather than
   re-picked, with the wizard's two breakpoints carried across — a teammate is
   the same five controls at the same measure on both surfaces, and the mock's
   own note says so. Its .sgu-invite-row copy has 28px in the last column against
   the shipped 32px; the shipped value wins, as it did for every other promotion
   in this ticket.

   ITS OWN CLUSTER rather than reaching into .ob-*, which is the onboarding
   shell's — the same rule that gave chunk 2 .set-quiet and chunk 3
   .set-row.is-deactivated. */
.set-invite-row {
  display: grid;
  grid-template-columns: 1fr 1fr 1.8fr 132px 32px;
  gap: 10px;
  align-items: center;
}
/* display:contents, so the two name inputs are direct items of the row grid
   above rather than a nested pair. The wrapper earns its place at the narrow
   breakpoint, where it becomes a real grid again. */
.set-invite-names { display: contents; }

/* ── THE LIST SCROLLS SO THE MODAL DOES NOT GROW ──
   .ps-modal is capped at 90vh and .ps-modal-body already scrolls, so the modal
   could never exceed the viewport. What it could do is push .ps-modal-footer out
   of reach: shared/_modal wraps only the yield, so every caller's footer sits
   INSIDE that scrolling body, and "Send invites" would scroll away once enough
   teammates were added.

   Bounding the ROWS keeps the body's height fixed and the footer where it is.
   The precedent is .ps-modal-body turbo-frame#collection_product_picker, whose
   comment gives the same reason: "so the overlay height never changes".

   EIGHT ROWS, AND NOT A BARE VIEWPORT FRACTION. 50vh alone let a tall window
   show a dozen rows before scrolling, which is more of a form than anyone reads
   at once — and past that the modal hits its own 90vh cap and the BODY starts
   scrolling too, giving two nested scroll regions for one list. Eight is the
   number of rows the list holds before it takes over the scrolling itself.

   8 * 36px (a .form-input) + 7 * 10px (the gap) + 4px (this element's own
   padding, which counts toward scrollHeight) = 362px. Written out rather than as
   a magic number so the next person can change the row count.

   min() against 50vh keeps the short-window case honest: on a laptop with a
   toolbar open, half the viewport is still the ceiling. -2px/2px of padding
   keeps a focus ring from clipping against the scroll edge. */
.set-invite-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* ── AND THE OVERFLOW ARRIVES ONLY WHEN THE LIST NEEDS IT ──
   The overflow used to live on .set-invite-list itself, and it could not: the
   role control is shared/_select, whose menu is absolutely positioned below the
   trigger and escapes the list. An overflow box either COUNTS that menu — a
   closed .popover is `visibility: hidden`, not `display: none`, so 82px of it
   still took part in layout and put a scrollbar on a TWO-row list — or CLIPS it
   on open, which is what the closed-menu fix then exposed.

   repeater#scrollAfterValue adds .is-scrollable past the eighth row instead, so
   for every count anyone will actually use there is no scroll container at all
   and the menu is simply not involved. Past eight the list scrolls and the
   footer stays put, which is the whole point of bounding it.

   KNOWN, AND NOT FIXED HERE: past eight rows the bottom row's menu is clipped
   again, because the overflow is back. Making the menu escape the scroller needs
   position:fixed placement in select_controller.js — a shared component, and its
   own piece of work. */
.set-invite-list.is-scrollable {
  max-height: min(calc(8 * 36px + 7 * 10px + 4px), 50vh);
  overflow-y: auto;
  padding: 2px;
  margin: -2px;
}

/* Closed menus stay out of the scroll measurement once it IS scrolling, so the
   scrollbar reflects the rows and nothing else. Parked at top:0 inside its own
   row: 13px of padding and border, well inside a 36px row. NOT `display: none`,
   which would cost the fade — a display flip gives the opacity and transform
   transitions nothing to run from — where :not(.popover-open) simply stops
   matching on open and .select-menu's own top and height come back. */
.set-invite-list.is-scrollable .popover:not(.popover-open) {
  top: 0;
  max-height: 0;
  overflow: hidden;
}


@media (max-width: 820px) {
  /* Below the wide measure the five-up squeezes the email past usefulness.
     Names take their own line; email/role/remove keep the three-up. */
  .set-invite-row { grid-template-columns: 1fr 132px 32px; }
  .set-invite-names {
    display: grid;
    grid-column: 1 / -1;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
  }
  /* Two-line rows need more separation than the 10px inside one, or two
     teammates read as four unrelated fields. */
  .set-invite-list { gap: 20px; }
}
@media (max-width: 560px) {
  /* The role select and remove button keep their widths; the email takes the
     rest. */
  .set-invite-row { grid-template-columns: 1fr 32px; }
  .set-invite-row .control { grid-column: 1 / -1; order: 1; }
  .set-invite-names { grid-template-columns: 1fr; }
}

/* ATT-259 — the role control is the DS popover select (.ctl-trigger), not a
   .form-select, so .form-field-error's own rule does not reach it and one cell
   in a reddened row stayed grey. SURFACE-SCOPED rather than added to the DS
   rule: every other error form in the app uses .form-select, and widening the
   shared selector to cover a control none of them render is a change they would
   inherit without asking for it. */
.set-invite-row.form-field-error .ctl-trigger { border-color: var(--status-error-subtle); }

/* ATT-264 — the wizard's invite row needs the identical rule for the identical
   reason: its role control is a .ctl-trigger too. Measured on both grounds
   before shipping, because the wizard is the dark surface: --status-error-subtle
   (#dd524c) is 3.50:1 against the dark input fill (--bg-elevated #262e3c) and
   3.90:1 against the light one, both over the 3:1 a non-text boundary needs. So
   unlike the message colour, the stroke needs no dark correction. */
.ob-invite-row.form-field-error .ctl-trigger { border-color: var(--status-error-subtle); }

.set-invite-remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  transition: color 140ms ease, background 140ms ease;
}
.set-invite-remove:hover { color: var(--text-primary); background: var(--bg-elevated); }
.set-invite-remove svg { width: 16px; height: 16px; }

.set-add-row {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 12px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text-link);
  font-family: var(--font-sans);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
}
.set-add-row svg { width: 15px; height: 15px; }

/* The line under a modal title. .um-modal-sub's values (:7705) under this
   surface's prefix — the invite modal is the first on the brand side to need a
   subtitle, exactly as the staff one was on the console side. */
.set-modal-sub {
  font-family: var(--font-sans);
  font-size: var(--text-body-small-font-size);
  line-height: var(--text-body-small-line-height);
  color: var(--text-muted);
  margin: 0 0 16px;
}

/* ── ATT-232 4b: the Change plan page ──
   Its own route under Billing, so it needs a heading and a way back. The tier
   grid and the period toggle are the WIZARD'S (.ob-billing / .ob-tiers /
   .ob-tier / .segmented), used as-is rather than restyled — a merchant who chose
   a plan during onboarding meets the identical control here. */

/* The back link exists already for the settings nav, where it is a full-width
   flex row. Inside the header band it must not stretch, and the -10px pulls the
   label back into line with the h1 below it, cancelling the link's own padding.
   .set-head is not a flex container, so align-self would do nothing here. */
.set-head .set-back { display: inline-flex; margin: 0 0 4px -10px; }

/* The tier grid is three columns and wants 880px — the width ATT-205 settled on
   for exactly this content (.ob-card-wide). The settings column is 750px, so it
   bleeds 65px each side to get there. Arithmetic, not a guess: 750 + (2 x 65). */
.set-tierwrap { margin: 0 -65px; }
@media (max-width: 720px) {
  /* Stacked, the grid is one column and the bleed would push the cards out of
     line with everything else on the page. Mirrors .ob-card-wide's own reset. */
  .set-tierwrap { margin: 0; }
}

/* ── ATT-232 4c: the cancellation row ──
   Quiet and last, under a rule. .sgs-danger's values. The rule is what separates
   it from the controls a merchant touches every month; it carries no fill and no
   colour of its own, because the button is the only part that should read as
   heavy. */
.set-danger { margin-top: 8px; padding-top: 20px; border-top: 1px solid var(--border-primary); }
.set-danger-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.set-danger-copy { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
/* button_to wraps its button in a form, which would otherwise take a flex line
   of its own and push the button onto a second row. */
.set-danger-row form { display: inline-flex; margin: 0; }

/* Form action rows are right-aligned throughout the app. */
.set-form-actions { display: flex; justify-content: flex-end; gap: 8px; }

/* ATT-257 — the gap above Save profile, which the email field used to provide.
   Moving that field into its own card (a nested <form> is dropped by the browser)
   left the actions row sitting flush against the name row, because
   .set-form-actions carries no margin of its own and never has: on every other
   settings page it follows a .form-field and inherits the rhythm from there.

   SCOPED TO THIS ADJACENCY ON PURPOSE. `.um-name-row + .set-form-actions` matches
   only where a name row is the last thing in a card — which is Settings ->
   Profile and nowhere else (.um-name-row is rendered by users/_profile_fields,
   and the console's copy uses .adm-form-actions). A broader
   `.form-field + .set-form-actions` would have added 20px to Brand and Brand
   voice, which are shipped and correct.

   20px is the value .set-card .form-field + .form-field already uses, so the
   button sits on the same rhythm as the fields above it. */
.um-name-row + .set-form-actions { margin-top: 20px; }

/* What the brand is on, above the control it frames. Body copy rather than a
   page sub-head, so it sits a step down from the h1: secondary rather than
   muted, since it is the sentence the choice is made against. Centred to the
   segmented control below it, which is itself centred. */
.set-plan-lead {
  margin: 0 0 16px;
  text-align: center;
  font-family:    var(--text-body-small-font-family);
  font-weight:    var(--text-body-small-font-weight);
  font-size:      var(--text-body-small-font-size);
  line-height:    var(--text-body-small-line-height);
  letter-spacing: var(--text-body-small-letter-spacing);
  color: var(--text-secondary);
}

/* The tier grid bleeds past this column, so its bottom edge sits closer to the
   button than the 750px measure suggests — without this the CTA butts straight
   into the selected card. */
.set-plan-actions { margin-top: 24px; }

/* ── The confirm block ──
   .sgs-confirm's values. Reuses the semibold-lead pattern .cr-banner already
   has rather than introducing a second callout treatment. */
.set-confirm {
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-small);
  background: var(--bg-elevated);
  padding: 14px 20px;
  font-size: 13px;
  line-height: 20px;
  color: var(--text-primary);
}
.set-confirm-text { margin: 0; }
.set-confirm-text strong { font-weight: 600; }
.set-confirm-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }
/* Picking the plan you are already on is a slip, not a fault: same block, no
   alarm, and no action row to press. */
.set-confirm-muted { color: var(--text-muted); }

/* ─── ATT-199: user management (.um-*) ─────────────────────────────────────
   The Attune staff list in the admin console, promoted from
   styleguide/_user_row_styles (.sgu-*) — whose values are .adm-brand-row's,
   copied rather than re-picked. Promotion is a rename, not a redesign.

   ITS OWN CLUSTER, and NOT shared with the brand side. The recon for this ticket
   argued for a neutral prefix on the grounds that ATT-187 would render the same
   row; that was wrong. ATT-232 already promoted this same mock row for the brand
   surface as .set-* (settings/_member_row), so ATT-187 adds verbs to THAT and
   never touches this. Two clusters is the project rule anyway — each index
   surface owns its own row cluster, at the cost of more CSS — and it is why
   .set-* and .um-* diverge freely: this one has a kebab and a deactivated state,
   that one has a role segment and rows with no user behind them.

   Flat, with every cell a DIRECT flex child — never nested in an intermediate
   shrink:0 wrapper, which overflows in Safari while looking fine in Chrome
   (.pc-row, a bug already paid for once). */
.um-list { display: flex; flex-direction: column; gap: 8px; }
.um-row {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 14px 16px;
  border: 1px solid var(--palette-neutrals-200);
  border-radius: var(--radius-small);
  background: var(--bg-surface);
  transition: border-color 0.12s;
}
.um-row:hover { border-color: var(--palette-purple-300); }

.um-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 5px; }
.um-name-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.um-name {
  font-family: var(--text-body-bold-font-family);
  font-weight: var(--text-body-bold-font-weight);
  font-size:   var(--text-body-bold-font-size);
  color: var(--text-primary);
}
/* Email and activity, dot-separated. Staff carry no role segment — one role means
   a column that always reads the same, which is noise. The pill on the right is
   STATE; mixing attribute and state in one column makes both harder to scan. */
.um-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-family: var(--font-sans);
  font-size: var(--text-caption-font-size);
  color: var(--text-muted);
}
.um-dot { color: var(--palette-neutrals-300); }

/* State cell: right-aligned, ahead of the kebab, a DIRECT flex child.
   align-items:center on the row does the vertical centring; this only has to
   refuse to stretch or shrink. */
.um-state { flex: 0 0 auto; display: flex; align-items: center; }

.um-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
/* The kebab is hover-revealed. Unlike the brand row there is no second,
   always-visible control beside it — a staff row has no equivalent of "enter
   brand", so every action on it is a quiet one. focus-within keeps it reachable
   by keyboard, where hover never fires.

   ── ATT-259 chunk 3: A SANCTIONED CROSS-CLUSTER FIX ──
   `display: flex; align-items: center` is not a style choice, it is a one-line
   defect fix, and it is the only edit ATT-259 makes outside .set-* and settings/.

   WITHOUT IT this span has no `display`, so as a flex item it blockifies to
   `block` and establishes an INLINE formatting context — and .jh-kebab-wrap
   inside it is `inline-flex` (:4624). An inline-level box baseline-aligns, so the
   line box is 2px taller than the 22px wrapper and both pixels sit BELOW it,
   putting the trigger's centre 1px ABOVE the pill's beside it. Measured on this
   markup, not inferred: .um-quiet h=24 against a 22px child, pill mid 74.5 vs
   kebab mid 73.5. After: both 74.5, delta 0.00px.

   WHY CROSS THE PREFIX BOUNDARY AT ALL, when the rule is that each surface owns
   its cluster: the defect was found by measuring .set-quiet's twin during chunk
   2, is one line, and is visible on a shipped page. Leaving a known, measured,
   one-line defect in place because of a boundary rule is worse than crossing the
   boundary once with the reason written down. .set-quiet carries the identical
   fix and the identical comment. */
.um-quiet {
  opacity: 0;
  transition: opacity 0.12s;
  display: flex;
  align-items: center;
}
.um-row:hover .um-quiet,
.um-row:focus-within .um-quiet { opacity: 1; }
/* button_to wraps its button in a form; without this the two kebab items that
   are forms would stack as blocks at their own intrinsic width instead of
   filling the menu like the link items beside them. */
.um-actions .ps-menu-dropdown form { margin: 0; display: contents; }

/* A deactivated row stays readable but visibly out of service. Border and fill
   hold so the list keeps its rhythm; only the text recedes. */
.um-row.is-deactivated .um-name { color: var(--text-muted); }

/* ── The add-staff and profile forms ──
   .ob-row's grid, under this surface's own prefix rather than reaching into the
   wizard's (.ob-* is the onboarding shell's cluster). Same two-up pairing of
   first and last name, same 14px gutter. */
.um-name-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.um-name-row .form-field + .form-field { margin-top: 0; }
/* The card's "fields inside a card" rule is .form-field + .form-field, which does
   NOT match across this wrapper — so a field following the name pair sat flush
   against it. The wizard has the identical pair of rules for .ob-row, for the
   identical reason. On the wrapper rather than on a card class so it holds
   wherever the shared partial is rendered, including ATT-256's settings card. */
.um-name-row + .form-field,
.form-field + .um-name-row { margin-top: 20px; }

/* The line under a modal title. .sgu-modal-sub's values; the DS had no such class
   because no shipped modal had needed a subtitle before. */
.um-modal-sub {
  font-family: var(--font-sans);
  font-size: var(--text-body-small-font-size);
  line-height: var(--text-body-small-line-height);
  color: var(--text-muted);
  margin: 0 0 16px;
}

/* A value shown but not editable. Rendered as text rather than a disabled input,
   which still reads as a control someone failed to activate. */
.um-readonly {
  font-family: var(--font-sans);
  font-size: var(--text-body-font-size);
  line-height: var(--text-body-line-height);
  color: var(--text-primary);
  margin: 0;
}

/* ─── ATT-261: the dashboard shell (.db-*) ─────────────────────────────────
   ONE RULE, and it is a proven gap rather than a preference: there is no generic
   page container in this sheet. Six exist and every one is surface-owned
   (.adm-page, .aud-page, .focus-page, .jli-page, .pi-page, .ps-page), because
   each index surface owns its own cluster — so a new shell surface borrowing one
   of theirs is the coupling that rule exists to prevent.

   The card centres itself horizontally (max-width 500px, margin: 0 auto), so the
   only thing this adds is padding and VERTICAL centring — and that second part is
   specific to this surface rather than a preference. Every other empty state in
   the app sits under a header and a toolbar, so it lands where the eye already
   is; this one is the whole page, and pinned to the top of a full-height panel it
   reads as a rendering fault rather than as a deliberate screen. Only a
   screenshot shows that: the markup is identical either way. */
.db-page {
  padding: 32px;
  min-height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
