/* ══════════════════════════════════════════
   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;
  /* ATT-273 — the Live Status failed fill. --status-error-default (luminance
     0.8057) is far heavier than --status-success-default (0.9519), so a failed
     strip shouted while a completed one whispered. This is --status-error-subtle
     at 8% over white (0.8996), which closes the gap to 0.052. */
  --status-error-faint:        #fcf1f1;
  --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: Theme (ATT-314) ───
     One color per GOAL, so a theme reads the same on a journey step, a brief
     section and an insight category. Values from /styleguide/theme-colors,
     which is the source of truth for design (decided 2026-09-18); each text
     color measured on its own tint, not trusted:
       education 4.69:1   troubleshooting 5.94:1
       return    5.43:1   upsell          5.34:1
     all above the 4.5:1 AA threshold for normal text, none above 7:1 (AAA).

     THEME IS NOT STATUS. These never mark an error, a warning or success —
     that is what the --status-* tokens above are for, and the goal pills that
     borrowed them are what this replaces. The hues are chosen to be
     unmistakable for those: no red, no amber, no green at status saturation.

     No dark-mode values: the design system has no dark status tokens yet, and
     theme follows status rather than inventing its own dark scale.

     Replaced --node-message / --node-check-in / --node-branch / --node-handoff,
     which colored a step by its TYPE. The type is an icon now (ATT-309), and
     nothing had referenced those four since. */
  --theme-education-text:       #0e7490;
  --theme-education-tint:       #e2f3f7;
  --theme-troubleshooting-text: #3b5a8c;
  --theme-troubleshooting-tint: #e8eef6;
  --theme-return-text:          #a21caf;
  --theme-return-tint:          #f8e9fb;
  --theme-upsell-text:          #436f0c;
  --theme-upsell-tint:          #eef5e1;
  /* ATT-379 — the closing rating step and its replies (/styleguide/rating-step,
     approved 2026-10-14). An amber BROWN, darker and less saturated than the
     warning pill's yellow, so it does not read as a status. 6.0:1. */
  --theme-review-text:          #8a4b08;
  --theme-review-tint:          #fbf0e1;

  /* ─── 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;
}

/* ─── ATT-410: Reddit threads on the discovered card (.disco-quote*) ───
   A Reddit thread is an ordinary .disco-card plus its kept owner quotes,
   which open under the row through the shared `disclosure` controller
   (is-collapsed on the card). The quote block is shared with Source Details
   and the Insights popover: the commenter's words, linked to the thread,
   which OpenAI's web search terms require wherever a result is shown.
   Approved mock: /styleguide/reddit-sources. */
.disco-card--quotes { flex-wrap: wrap; }
.disco-sub {
  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);
  color: var(--text-muted);
}
.disco-quotes-toggle {
  display: inline-flex;
  align-items: center;
  padding: 0;
  border: 0;
  background: none;
  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);
  color: var(--brand-primary);
  cursor: pointer;
}
.disco-quotes-show,
.disco-quotes-hide { display: inline-flex; align-items: center; gap: 4px; }
.disco-quotes-toggle svg { width: 14px; height: 14px; }
.disco-card--quotes.is-collapsed .disco-quotes-hide,
.disco-card--quotes:not(.is-collapsed) .disco-quotes-show,
.disco-card--quotes.is-collapsed .disco-quotes { display: none; }
/* Full width under the row, aligned with the title: the score box (44px) plus
   the card's 20px gap. */
.disco-quotes {
  flex: 0 0 calc(100% - 64px);
  margin-left: 64px;
  padding-top: 12px;
  border-top: 1px solid var(--border-primary);
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.disco-quote-list { display: flex; flex-direction: column; gap: 12px; }
.disco-quote {
  margin: 0;
  padding-left: 12px;
  border-left: 2px solid var(--border-primary);
}
.disco-quote p {
  margin: 0 0 4px;
  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);
}
.disco-quote a {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  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);
  color: var(--brand-primary);
  text-decoration: none;
}
.disco-quote a:hover,
.disco-post a:hover { text-decoration: underline; }
/* The thread's original post: a filled block rather than a rule, so it reads
   as the opening the replies answer, not as another reply. */
.disco-post {
  margin: 0;
  padding: 12px 14px;
  border-radius: var(--radius-small);
  background: var(--bg-elevated);
}
.disco-post p {
  margin: 0 0 6px;
  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);
}
.disco-post a {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  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);
  color: var(--brand-primary);
  text-decoration: none;
}
.disco-post a svg { width: 12px; height: 12px; }
/* Names each part of an opened thread: "Original post", "Owner reports in
   the replies". */
.disco-part {
  margin: 0;
  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);
  color: var(--text-muted);
}
.disco-quote a svg { width: 12px; height: 12px; }
.disco-left-out {
  margin: 0;
  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);
}

/* 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.
   (The DS dropdown, .ctl-trigger, is reddened by its own rule beside the
   component, after its open state.)
   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 (beside
   .ctl-trigger) and .form-field-error .ob-otp-box: 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); }
/* ATT-307 — the number stepper owns its border the same way. */
.form-field-error .input-stepper { border-color: var(--status-error-subtle); }
/* ATT-337 — and the radio listbox, first used for a required choice by the
   journey start's "Which moment is this?". Until then no listbox could be in
   error, so the gap was never visible. */
.form-field-error .listbox { border-color: var(--status-error-subtle); }
/* ATT-304 — and the DS checkbox, first required by the Terms checkbox at
   signup. Its border is on the painted circle, not the (hidden) input. */
.form-field-error .pcf-check-visual { 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;
}

/* ATT-287 — the same banner as a warning, for a form reporting something the
   person can wait out or fix elsewhere (the test-send modal). Same shape as the
   error banner; only the three colours change, taken from the tokens
   .banner-warning already uses. */
.form-error-banner.form-banner-warning {
  border-color: var(--status-warning-subtle);
  background:   var(--status-warning-default);
  color:        var(--status-warning-subtle);
}
.form-error-banner a { color: inherit; font-weight: 500; text-decoration: underline; }

/* ── 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 carried the same guard (.opt, .jc-tab-panel, .jc-rail-empty,
   .jc-suggestion; the last three went with the ATT-309 rail, whose
   .jc-rail-suggestion carries it now) 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; }
/* ATT-314 — the caption and its theme pill travel together on the left,
   leaving the claim count on the right where it was. */
.brief-section-hdr-left { display: inline-flex; align-items: center; gap: 8px; }
.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.
   ATT-406: a brief finding is now .brief-finding (below); this chrome is kept
   for the group brief's Product differences, the one card still drawn with
   it, through .claim-card-warning. The confidence modifiers went with the old
   finding card. */
.claim-card {
  background: var(--bg-surface);
  border: 1px solid; border-left-width: 6px;
  border-radius: var(--radius-small); padding: 16px; margin-bottom: 16px;
}
/* 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;
}

/* 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); }

/* ─── ATT-406: the Brief tab's timing map, finding cards and contents
   (.brief-*). /styleguide/product-brief, approved 2026-10-21; the mock's
   .sgpb-* rules promoted. A brand reads "finding"; the code says "claim". ─── */

/* The contents travel down the whole brief: sticky inside .brief-body, at
   zero height, with the ticks hung in .ps-content's left padding. */
.brief-body { position: relative; }
.brief-toc { position: sticky; top: 8px; z-index: 3; height: 0; }
.brief-toc-ticks { position: absolute; left: -30px; top: 0; display: flex; flex-direction: column; gap: 8px; padding: 8px 8px 8px 4px; }
.brief-toc-tick { display: block; width: 12px; height: 2px; border-radius: 1px; background: var(--palette-neutrals-200); transition: width 140ms ease-out, background-color 140ms ease-out; }
.brief-toc-tick.is-current { width: 16px; background: var(--text-secondary); }
.brief-toc-panel {
  position: absolute; left: -30px; top: -8px; width: 240px;
  padding: 8px; border-radius: var(--radius-small); background: var(--bg-surface);
  box-shadow: 0 0 0 1px var(--border-primary), var(--shadow-container-elevated-bg);
  opacity: 0; visibility: hidden; transform: translateX(-4px);
  transition: opacity 0.14s ease, transform 0.14s ease, visibility 0.14s;
}
.brief-toc:hover .brief-toc-panel, .brief-toc:focus-within .brief-toc-panel { opacity: 1; visibility: visible; transform: translateX(0); }
.brief-toc-item {
  display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 8px;
  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-secondary); text-decoration: none;
}
.brief-toc-item:hover { background: var(--palette-neutrals-50); color: var(--text-primary); }
.brief-toc-item.is-current { color: var(--text-primary); font-weight: 500; }
.brief-toc-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.brief-toc-count { font-family: var(--mono); font-size: 12px; color: var(--text-muted); }
.brief-section, .brief-timing { scroll-margin-top: 16px; }

/* The timing map, after attune.to's How it works friction map: a row per
   timed finding, a bar across the days it matters in its section's theme
   color. The label column and the first week's share of the track
   (BriefsHelper: 7 of the first 30 days' 76%) set the band and the axis. */
.brief-timing { --brief-timing-label: 260px; --brief-timing-gap: 16px; margin: 0 0 32px; padding: 20px 20px 12px; border: 1px solid var(--border-primary); border-radius: var(--radius-default); }
.brief-timing-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 12px; }
.brief-timing-title { margin: 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-primary); }
.brief-timing-sub { margin: 0; font-family: var(--text-caption-font-family); font-size: var(--text-caption-font-size); line-height: var(--text-caption-line-height); color: var(--text-muted); }
.brief-timing-legend { display: flex; gap: 14px; flex-wrap: wrap; margin: 0; padding: 0; list-style: none; font-family: var(--text-caption-font-family); font-size: var(--text-caption-font-size); line-height: var(--text-caption-line-height); color: var(--text-muted); }
.brief-timing-legend li { display: inline-flex; align-items: center; gap: 6px; }
.brief-timing-key { width: 14px; height: 4px; border-radius: 2px; }
.brief-timing-key-education       { background: var(--theme-education-text); }
.brief-timing-key-troubleshooting { background: var(--theme-troubleshooting-text); }
.brief-timing-key-return          { background: var(--theme-return-text); }
.brief-timing-key-upsell          { background: var(--theme-upsell-text); }
.brief-timing-rows { position: relative; }
.brief-timing-band {
  position: absolute; top: 0; bottom: 0; z-index: 0;
  left: calc(var(--brief-timing-label) + var(--brief-timing-gap));
  width: calc((100% - var(--brief-timing-label) - var(--brief-timing-gap)) * 0.1773);
  background: var(--palette-neutrals-50); border-radius: 6px;
}
.brief-timing-row {
  position: relative; z-index: 1;
  display: grid; grid-template-columns: var(--brief-timing-label) minmax(0, 1fr); gap: var(--brief-timing-gap); align-items: center;
  padding: 6px 8px; margin: 0 -8px; border-radius: 8px; color: inherit; text-decoration: none;
}
.brief-timing-row:hover { background: color-mix(in srgb, var(--palette-neutrals-100) 60%, transparent); }
.brief-timing-label { display: flex; flex-direction: column; min-width: 0; }
.brief-timing-finding { 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); color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.brief-timing-meta { display: flex; gap: 10px; font-family: var(--text-caption-font-family); font-size: var(--text-caption-font-size); line-height: var(--text-caption-line-height); color: var(--text-muted); }
.brief-timing-track { width: 100%; height: 10px; overflow: visible; }
.brief-timing-base { stroke: var(--palette-neutrals-100); stroke-width: 1; }
.brief-timing-bar { stroke-width: 4; stroke-linecap: round; }
.brief-timing-bar-education       { stroke: var(--theme-education-text); }
.brief-timing-bar-troubleshooting { stroke: var(--theme-troubleshooting-text); }
.brief-timing-bar-return          { stroke: var(--theme-return-text); }
.brief-timing-bar-upsell          { stroke: var(--theme-upsell-text); }
/* Recurring keeps mattering, so it runs on lighter; an event has no day, so
   it is dashed across the whole track. */
.brief-timing-bar-open  { stroke-opacity: 0.45; }
.brief-timing-bar-event { stroke-width: 2; stroke-dasharray: 2 5; stroke-opacity: 0.7; }
.brief-timing-axis {
  display: grid; grid-template-columns: 17.73% 17.73% 17.73% 22.8% 24.04%;
  margin: 6px 0 0 calc(var(--brief-timing-label) + var(--brief-timing-gap));
  font-family: var(--text-caption-font-family); font-size: var(--text-caption-font-size); line-height: var(--text-caption-line-height); color: var(--text-muted);
}
.brief-timing-axis span { white-space: nowrap; }
.brief-timing-axis-last { display: flex; justify-content: space-between; }
/* "Show N more" opens the folded rows and "Show less" closes them, both
   animated: the wrapper's single grid row eases between 0fr and 1fr (a
   height transition with no measuring), and the rows fade with it. Closed,
   visibility drops at the END of the fold so the rows leave the tab order
   without vanishing mid-animation. reveal_controller flips .is-open and keeps
   the button on screen while the rows fold up into it. */
.brief-timing-extra {
  display: grid; grid-template-rows: 0fr; opacity: 0; visibility: hidden;
  transition: grid-template-rows 320ms cubic-bezier(0.2, 0, 0, 1), opacity 200ms ease, visibility 0s linear 320ms;
}
.brief-timing-more.is-open .brief-timing-extra {
  grid-template-rows: 1fr; opacity: 1; visibility: visible;
  transition: grid-template-rows 320ms cubic-bezier(0.2, 0, 0, 1), opacity 240ms ease 60ms, visibility 0s;
}
/* min-height: 0 lets the row shrink below its content. The side padding
   gives the rows' hover fill (which bleeds 8px past them) room inside the
   clip. */
.brief-timing-extra-inner { min-height: 0; overflow: hidden; margin: 0 -8px; padding: 0 8px; }
.brief-timing-toggle {
  display: inline-flex; align-items: center; gap: 4px; margin: 4px 0 0; padding: 0; border: 0; background: none; cursor: pointer;
  font-family: var(--text-caption-font-family); font-size: var(--text-caption-font-size); line-height: var(--text-caption-line-height); font-weight: 500; color: var(--brand-primary);
}
.brief-timing-toggle-icon { width: 14px; height: 14px; transition: transform 320ms cubic-bezier(0.2, 0, 0, 1); }
.brief-timing-more.is-open .brief-timing-toggle-icon { transform: rotate(180deg); }
.brief-timing-more:not(.is-open) .brief-timing-toggle-less,
.brief-timing-more.is-open .brief-timing-toggle-more { display: none; }
@media (prefers-reduced-motion: reduce) {
  .brief-timing-extra, .brief-timing-more.is-open .brief-timing-extra, .brief-timing-toggle-icon { transition: none; }
}
.brief-timing-note {
  display: flex; align-items: flex-start; gap: 8px; margin: 12px 0 0; padding-top: 12px; border-top: 1px solid var(--border-primary);
  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-secondary);
}
.brief-timing-note svg { width: 16px; height: 16px; margin-top: 2px; flex: 0 0 auto; color: var(--text-muted); }

/* The finding card: the journey step card's frame (.jc-step), full width and
   stacked. Who says it top left, when top right, the finding, then one line
   of evidence that opens. */
.brief-findings { display: flex; flex-direction: column; gap: 12px; }
.brief-finding {
  display: flex; flex-direction: column; gap: 6px; padding: 12px 14px;
  background: var(--bg-surface); border: 1px solid var(--border-primary); border-radius: var(--radius-small);
  box-shadow: 0 0 0 0 transparent, var(--shadow-container-elevated-bg);
  transition: border-color 140ms ease-out, box-shadow 140ms ease-out;
  scroll-margin-top: 24px;
}
.brief-finding:target { border-color: var(--border-focus); box-shadow: 0 0 0 3px var(--brand-subtle), var(--shadow-container-elevated-bg); }
.brief-finding-top { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.brief-finding-kind { font-family: var(--text-caption-font-family); font-size: var(--text-caption-font-size); line-height: var(--text-caption-line-height); color: var(--text-muted); }
.brief-finding-when { 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); color: var(--text-primary); white-space: nowrap; }
.brief-finding-text { margin: 0; font-family: var(--text-body-font-family); font-weight: 500; font-size: var(--text-body-font-size); line-height: var(--text-body-line-height); color: var(--text-primary); }
.brief-finding-evidence { margin-top: 4px; }
.brief-finding-foot {
  list-style: none; cursor: pointer;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding-top: 8px; border-top: 1px solid var(--border-primary);
  font-family: var(--text-caption-font-family); font-size: var(--text-caption-font-size); line-height: var(--text-caption-line-height); color: var(--text-muted);
}
.brief-finding-foot::-webkit-details-marker { display: none; }
.brief-finding-src { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.brief-finding-src .evidence-src-link { font-family: inherit; }
.brief-finding-chevron { width: 14px; height: 14px; flex: 0 0 auto; margin-right: auto; margin-left: -6px; color: var(--text-muted); transition: transform 140ms ease-out; }
.brief-finding-evidence[open] .brief-finding-chevron { transform: rotate(180deg); }
/* Confidence in words and neutral: how much the finding rests on, not an
   alarm. */
.brief-finding-confidence { flex: 0 0 auto; white-space: nowrap; }
.brief-finding-quotes { display: flex; flex-direction: column; gap: 6px; padding-top: 8px; }
.brief-finding-quotes .evidence-quote { margin: 0; }
.brief-finding-cite { white-space: nowrap; }

/* 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; }
/* ATT-348 — the development-only "Download as Markdown" row above the
   insights, right-aligned like the Brief tab's toolbar actions. */
.intel-dev-actions { display: flex; justify-content: flex-end; margin-bottom: 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; }
/* ATT-314 — as on the brief: caption + theme pill left, count right. */
.intel-section-hdr-left { display: inline-flex; align-items: center; gap: 8px; }
.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; }
/* ATT-410 — a popover citing Reddit quotes needs a reading measure, and
   says when one owner is the only source. */
.tooltip-box:has(.disco-quote) { max-width: 360px; width: 360px; gap: 8px; }
.tooltip-box .disco-quote { margin: 0 10px; }
.tooltip-box-note {
  padding: 0 10px;
  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);
}


/* ── 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;
}

/* ATT-273 — STATE LIVES IN THE GROUND (/styleguide/live-status, "The three
   states"). This replaces the Figma node 1:1852 ruling, where all three states
   shared one neutral surface and the mark's colour plus red failure text said
   which state it was. On a floating pill the mark is too small to be the only
   signal, and dark mode has no red text token, so the fill carries it instead:
   the .cr-banner recipe, a pale state fill with the saturated value of the same
   family as the border. In progress stays neutral, because "working" is not a
   state worth colouring. The mark and the text are held constant below. */
.ls-strip--completed {
  background: var(--status-success-default);
  border-color: var(--status-success-subtle);
}

.ls-strip--failed {
  background: var(--status-error-faint);
  border-color: var(--status-error-subtle);
}

/* Dark (the wizard's .ob-root scope). The recessed --bg-subtle ground, and the
   edge lifted to neutrals-300 because the light border measures 1.13:1 on dark
   and a shadow cannot separate dark on dark. The state tints are layered OVER
   --bg-subtle, since a bare rgba would composite over whatever is behind the
   strip rather than over its own ground. */
.ob-root[data-theme="dark"] .ls-strip {
  background: var(--bg-subtle);
  border-color: var(--palette-neutrals-300);
}
.ob-root[data-theme="dark"] .ls-strip--completed {
  background: linear-gradient(rgba(16, 185, 129, 0.18), rgba(16, 185, 129, 0.18)), var(--bg-subtle);
  border-color: rgba(16, 185, 129, 0.45);
}
.ob-root[data-theme="dark"] .ls-strip--failed {
  background: linear-gradient(rgba(221, 82, 76, 0.18), rgba(221, 82, 76, 0.18)), var(--bg-subtle);
  border-color: rgba(221, 82, 76, 0.45);
}

/* ── Floating (ATT-273) ──
   Product Show and Collection Show only. The inline strip above stays in the
   page where the progress IS the content (brand voice, product import).

   THE DOCK IS STICKY, NOT ABSOLUTE, which is a departure from the mock and the
   reason for it is the panel: .ps-content scrolls internally, so a box anchored
   absolutely to it scrolls away with the tab content. The dock is the strip's
   always-rendered mount (#product_<id>_live_status / #collection_<id>_live_status),
   moved to the END of .ps-tab-content by `order` and held 16px above the
   panel's bottom edge while the content scrolls under it. It still takes its
   own height at the end of the content, so nothing is ever covered for good,
   and appearing there moves nothing above it.

   16px FROM THE PANEL'S EDGE IS bottom: -16px, NOT 16px. Chrome measures a
   sticky offset from inside the scroll container's padding, and .ps-content
   has 32px of it: `bottom: 16px` measured 49px from the edge in a screenshot.
   margin-bottom: -16px does the same for a short page whose pill rests at the
   end of the content rather than sticking. The dock is full width and ignores
   the pointer; only the pill takes clicks. */
.ls-dock {
  order: 99;
  position: sticky;
  bottom: -16px;
  z-index: 5;
  margin-top: auto;
  margin-bottom: -16px;
  display: flex;
  justify-content: center;
  pointer-events: none;
}
/* No job: the mount stays in the DOM for Turbo Streams to target, but takes no
   flex gap. */
.ls-dock:not(:has(.ls-strip)) { display: none; }

/* The modal frames (compile-journey-modal, product_edit_modal, ...) sit in
   .ps-content AFTER the tab content, and an empty frame is still a flex item,
   so each one added a 20px gap under the page: four of them lifted a short
   page's resting pill 80px off the panel's edge (screenshotted on Collection
   Show). Hidden only while EMPTY: a modal loading into one makes it non-empty
   and it displays as before, and its backdrop is position: fixed anyway. */
.ps-content > turbo-frame:empty { display: none; }

/* The pill. ONE FIXED WIDTH (Arash, 2026-10-06): the mock's max-content with
   a 520px floor still widened for every longer step message, so the pill
   changed size between broadcasts. 600px, or the panel's width if narrower;
   a longer message truncates with .ls-current-step's ellipsis. 20px each
   side, not the strip's 14: a stadium radius eats its own corners. */
.ls-strip--floating {
  pointer-events: auto;
  margin-bottom: 0;
  width: min(600px, 100%);
  border-radius: 9999px;
  box-shadow: var(--shadow-container-elevated-bg);
  /* BACKWARDS, NOT BOTH. `both` held the last keyframe's opacity: 1 after the
     entrance ended, and a held animation value beats a normal rule, so
     .ls-strip--hiding's opacity: 0 never applied, no transitionend fired, and
     the completed strip never left (live_status#hideStrip waits for that
     event). With no delay, `backwards` draws the entrance the same. */
  animation: ls-rise 280ms cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
}
/* Taller than the inline strip's 42px (Arash: it read too thin floating):
   10px above and below the 28px mark makes a 48px pill. */
.ls-strip--floating .ls-strip-row { height: auto; min-height: 48px; padding: 10px 20px; gap: 10px; }
/* Opened, it is no longer a pill: a stadium radius on a tall box reads as a
   lozenge, so it drops to the card radius while the history shows. */
.ls-strip--floating:has(.ls-history:not([hidden])) { border-radius: var(--radius-small); }
.ls-strip--floating .ls-history { padding: 0 20px 12px 60px; }

/* The entrance. The mock's 140ms and 8px (the popover timing) went unnoticed
   in the app, where the pill usually arrives with a page load or a broadcast:
   280ms, a 16px rise and a slight scale-up on an ease-out curve. It replays
   when the strip is re-rendered (a page load mid-run, or one job handing off
   to the next), which this length tolerates. */
@keyframes ls-rise {
  from { opacity: 0; transform: translateY(16px) scale(0.96); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .ls-strip--floating { animation: none; }
}

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

/* ATT-273 — THE MARK STAYS THE BRAND IN EVERY STATE. It used to shift to
   brand secondary (green) on completion per Figma node 14:1113; the ground now
   says what happened, and a mark that also says it states the state twice (a
   purple mark on a red fill read as an oversight). Per MODE, not per hex: dark
   keeps the near-white mark from the .ob-root rule further down. */

.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. Its colour is
   the brand in every state (ATT-273); the strip's ground carries the state. */
.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);
}

/* ATT-273 — no red failure text any more: the failed ground says it, and the
   message stays at body colour in every state (the DS has no dark-mode red,
   so red text could not be stated honestly in the wizard anyway). */

.ls-current-step {
  color: var(--text-secondary);
  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; }
}

/* ── The Details control (ATT-273) ──
   Label-free, on Lucide's `timeline` (vendored as shared/_icon_timeline:
   lucide-rails 0.7.4 does not ship it). Rendered ONLY from
   shared/_live_status_details, at every site, so no strip can drift from the
   ruling. chevron-down was rejected because it means "a menu opens" in this
   app; `route` because it means journey. The 24px target is the glyph plus
   3px; the small left margin keeps a full-width message off it. */
.ls-details {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 3px;
  margin-left: 6px;
  margin-right: -3px;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
}
.ls-details:hover { background: var(--palette-neutrals-100); }
.ob-root[data-theme="dark"] .ls-details:hover { background: var(--palette-neutrals-300); }
.ls-details svg { width: 18px; height: 18px; }

/* ── Tooltip (ATT-273) ──
   The app's first, promoted from the mock's .sgl-tip. CSS only: it labels a
   static trigger and never has to flip, so it always opens above. A dark
   bubble, so it reads as the system speaking rather than as part of the
   surface it explains (#f3f4f6 on neutrals-900 is 16.1:1). Shown on hover AND
   :focus-within, so the keyboard reaches it. The bubble is aria-hidden: the
   trigger's aria-label already names it, and without that a screen reader
   announces the control twice. */
.tooltip { position: relative; display: inline-flex; }
.tooltip-bubble {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translate(-50%, 4px);
  padding: 5px 9px;
  border-radius: 8px;
  white-space: nowrap;
  background: var(--palette-neutrals-900);
  color: #f3f4f6;
  font-size: var(--text-caption-font-size);
  line-height: 16px;
  font-weight: 500;
  opacity: 0;
  pointer-events: none;
  z-index: 10;
  transition: opacity 140ms ease-out, transform 140ms ease-out;
  box-shadow: 0 4px 12px -4px rgba(0, 0, 0, 0.35);
}
/* The arrow, a rotated square so it inherits the bubble's background. */
.tooltip-bubble::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  width: 7px;
  height: 7px;
  margin: -4px 0 0 -3.5px;
  background: inherit;
  transform: rotate(45deg);
  border-radius: 1px;
}
.tooltip:hover .tooltip-bubble,
.tooltip:focus-within .tooltip-bubble,
.tooltip.is-shown .tooltip-bubble { opacity: 1; transform: translate(-50%, 0); }
/* ATT-404: a sentence rather than a label (a failed source's reason), so it
   wraps at a fixed measure instead of running one line across the row. */
.tooltip-bubble--wrap { white-space: normal; width: 280px; }
@media (prefers-reduced-motion: reduce) {
  .tooltip-bubble { transition: none; }
}

/* Inline strips that collapse their history without a controller of their own
   (brand voice, product import): the `disclosure` controller holds
   is-collapsed on this wrapper, so a Turbo Stream replacing the strip inside
   keeps the reader's choice. Inline history opens by default. */
.ls-disclosure.is-collapsed .ls-history { display: none; }

/* History timeline — full step list, rendered inside the strip and toggled
   via the Details button. Scrolls internally past 132px (ATT-273: one cap for
   the floating pill and the inline strip) 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: 132px;
  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;
}

/* ─── ATT-421: the Insights tab as the evidence layer (.ins-*) ───────────────
   Built to /styleguide/insights (option B, 2a quick filters), whose .sgin-*
   rules these are. The card itself is the brief's .brief-finding; .ins-*
   adds the summary line, Worth a look, the quick filters, and the card's
   last row (where the brief uses it, and the brand's reaction). The brief and
   thin-evidence lines are the shared .callout (ATT-420). */
.ins-summary {
  display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 8px 24px;
  margin: 0 0 28px; padding-bottom: 16px; border-bottom: 1px solid var(--border-primary);
}
.ins-summary-text { margin: 0; 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-secondary); }
.ins-summary-text strong { font-weight: 600; color: var(--text-primary); }
.ins-summary-meta { font-family: var(--text-caption-font-family); font-size: var(--text-caption-font-size); line-height: var(--text-caption-line-height); color: var(--text-muted); white-space: nowrap; }
/* The brief's line and the thin-evidence line, stacked above the summary. */
.ins-notes { display: flex; flex-direction: column; gap: 8px; margin: 0 0 20px; }
/* The same updates line on the Brief tab, under its toolbar. */
.brief-updates { margin: 0 0 20px; }
.ins-section-desc { margin: -6px 0 12px; font-family: var(--text-caption-font-family); font-size: var(--text-caption-font-size); line-height: var(--text-caption-line-height); color: var(--text-muted); }

/* The card: the brief's finding card plus one row. */
.ins-card { scroll-margin-top: 24px; }
/* Discovery's timing is free text, often a sentence, where the brief's is a
   short label: the frequency keeps its line and the timing wraps instead. */
.ins-card .brief-finding-kind { flex: 0 0 auto; white-space: nowrap; }
.ins-card .brief-finding-when { min-width: 0; white-space: normal; text-align: right; }
/* More room under that line than the brief's 6px, so a long timing does not
   crowd the insight below it. */
.ins-card .brief-finding-top { margin-bottom: 6px; }
.ins-owners { display: flex; flex-direction: column; gap: 10px; padding-top: 4px; }
.ins-single { margin: 0; font-family: var(--text-caption-font-family); font-size: var(--text-caption-font-size); line-height: var(--text-caption-line-height); color: var(--text-muted); }
.ins-card-actions {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 16px;
  padding-top: 8px; border-top: 1px solid var(--border-primary);
  font-family: var(--text-caption-font-family); font-size: var(--text-caption-font-size); line-height: var(--text-caption-line-height); color: var(--text-muted);
}
.ins-used { display: inline-flex; align-items: center; gap: 6px; min-width: 0; flex-wrap: wrap; }
.ins-used svg { width: 14px; height: 14px; flex: 0 0 auto; }
.ins-used .evidence-src-link { font-family: inherit; }
.ins-used-none { color: var(--text-muted); }
/* New since the brief: a dot and words in the brand color, where "Used in"
   sits once the brief has seen it. */
.ins-fresh { color: var(--brand-primary); font-weight: 500; }
.ins-fresh-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--brand-primary); }

/* The reaction: three quiet text buttons, the same weight as the evidence
   line, so a list of cards does not read as a list of forms. */
.ins-react { display: inline-flex; gap: 2px; margin-left: auto; }
.ins-react-form { display: inline; margin: 0; }
.ins-react-btn {
  border: 0; background: none; cursor: pointer; padding: 2px 8px; border-radius: 6px;
  font: inherit; color: var(--text-secondary);
  transition: background-color 140ms ease-out, color 140ms ease-out;
}
.ins-react-btn:hover { background: var(--palette-neutrals-50); color: var(--text-primary); }
.ins-react-btn:focus-visible { outline: 2px solid var(--border-focus); outline-offset: 1px; }
.ins-react-done { display: inline-flex; align-items: center; gap: 6px; margin-left: auto; color: var(--text-secondary); }
.ins-react-done svg { width: 14px; height: 14px; }
.ins-react-undo { border: 0; background: none; padding: 0; cursor: pointer; font: inherit; color: var(--brand-primary); }
.ins-react-undo:hover { text-decoration: underline; }

/* Marked not accurate: kept on the page, so the brand can see what it ruled
   out, but quiet. */
.ins-card-wrong { background: var(--palette-neutrals-50); box-shadow: none; }
.ins-card-wrong .brief-finding-text { color: var(--text-muted); }
.ins-card-note { margin: 0; font-family: var(--text-caption-font-family); font-size: var(--text-caption-font-size); line-height: var(--text-caption-line-height); color: var(--text-secondary); }

/* Worth a look: a short list chosen by rules, each row saying why it is there,
   answered in place. */
.ins-wal { margin: 0 0 28px; padding: 16px 20px 8px; border: 1px solid var(--border-primary); border-radius: var(--radius-default); }
.ins-wal-head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; margin-bottom: 4px; flex-wrap: wrap; }
.ins-wal-title { margin: 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); }
.ins-wal-sub { margin: 0; font-family: var(--text-caption-font-family); font-size: var(--text-caption-font-size); line-height: var(--text-caption-line-height); color: var(--text-muted); }
.ins-wal-list { margin: 0; padding: 0; list-style: none; }
.ins-wal-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "why why" "text react"; gap: 4px 16px; align-items: center; padding: 12px 0; border-top: 1px solid var(--border-primary); }
.ins-wal-row:first-child { border-top: 0; }
.ins-wal-why { grid-area: why; display: inline-flex; align-items: center; gap: 6px; font-family: var(--text-caption-font-family); font-size: var(--text-caption-font-size); line-height: var(--text-caption-line-height); color: var(--text-secondary); }
.ins-wal-why svg { width: 14px; height: 14px; color: var(--text-muted); }
.ins-wal-text { grid-area: text; margin: 0; 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); color: var(--text-primary); }
.ins-wal-text a { margin-left: 6px; font-weight: 400; font-family: var(--text-caption-font-family); font-size: var(--text-caption-font-size); color: var(--brand-primary); text-decoration: none; }
.ins-wal-text a:hover { text-decoration: underline; }
.ins-wal-row .ins-react, .ins-wal-row .ins-react-done { grid-area: react; font-family: var(--text-caption-font-family); font-size: var(--text-caption-font-size); line-height: var(--text-caption-line-height); }
.ins-wal-foot { margin: 0; padding: 8px 0 4px; border-top: 1px solid var(--border-primary); font-family: var(--text-caption-font-family); font-size: var(--text-caption-font-size); line-height: var(--text-caption-line-height); color: var(--text-muted); }

/* Quick filters (2a): toggles with counts; pressed takes the filter bar's
   chip colors. */
.ins-qf { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 20px; }
.ins-qf-btn {
  display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 10px;
  border: 1px solid var(--border-primary); border-radius: 8px; background: var(--bg-surface);
  font-family: var(--text-caption-font-family); font-size: var(--text-caption-font-size); line-height: var(--text-caption-line-height); font-weight: 500;
  color: var(--text-secondary); cursor: pointer;
  transition: border-color 140ms ease-out, background-color 140ms ease-out, color 140ms ease-out;
}
.ins-qf-btn:hover { border-color: var(--palette-neutrals-300); color: var(--text-primary); }
.ins-qf-btn:disabled { cursor: default; opacity: 0.5; }
.ins-qf-btn:disabled:hover { border-color: var(--border-primary); color: var(--text-secondary); }
.ins-qf-n { font-family: var(--mono); font-size: 12px; color: var(--text-muted); }
.ins-qf-btn.is-on { background: var(--brand-subtle); border-color: transparent; color: var(--brand-primary); }
.ins-qf-btn.is-on .ins-qf-n { color: var(--brand-primary); }
/* The index filter bar's Clear all (.fb-linkbtn): muted text, pinned right,
   shown only while a filter is on. */
.ins-qf-clear {
  margin-left: auto; align-self: center; border: 0; background: none; padding: 0; cursor: pointer;
  font-family: var(--font-sans); font-size: var(--text-caption-font-size); color: var(--text-muted);
}
.ins-qf-clear:hover { color: var(--text-primary); }
/* The filters hide with the hidden attribute; these classes set display. */
.ins-qf-empty { margin: 0 0 20px; 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); }
.ins-qf-clear[hidden], .ins-qf-empty[hidden], .ins-card[hidden], .ins-section[hidden] { display: none; }

/* ─── 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 ─── */

/* ATT-287 — the focused layout's streamable flash slot. Adds no box. */
.flash-slot { display: contents; }

.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. */
/* SPACING IS A GAP, ON ICON BUTTONS ONLY (2026-10-15). It was margins keyed on
   :first-child / :last-child, which see ELEMENTS only: a label written as plain
   text is not one, so a lone icon was both first AND last child and took both
   margins, 8px of dead space between the button's edge and the icon (28px
   instead of 20px; Arash: "it elongates the button"). A flex gap spaces the
   icon from the label on whichever side it sits, text node or <span>, and
   :has(> svg) keeps it off the label-only buttons. */
.btn-elevated:has(> svg) { gap: 8px; }
.btn-elevated > svg { width: 16px; height: 16px; flex-shrink: 0; }
/* 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; }
/* ATT-341 — a menu button: a trailing chevron sits closer to the edge than a
   label does, so the right padding, the chevron and its gap all shrink. First
   used by "Import products" (/styleguide/product-import, section 1). */
.btn-elevated.btn-elevated-menu { padding-right: 14px; gap: 6px; }
.btn-elevated.btn-elevated-menu > svg:last-child { width: 14px; height: 14px; }
.btn-elevated-sm.btn-elevated-menu { padding-right: 12px; gap: 4px; }
.btn-elevated-sm.btn-elevated-menu > svg:last-child { width: 12px; height: 12px; }
.btn-elevated-xs:has(> svg) { gap: 4px; }
.btn-elevated-xs > svg { width: 12px; height: 12px; }

/* 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-314 — THEME variants, one per goal (/styleguide/theme-colors). These are
   how a goal gets its color: a view names the modifier and never a color. Text
   on a tint always uses that theme's own text color, never brand or status.
   An item with no theme (a brief section outside the mapping, the insights
   Timing questions) keeps .pill-label-muted or no pill at all. */
.pill-label-theme-education       { background: var(--theme-education-tint);       color: var(--theme-education-text); }
.pill-label-theme-troubleshooting { background: var(--theme-troubleshooting-tint); color: var(--theme-troubleshooting-text); }
.pill-label-theme-return          { background: var(--theme-return-tint);          color: var(--theme-return-text); }
.pill-label-theme-upsell          { background: var(--theme-upsell-tint);          color: var(--theme-upsell-text); }
.pill-label-theme-review          { background: var(--theme-review-tint);          color: var(--theme-review-text); }
/* 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
   ══════════════════════════════════════════ */

/* ATT-341 — the Collections / Audiences shape (.pc-page, .aud-page): the page
   pads top and bottom only and the 32px gutter lives inside the header and the
   body, so the header's bottom border runs edge to edge. It used to pad all four
   sides, which stopped the line 32px short of each edge. */
.pi-page {
  padding: 32px 0;
  min-height: 100%;
}

.pi-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 0 32px 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's `.pi-page > .brief-empty-card { width: 100% }` is gone with the
   flex column it corrected: inside the block .pi-body the card's own
   `margin: 0 auto` and max-width center it, as on Collections. */
.pi-body { padding: 32px 32px 0; }

/* ATT-341 — "Import products": the menu, its items' second line, and the
   empty card's pair of buttons (every other empty card has one). */
.pi-import-menu { position: relative; }
/* Doubled so it outranks .ps-menu-dropdown's own min-width, which sits later
   in this file. */
.ps-menu-dropdown.pi-import-dropdown { min-width: 240px; }
.pi-menu-text { display: flex; flex-direction: column; }
.pi-menu-hint {
  white-space: nowrap;
  font-weight: 400;
  font-size: 11px;
  line-height: 16px;
  color: var(--text-muted);
}
.pi-empty-actions { display: flex; align-items: center; gap: 8px; }

.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-history-icon already does. */

/* ══════════════════════════════════════════
   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; }
/* ATT-410 — Source Details is wide, and at 880px the 90vh cap read as too tall
   for its width, so it stops at 80vh. Scoped to this modal, not to
   .ps-modal-wide: the journey preview's two 60vh scroll panes plus its header
   and footer need about 60vh + 220px, which fits under 90vh but would give it
   a second scrollbar under 80vh. */
.source-details-modal { max-height: 80vh; }
.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;
}
/* ATT-410 — the facts about a source (title, URL, type, score, why) as a
   label/value grid, so five short values no longer take five stacked rows.
   The row wrappers step aside (display: contents) and each label and value
   is placed in its column by name, never by auto-placement. The thread's
   post and replies follow in the stacked list, under a rule. */
.source-details-list--facts {
  display: grid;
  grid-template-columns: 120px minmax(0, 1fr);
  column-gap: 16px;
  row-gap: 12px;
  align-items: baseline;
}
.source-details-list--facts > div { display: contents; }
.source-details-list--facts .source-details-label { grid-column: 1; margin-bottom: 0; }
.source-details-list--facts dd { grid-column: 2; }
.source-details-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);
}
.source-details-inline { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.source-details-list--thread {
  padding-top: 16px;
  border-top: 1px solid var(--border-primary);
}

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

/* ─── The loading mark (shared/_loading_mark), the app's loading indicator ───
   The Attune logomark, whole, grayscale and breathing: ATT-312's preview
   treatment (.jc-walk-mark) made a shared component, so every wait wears the
   brand mark (Arash, 2026-10-09). Same gray and rhythm; 40px, because it sits
   in a list or a form rather than filling a pane. Still under reduced motion. */
.loading-mark {
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  padding: 32px 0;
}
.loading-mark-icon {
  width: 40px; height: 40px;
  color: var(--palette-neutrals-400);
  animation: loading-mark-breathe 1.6s ease-in-out infinite;
}
.loading-mark-label {
  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);
}
@keyframes loading-mark-breathe {
  0%, 100% { opacity: 0.35; }
  50%      { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .loading-mark-icon { animation: none; opacity: 0.7; }
}

/* ─── ATT-357: audience sources (.aud-from-*, .aud-seg-*, .aud-synced-*) ───
   Promoted from the approved /styleguide/audience-sources mock (.sga-*).
   .aud-from-* is a synced audience's rail block (B1), in place of the three
   source counts; .aud-seg-* is the segment picker (A2, A4) on step 2 and in
   the Change segment modal; .aud-synced-note replaces the toolbar actions. */
.aud-from { display: flex; flex-direction: column; gap: 4px; }
.aud-from-kind {
  font-family: var(--text-caption-font-family); font-size: var(--text-caption-font-size);
  line-height: 16px; font-weight: 500; color: var(--text-secondary);
}
.aud-from-name {
  font-family: var(--text-body-small-font-family); font-size: var(--text-body-small-font-size);
  line-height: 18px; font-weight: 500; color: var(--text-primary);
}
.aud-from-sync {
  font-family: var(--text-caption-font-family); font-size: var(--text-caption-font-size);
  line-height: 16px; color: var(--text-muted);
}
.aud-from-sync-error { color: var(--status-error-text); }

.aud-seg { display: flex; flex-direction: column; gap: 12px; }
/* The list's frame holds the toolbar and the rows, so it is the column that
   spaces them; the frame element itself would otherwise be one flex child. */
.aud-seg-list { display: flex; flex-direction: column; gap: 12px; }
.aud-seg-toolbar {
  display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
  font-family: var(--text-body-small-font-family); font-size: var(--text-body-small-font-size);
  color: var(--text-muted);
}
.aud-seg-rule { font-family: var(--font-mono); font-size: var(--text-caption-font-size); color: var(--text-secondary); }
.aud-seg-error {
  margin: 0; font-family: var(--text-body-small-font-family); font-size: var(--text-body-small-font-size);
  line-height: 19px; color: var(--status-error-text);
}
.aud-seg-empty {
  margin: 0; padding: 12px 14px;
  font-family: var(--text-body-small-font-family); font-size: var(--text-body-small-font-size);
  line-height: 19px; color: var(--text-muted);
  border: 1px dashed var(--palette-neutrals-300); border-radius: var(--radius-small);
}
/* Step 2's actions stay in reach of a long list: the page scrolls, not the
   list, so the row sticks (product import's .imp-bar, ATT-341). */
.aud-seg-bar {
  position: sticky; bottom: 0; z-index: 1;
  padding: 14px 0; border-top: 1px solid var(--border-primary); background: var(--bg-surface);
}

.aud-synced-note {
  font-family: var(--text-body-small-font-family); font-size: var(--text-body-small-font-size);
  color: var(--text-muted);
}
.aud-synced-note strong { color: var(--text-secondary); font-weight: 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; }

/* ATT-295 — "Download a sample", beside Import CSV. .text-link is `font: inherit`
   by design (it is built for inline prose, where matching the sentence around it
   is the whole point), so in a toolbar it inherited 14px body text and sat a size
   above the two .btn-elevated-sm controls next to it. Caption is the same
   12px/18px those buttons already use. */
.aud-sample-link {
  font-size:      var(--text-caption-font-size);
  line-height:    var(--text-caption-line-height);
  letter-spacing: var(--text-caption-letter-spacing);
}
.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; }
/* The dropdown's ERROR STATE, for every .ctl-trigger in a .form-field-error
   (ATT-299). The canonical rule names .form-input/.form-textarea/.form-select,
   and this control draws its own border, so it was never reddened except where
   a surface named it: ATT-259's invite row and ATT-264's wizard row each got a
   scoped copy, on the reasoning that no other form rendered this control in an
   error state. The integrations anchor select made that false, and shipped a
   red hint under a grey field. One rule now, so the next surface cannot miss it.
   AFTER [aria-expanded="true"], so an open dropdown in error keeps the red
   stroke; equal specificity, later wins. */
.form-field-error .ctl-trigger { border-color: var(--status-error-subtle); }

.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;
}
/* ATT-287 — a listbox option that exists and cannot be chosen (the test send's
   SMS row). Mirrors .seg-input:disabled + .seg-label above: dimmed, no hover,
   not-allowed. Keyed on the input, so the disabled attribute is the one switch. */
.lb-item:has(.lb-input:disabled) { opacity: 0.55; cursor: not-allowed; }
.lb-item:has(.lb-input:disabled):hover { background: transparent; }

/* ══════════════════════════════════════════
   Shared Journey List Item (.jli-*)
   ATT-120. Figma 282:9716. Restyled in ATT-313 to the approved
   /styleguide/journey-experience section 2, whose page-scoped .sgj-* classes
   are promoted here (the renames are listed at the .jli-live block). Used by:
     - app/views/journeys/index.html.erb
     - app/views/products/tabs/_journeys_content.html.erb (+ the live card)
     - app/views/product_collections/tabs/_journeys_content.html.erb
     - app/views/audiences/_journeys_panel.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).
   ATT-313: tracks, gap and padding from the mock's .sgj-jrow-index (target 180,
   channel 90, audience 130, steps 90, status 110, date 100, chevron 22). */
.jli-row {
  position: relative; /* anchors .jli-row-link::after, the whole-row hit area */
  display: grid;
  grid-template-columns: minmax(0, 1fr) 180px 90px 130px 90px 110px 100px 22px;
  align-items: center;
  column-gap: 16px;
  padding: 12px 18px;
  background: var(--bg-surface);
  border: 1px solid var(--palette-neutrals-200);
  border-radius: var(--radius-small);
  transition: border-color 0.12s;
}

/* ATT-313 — Product Show and Collection Show: the page already names the
   target, so the column is dropped (the mock's .sgj-jrow-product). */
.jli-row-no-target {
  grid-template-columns: minmax(0, 1fr) 90px 130px 90px 110px 100px 22px;
}
.jli-row:hover { border-color: var(--palette-purple-300); }
/* ATT-313 — the whole row opens the journey (the mock's clickable row, without
   ATT-163's selection). The name is the one real link; its ::after covers the
   row, so a click anywhere lands on it with no JavaScript, and the keyboard and
   middle-click behave like any link. Keyboard focus draws on the row. */
.jli-row-link { color: inherit; text-decoration: none; }
.jli-row-link::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.jli-row-link:focus-visible { outline: none; }
.jli-row:has(.jli-row-link:focus-visible) { outline: 2px solid var(--brand-primary); outline-offset: 2px; }
.jli-chev { width: 20px; height: 20px; color: var(--text-muted); }

/* 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`. ATT-313 adds the channel track, which every
   journey has and the audience page is no exception to. */
.jli-row-compact {
  grid-template-columns: minmax(0, 1fr) 90px 90px 110px 100px 22px;
}

.jli-cell-title { min-width: 0; }
/* ATT-313 — the mock's .sgj-jrow-name: body size at weight 500 (no medium
   body token exists, so the weight is the one literal). */
.jli-title {
  display: block;
  font-family:    var(--text-body-font-family);
  font-weight:    500;
  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);
  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-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;
}

/* Channel / audience / steps / status: flex so the inline-flex content (icon +
   label, or a content-hugging pill) sits at the column's left edge within its
   fixed track. */
.jli-cell-channel,
.jli-cell-audience,
.jli-cell-steps,
.jli-cell-status {
  display: flex;
  align-items: center;
  min-width: 0;
}
/* ATT-313 — the mock's .sgj-jrow-muted: a quiet cell value, with an optional
   leading icon. Replaces the audience pill and the mono "N Nodes" count in
   rows; the live card keeps pills. */
.jli-muted {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  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);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.jli-muted svg { width: 14px; height: 14px; flex-shrink: 0; }

/* Mono date, left-aligned in its track as the mock draws it (.sgj-jrow-date). */
.jli-cell-date {
  font-family: var(--mono);
  font-size: 12px;
  line-height: 16px;
  color: var(--text-muted);
}


/* ══ ATT-313 — the live journey card on Product Show → Journeys tab ══
   Promoted from the mock's page-scoped classes:
     .sgj-live       → .jli-live
     .sgj-live-main  → .jli-live-main
     .sgj-live-name  → .jli-live-name
     .sgj-live-meta  → .jli-live-meta
     .sgj-list-cap   → .jli-list-cap
     .sgj-pill-icon  → .jli-pill-icon
     .sgj-jrow       → .jli-row (restyled, above)
     .sgj-jrow-index → .jli-row (the default grid)
     .sgj-jrow-product → .jli-row-no-target
     .sgj-jrow-name  → .jli-title
     .sgj-jrow-muted → .jli-muted
     .sgj-jrow-date  → .jli-cell-date
     .sgj-rows       → .jt-list (existing)
     .sgj-chev       → .jli-chev
   Not promoted: .sgj-panel (mock scaffolding). */
.jli-live {
  display: flex;
  align-items: center;
  gap: 20px;
  padding: 18px 20px;
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-small);
  background: var(--bg-surface);
  box-shadow: var(--shadow-container-elevated-bg);
}
.jli-live-main {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.jli-live-name {
  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);
  color: var(--text-primary);
}
.jli-live-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-family: var(--text-caption-font-family);
  font-size:   var(--text-caption-font-size);
  line-height: var(--text-caption-line-height);
  color: var(--text-muted);
}
/* A pill with a leading icon (the live card's channel). */
.jli-pill-icon { display: inline-flex; align-items: center; gap: 4px; }
.jli-pill-icon svg { width: 12px; height: 12px; }

/* ATT-323 — the email fallback flag (/styleguide/journey-experience 10b, the
   mock's .sgj-fb-pill). On a row the title cell becomes a flex line so the
   pill follows the name, and the name still truncates before the pill does. */
.jli-fallback-pill { flex-shrink: 0; white-space: nowrap; }
.jli-cell-title-flagged { display: flex; align-items: center; gap: 8px; }
.jli-cell-title-flagged .jli-title { min-width: 0; }

/* ATT-323 — a live pair is one card: the fallback wraps onto its own line
   beneath the SMS journey, ruled off, with its own Open. */
.jli-live-paired { flex-wrap: wrap; row-gap: 14px; }
.jli-live-fallback {
  flex: 1 0 100%;
  display: flex;
  align-items: center;
  gap: 8px;
  padding-top: 14px;
  border-top: 1px solid var(--border-primary);
}
.jli-live-fallback-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--text-body-font-family);
  font-size:   var(--text-body-font-size);
  line-height: var(--text-body-line-height);
  font-weight: 500;
  color: var(--text-primary);
}
.jli-live-fallback-meta {
  font-family: var(--text-caption-font-family);
  font-size:   var(--text-caption-font-size);
  line-height: var(--text-caption-line-height);
  color: var(--text-muted);
  white-space: nowrap;
}
.jli-live-fallback .btn-elevated { margin-left: auto; flex-shrink: 0; }

/* Section caption between the live card and its history: an eyebrow, so
   UPPERCASE per the capitalization rule's one exception. */
.jli-list-cap {
  margin: 20px 0 8px;
  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);
  text-transform: uppercase;
  color: var(--text-muted);
}

/* ══════════════════════════════════════════
   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); }

/* ATT-13 — CHOICE CARDS (shared/_choice_cards). A radio drawn as a card, for a
   choice that changes what happens rather than naming a value: the compile
   modal's channel, where each option has to say what it produces. Two to three
   options; more belong in the listbox.

   The radio itself reuses .lb-input and .lb-radio, so the dot is the DS's one
   radio treatment and this file has no second copy of it. State is read from
   the input with :has, the way .lb-item does for its disabled option. */
.choice-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 12px;
}
.choice-card {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 14px;
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-small);
  background: var(--bg-surface);
  cursor: pointer;
  transition: border-color 0.12s, background-color 0.12s;
}
.choice-card:hover { border-color: var(--palette-purple-200); }
.choice-card:has(.lb-input:checked) { border-color: var(--brand-primary); background: var(--brand-subtle); }
.choice-card:has(.lb-input:checked) .choice-card-icon { color: var(--brand-primary); }
.choice-card:has(.lb-input:focus-visible) { outline: 2px solid var(--brand-primary); outline-offset: 2px; }
.choice-card:has(.lb-input:disabled) { opacity: 0.55; cursor: not-allowed; }
.choice-card:has(.lb-input:disabled):hover { border-color: var(--border-primary); }
.choice-card-top { display: flex; align-items: center; justify-content: space-between; }
.choice-card-icon { width: 20px; height: 20px; color: var(--text-secondary); flex-shrink: 0; }
.choice-card-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);
}
.choice-card-sub { font-size: 12px; line-height: 16px; color: var(--text-muted); }
/* ATT-323 — an option that CARRIES a control (promoted from the approved mock's
   .sgj-fb-* at /styleguide/journey-experience, section 10a). Ruled off from the
   description so it reads as a second thing the card holds rather than more
   prose about the choice. */
.choice-card-extra { display: block; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border-primary); }
.choice-card-extra .switch-wrap { display: flex; align-items: flex-start; gap: 10px; }
/* The real checkbox sits invisibly OVER the track so it takes the click itself;
   anywhere else in the card, the click goes to the card's radio (see
   shared/_choice_cards). */
.choice-card-extra-control { position: relative; display: inline-flex; flex-shrink: 0; margin-top: 2px; }
.choice-card-extra-control .switch-input { inset: 0; width: 100%; height: 100%; margin: 0; pointer-events: auto; cursor: pointer; z-index: 1; }
.choice-card-extra-text { font-size: 12px; line-height: 16px; color: var(--text-secondary); }
/* The control goes live only with its card. Nothing is preselected (ATT-288),
   so an on-by-default switch would otherwise open bright purple inside a choice
   nobody has made. The knob keeps its position because the value is real: it
   applies the moment the card is chosen. */
.choice-card:not(:has(.lb-input:checked)) .switch-input:checked + .switch { background: var(--palette-neutrals-300); }

/* ─── ATT-432: compile modal provider warnings (.cjm-*) ───
   Every warning that could apply is rendered; the picked channel shows its
   own (shared/_compile_journey_modal). The pair warning needs SMS picked AND
   the email fallback on. The gap is the mock's (.int-block). */
.cjm-warning { display: none; margin-top: 22px; }
.ps-modal-form:has(input[name="channel"][value="email"]:checked) .cjm-warning-email,
.ps-modal-form:has(input[name="channel"][value="sms"]:checked) .cjm-warning-sms,
.ps-modal-form:has(input[name="channel"][value="sms"]:checked):has(input[name="email_fallback"]:checked) .cjm-warning-pair { display: block; }

/* ATT-13 — FORM NOTE. A fact where a control would otherwise sit: the compile
   modal's audience when a brand has no audiences to choose between. It is not a
   hint (that sits under a field) and not a banner (nothing is wrong). */
.form-note {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  border-radius: var(--radius-small);
  background: var(--bg-elevated);
  font-family: var(--font-sans);
  font-size: 13px;
  line-height: 20px;
  color: var(--text-secondary);
}
.form-note svg { width: 16px; height: 16px; flex-shrink: 0; color: var(--text-muted); }

/* ATT-307 — number stepper (shared/_number_stepper). A real number input with
   − and + buttons at its right edge, replacing the browser's spin arrows, which
   every browser draws differently. Same shell as .input-prefix: the wrapper owns
   the border, fill and focus ring, and the input inside is unstyled. The
   buttons are separated by hairlines and disable at the field's min and max
   (number_stepper_controller). */
.input-stepper {
  display: flex;
  align-items: stretch;
  height: 36px;
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-small);
  background: var(--bg-elevated);
  overflow: hidden;
}
.input-stepper:focus-within {
  border-color: var(--border-focus);
  background: var(--bg-surface);
  box-shadow: 0 0 0 3px var(--brand-subtle);
}
.input-stepper input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: transparent;
  outline: none;
  padding: 0 12px;
  font-family: var(--font-sans);
  font-size: 14px;
  line-height: 20px;
  color: var(--text-primary);
  appearance: textfield;
  -moz-appearance: textfield;
}
.input-stepper input::-webkit-outer-spin-button,
.input-stepper input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.input-stepper input::placeholder { color: var(--text-muted); }
.input-stepper-btn {
  flex: 0 0 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-left: 1px solid var(--border-primary);
  background: transparent;
  color: var(--text-secondary);
  cursor: pointer;
  transition: background-color 120ms ease-out, color 120ms ease-out;
}
.input-stepper-btn svg { width: 14px; height: 14px; }
.input-stepper-btn:hover { background: var(--brand-subtle); color: var(--brand-primary); }
.input-stepper-btn:active { background: var(--palette-purple-100); }
.input-stepper-btn:disabled { color: var(--palette-neutrals-300); background: transparent; cursor: not-allowed; }
/* The whole field disabled: the DS disabled input treatment (ATT-122). */
.input-stepper:has(input:disabled) { background: var(--palette-neutrals-100); }
.input-stepper:has(input:disabled) input { color: var(--palette-neutrals-700); -webkit-text-fill-color: var(--palette-neutrals-700); cursor: not-allowed; }

/* 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;
}

/* .jc-stage hosts a flex row: the canvas (left) + the always-visible rail
   (right). ATT-126; restyled by ATT-309 to the journey-experience mock's
   .sgj-stage (padding 20/24/24/32, 20px gap). Dot-grid background carried
   from ATT-125 (Figma 295:11385). */
/* ATT-422 — the journey page's "Brief updated" line, between the header and
   the stage (.jbb-*). The callout is the shared one (ATT-420). ATT-435's
   "no provider" line (.jpm-slot) sits in the same place, and both stack. */
.jbb-slot,
.jpm-slot { flex: 0 0 auto; padding: 16px 24px 0 32px; }

.jc-stage {
  display: flex;
  align-items: stretch;
  gap: 20px;
  padding: 20px 24px 24px 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;
}
/* The canvas takes whatever the 480px rail leaves, and scrolls on its own. */
.jc-canvas-area {
  flex: 1 1 0;
  min-width: 0;
  overflow-y: auto;
  display: flex;
  justify-content: center;
  align-items: flex-start;
}
.jc-canvas-empty {
  padding: 80px 0;
  text-align: center;
  font-family: var(--text-body-font-family);
  font-size: var(--text-body-font-size);
  line-height: var(--text-body-line-height);
  color: var(--text-muted);
}

/* ══════════════════════════════════════════
   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-body each scroll
     independently), so the rail itself no longer scrolls. */
  overflow: hidden;
}


/* ══════════════════════════════════════════
   Journey page canvas + rail (.jc-*)
   ATT-309. /styleguide/journey-experience section 3 (option A, the permanent
   rail, chosen 2026-09-17). The mock's page-scoped .sgj-* classes, promoted:
     .sgj-stage          → .jc-stage (above)
     .sgj-canvas-area    → .jc-canvas-area (above)
     .sgj-canvas         → .jc-timeline
     .sgj-row            → .jc-tl-row (+ .jc-tl-gap-N for the mock's inline --sgj-gap)
     .sgj-day            → .jc-tl-day
     .sgj-line           → .jc-tl-line
     .sgj-dot            → .jc-tl-dot
     .sgj-lane           → .jc-tl-lane
     .sgj-card           → .jc-step
     .sgj-card-top       → .jc-step-top
     .sgj-type(-icon)    → .jc-step-type(-icon)
     .sgj-flag           → .jc-step-flag
     .sgj-card-title     → .jc-step-title
     .sgj-card-body      → .jc-step-body (+ .jc-step-body-lead, see the card)
     .sgj-answers/-answer → .jc-step-answers / .jc-step-answer
     .sgj-card-meta      → .jc-step-meta
     .sgj-cta            → .jc-step-cta
     .sgj-branch(-label/-step/-day) → .jc-branch(-label/-step/-day)
     .sgj-continues      → .jc-continues
     .sgj-rail           → .jc-rail (+ .jc-rail-body, the scroll container)
     .sgj-overview       → .jc-overview
     .sgj-editor-eyebrow → .jc-overview-eyebrow
     .sgj-editor-title   → .jc-overview-title
     .sgj-facts          → .jc-facts
     .sgj-overview-preview → .jc-overview-preview
     .sgj-section-divider → .jc-overview-divider
     .sgj-rail-suggestion(-top) → .jc-rail-suggestion(-top)
     .sgj-link           → .jc-rail-link
     .sgj-pill-icon      → .jli-pill-icon (ATT-313, reused)
   Not promoted: the add-step controls (.sgj-gap-insert, .sgj-insert*,
   .sgj-pop*, .sgj-add-end), which are ATT-311, and the editor (.sgj-editor*),
   which is ATT-310.
   ══════════════════════════════════════════ */

/* ── The timeline: day column | line | lane ── */
.jc-timeline { width: 100%; max-width: 640px; padding-bottom: 40px; }
.jc-tl-row {
  position: relative;
  display: grid;
  grid-template-columns: 56px 28px minmax(0, 1fr);
  padding-top: var(--jc-gap, 0px);
}
/* The gap above a step: 6px per day since the previous step, 14px minimum,
   56px cap (JourneysHelper#journey_gap_px; values from the mock's
   _journey_canvas). A class per value rather than an inline style. */
.jc-tl-gap-0  { --jc-gap: 0px; }
.jc-tl-gap-14 { --jc-gap: 14px; }
.jc-tl-gap-18 { --jc-gap: 18px; }
.jc-tl-gap-24 { --jc-gap: 24px; }
.jc-tl-gap-30 { --jc-gap: 30px; }
.jc-tl-gap-36 { --jc-gap: 36px; }
.jc-tl-gap-42 { --jc-gap: 42px; }
.jc-tl-gap-48 { --jc-gap: 48px; }
.jc-tl-gap-54 { --jc-gap: 54px; }
.jc-tl-gap-56 { --jc-gap: 56px; }
.jc-tl-day {
  padding-top: 14px;
  white-space: nowrap;
  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);
  color: var(--text-muted);
}
.jc-tl-line { position: relative; display: flex; justify-content: center; }
/* The line runs up through the gap to the previous step's row. */
.jc-tl-line::before {
  content: "";
  position: absolute;
  top: calc(-1 * var(--jc-gap, 0px));
  bottom: 0;
  left: 50%;
  width: 2px;
  margin-left: -1px;
  background: var(--palette-neutrals-200);
}
/* It starts at the first dot and runs through the last step to the "Add step"
   row, where it ends (ATT-311; the mock's .sgj-line-end). ATT-309 had no end
   row and stopped the line at the last dot. */
.jc-tl-row:first-child .jc-tl-line::before { top: 18px; }
.jc-tl-row-end { --jc-gap: 20px; }
.jc-tl-line-end::before { bottom: auto; height: calc(var(--jc-gap) + 18px); }
.jc-tl-dot {
  position: relative;
  margin-top: 16px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--bg-surface);
  border: 2px solid var(--palette-neutrals-400);
}
.jc-tl-dot { transition: background-color 140ms ease-out, border-color 140ms ease-out, transform 140ms ease-out; }
.jc-tl-dot.is-selected { border-color: var(--brand-primary); background: var(--brand-primary); transform: scale(1.2); }
.jc-tl-lane { min-width: 0; }

/* ── Adding a step (ATT-311, mock section 5; .sgj-gap-insert, .sgj-insert-btn,
   .sgj-insert-hint, .sgj-add-end and .sgj-pop promoted to .jc-*) ──
   The gap above each step is the hover target. Nothing takes space until it
   is hovered, so the timeline's rhythm is unchanged: then the + fades and
   scales in, the hint slides in and the line above the step turns brand
   purple, all in 140ms. */
.jc-gap-insert {
  position: absolute;
  top: -6px;
  left: 0;
  right: 0;
  z-index: 3;
  height: calc(var(--jc-gap, 0px) + 12px);
  min-height: 26px;
  display: grid;
  grid-template-columns: 56px 28px minmax(0, 1fr);
  align-items: center;
  cursor: pointer;
}
.jc-gap-insert .jc-insert-btn { grid-column: 2; justify-self: center; opacity: 0; transform: scale(0.6); transition: opacity 140ms ease-out, transform 140ms ease-out; }
.jc-gap-insert .jc-insert-hint { grid-column: 3; opacity: 0; transform: translateX(-6px); transition: opacity 140ms ease-out, transform 140ms ease-out; }
.jc-gap-insert:hover .jc-insert-btn, .jc-gap-insert:focus-visible .jc-insert-btn { opacity: 1; transform: scale(1); }
.jc-gap-insert:hover .jc-insert-hint, .jc-gap-insert:focus-visible .jc-insert-hint { opacity: 1; transform: translateX(0); }
.jc-gap-insert:focus-visible { outline: none; }
.jc-tl-row:has(.jc-gap-insert:hover) .jc-tl-line::before,
.jc-tl-row:has(.jc-gap-insert:focus-visible) .jc-tl-line::before {
  background: linear-gradient(to bottom, var(--brand-primary) 0, var(--brand-primary) var(--jc-gap), var(--palette-neutrals-200) var(--jc-gap));
}
.jc-insert-btn {
  position: relative;
  width: 22px;
  height: 22px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--brand-primary);
  border-radius: 50%;
  background: var(--bg-surface);
  color: var(--brand-primary);
  box-shadow: 0 0 0 3px var(--brand-subtle);
}
.jc-insert-btn svg { width: 14px; height: 14px; }
.jc-insert-hint {
  padding-left: 2px;
  font-family: var(--text-caption-font-family);
  font-size:   var(--text-caption-font-size);
  line-height: var(--text-caption-line-height);
  font-weight: 500;
  color: var(--brand-primary);
}
.jc-add-end {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 8px;
  padding: 8px 14px;
  border: 1px dashed var(--palette-neutrals-300);
  border-radius: var(--radius-small);
  background: transparent;
  cursor: pointer;
  font-family: var(--text-body-small-font-family);
  font-size:   var(--text-body-small-font-size);
  line-height: var(--text-body-small-line-height);
  font-weight: 500;
  color: var(--text-secondary);
}
.jc-add-end:hover { border-color: var(--brand-primary); color: var(--brand-primary); }
.jc-add-end svg { width: 14px; height: 14px; }

/* The chooser is a ROW OF THE TIMELINE, not a layer over it: the mock's
   .sgj-insert-pop-row, whose day and line columns are empty and whose card
   sits in the lane. journey-canvas#openChooser moves it to the point it was
   opened from and animates this row's height from 0, so the steps below glide
   down to make room; the card itself fades down into place over the same
   moment. (It floated until 2026-09-21, which covered the step beneath it.)

   THE ROW IS THE ANIMATING BOX, not the card: a height of 0 on the card would
   still show its 18px of padding and its border. overflow: hidden also keeps
   the card's margins inside the measurement. */
.jc-canvas { position: relative; width: 100%; max-width: 640px; }
.jc-pop-row {
  display: grid;
  grid-template-columns: 56px 28px minmax(0, 1fr);
  overflow: hidden;
}
.jc-pop-row[hidden] { display: none; }
/* 180ms, not the house 140ms: this moves the whole timeline below it, and a
   travel of ~400px reads as a snap at 140. Only while it is opening or
   closing, so a resize never animates. */
.jc-pop-row.is-animating { transition: height 180ms ease-out; }
.jc-pop {
  /* Top: the timeline's own minimum gap between steps (14px), so the card is
     not tucked against the step it follows. Bottom: 4px, because the row below
     brings its own gap. */
  margin: 14px 0 4px;
  max-width: 420px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 18px;
  background: var(--bg-surface);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-default);
  box-shadow: var(--shadow-elevated-sm), var(--shadow-container-elevated-bg);
}
.jc-pop-form { display: flex; flex-direction: column; gap: 14px; }
.jc-pop-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);
  color: var(--text-primary);
}
.jc-pop-day { max-width: 220px; }
@keyframes jc-pop-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }
.jc-pop.is-open { animation: jc-pop-in 140ms ease-out both; }

/* ── ATT-312: the whole-journey preview modal (.jc-walk*) ──
   /styleguide/journey-experience section 6, .sgj-walk* promoted. A list of
   every step on the left, the selected step rendered on the right, a compact
   pager beneath. It replaced the journey_simulations page, whose styles were
   all inline. */
.jc-walk { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 20px; min-height: 420px; }
.jc-walk-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-right: 12px;
  border-right: 1px solid var(--border-primary);
  max-height: 60vh;
  overflow-y: auto;
}
.jc-walk-item {
  display: flex;
  flex-direction: column;
  padding: 8px 10px;
  border-radius: var(--radius-small);
  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-secondary);
  text-decoration: none;
}
.jc-walk-item span {
  font-family: var(--text-caption-font-family);
  font-size:   var(--text-caption-font-size);
  line-height: var(--text-caption-line-height);
  color: var(--text-muted);
}
.jc-walk-item:hover { background: var(--bg-elevated); }
/* The step being previewed. Brand, not a theme or a status: it is a selection,
   the same treatment the canvas gives a selected step. */
.jc-walk-item.is-active, .jc-walk-item.is-active:hover { background: var(--brand-subtle); color: var(--brand-primary); }
.jc-walk-item.is-active span { color: var(--brand-primary); }
/* A branch sits under the check-in it answers. */
.jc-walk-item.is-indented { margin-left: 14px; }
/* The pane scrolls rather than stretching the modal. The email render is an
   iframe of its own, which scrolls its own content: step-editor#fitPreview
   sizes it in the editor, and that controller is not in this modal, so the
   height is set here instead. */
.jc-walk-pane { min-width: 0; max-height: 60vh; overflow-y: auto; }
.jc-walk-pane .jc-mail-frame { height: 380px; }
.jc-walk-empty {
  margin: 0;
  padding: 24px 0;
  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);
}
/* While a step renders: the Attune logomark, whole, in GRAYSCALE, breathing.
   Gray and not --brand-primary because a brand mark would read as a brand
   moment and this is a wait.

   shared/_attune_mark, NOT the Live Status strip's mark: that one is drawn as
   separate waves so each can be animated, and they idle at 0.1 opacity — at
   rest it showed only its star and read as a sliver rather than the logo
   (Arash, 2026-09-21). This is the mark as drawn, currentColor, one solid
   gray; the animation is on the whole of it, a slow fade in and out rather
   than anything moving. */
.jc-walk-loading { display: flex; align-items: center; justify-content: center; min-height: 240px; }
.jc-walk-mark {
  width: 62px;
  height: 62px;
  color: var(--palette-neutrals-400);
  animation: jc-mark-breathe 1.6s ease-in-out infinite;
}
@keyframes jc-mark-breathe {
  0%, 100% { opacity: 0.35; }
  50%      { opacity: 1; }
}
/* ATT-319 put "Send journey preview" at the left of this row, so the pager
   moved into its own group and the row spreads them. With no send button (there
   is always one now, but the rule should not depend on that) the pager still
   sits right, because it is the only child. */
.jc-walk-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 16px; }
.jc-walk-pager { display: flex; align-items: center; gap: 12px; margin-left: auto; }
.jc-walk-send svg { width: 14px; height: 14px; }
.jc-walk-count {
  min-width: 76px;
  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-secondary);
}

/* ── The step card ── */
.jc-step {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 12px 14px;
  background: var(--bg-surface);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-small);
  /* The leading zero-width ring is what lets the selected halo GROW rather
     than appear: box-shadow only interpolates between matching lists. */
  box-shadow: 0 0 0 0 transparent, var(--shadow-container-elevated-bg);
  cursor: pointer;
  transition: border-color 140ms ease-out, box-shadow 140ms ease-out;
}
.jc-step:hover { border-color: var(--palette-purple-200); }
.jc-step:focus-visible { outline: 2px solid var(--brand-primary); outline-offset: 2px; }
/* Selected: the brand outline (a focus-colored border plus a 3px halo). */
.jc-step.is-selected {
  border-color: var(--border-focus);
  box-shadow: 0 0 0 3px var(--brand-subtle), var(--shadow-container-elevated-bg);
}
.jc-step-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 22px; }
.jc-step-type {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--text-caption-font-family);
  font-size:   var(--text-caption-font-size);
  line-height: var(--text-caption-line-height);
  color: var(--text-muted);
}
.jc-step-type-icon { width: 14px; height: 14px; }
.jc-step-flag { display: inline-flex; align-items: center; gap: 4px; }
.jc-step-flag svg { width: 12px; height: 12px; }
.jc-step-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-primary);
}
/* ATT-291 — an email step whose subject is blank: the placeholder reads as
   an empty value, not as a subject. */
.jc-step-title-empty { color: var(--text-muted); }
/* Two clamped lines of body. */
.jc-step-body {
  margin: 0;
  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);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* ATT-311 — a step added by hand, not written yet. */
.jc-step-body-empty { color: var(--text-muted); }
/* An SMS step has no subject, so its text is the lead line, not a muted
   second line under a heading that does not exist. */
.jc-step-body-lead { color: var(--text-primary); }
.jc-step-answers { display: flex; flex-wrap: wrap; gap: 6px; padding-top: 6px; }
.jc-step-answer {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px;
  border: 1px solid var(--border-primary);
  border-radius: 9999px;
  background: var(--bg-elevated);
  font-family: var(--text-caption-font-family);
  font-size:   var(--text-caption-font-size);
  line-height: 18px;
  color: var(--text-secondary);
}
.jc-step-answer svg { width: 12px; height: 12px; color: var(--text-muted); }
.jc-step-meta { display: flex; align-items: center; gap: 12px; padding-top: 6px; }
.jc-step-cta {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--text-caption-font-family);
  font-size:   var(--text-caption-font-size);
  line-height: 18px;
  color: var(--text-secondary);
}
.jc-step-cta svg { width: 12px; height: 12px; color: var(--text-muted); }
/* ATT-379 — the closing rating's review link (/styleguide/rating-step section 1):
   the button's shape with the Rating type's star. Muted and italic on a reply,
   where Attune adds it rather than the brand writing it. */
.jc-step-review { display: inline-flex; align-items: center; gap: 4px; font-family: var(--text-caption-font-family); font-size: var(--text-caption-font-size); line-height: 18px; color: var(--text-secondary); }
.jc-step-review svg { width: 12px; height: 12px; color: var(--text-muted); }
.jc-step-review-added { color: var(--text-muted); font-style: italic; }

/* ── Branches, indented under the answer that leads to them ── */
.jc-branch { margin: 10px 0 0 18px; padding-left: 16px; border-left: 2px solid var(--palette-neutrals-200); }
/* More room between one answer's group and the next, so each reads as its
   own path (Arash, 2026-09-18). The first group keeps the mock's 10px under
   the check-in. */
.jc-branch + .jc-branch { margin-top: 20px; }
.jc-branch-label {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 2px 0 6px;
  font-family: var(--text-caption-font-family);
  font-size:   var(--text-caption-font-size);
  line-height: 18px;
  color: var(--text-muted);
}
.jc-branch-label strong { color: var(--text-secondary); font-weight: 600; }
.jc-branch-label svg { width: 14px; height: 14px; flex-shrink: 0; }
.jc-branch-step { display: flex; flex-direction: column; gap: 4px; }
.jc-branch-step + .jc-branch-step { margin-top: 10px; }
.jc-branch-day {
  font-family: var(--text-caption-bold-font-family);
  font-weight: var(--text-caption-bold-font-weight);
  font-size: 11px;
  line-height: 16px;
  color: var(--text-muted);
}
.jc-continues {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 10px 0 0 18px;
  font-family: var(--text-caption-font-family);
  font-size:   var(--text-caption-font-size);
  line-height: 18px;
  color: var(--text-muted);
}
.jc-continues strong { color: var(--text-secondary); font-weight: 600; }
.jc-continues svg { width: 14px; height: 14px; flex-shrink: 0; }

/* ── The rail: 480px, always present ──
   The mock drew 460px; Arash widened it to 480px on 2026-09-18 (ATT-310) to
   give the step editor more room. */
.jc-rail {
  width: 480px;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--bg-surface);
  border-radius: var(--radius-small);
  box-shadow: var(--shadow-container-elevated-bg);
  /* The ring is a pseudo-element, not an inset shadow on the box (ATT-318).
     An inset shadow is painted with the element's own background, so ANY child
     carrying an opaque background paints straight over it: the live-journey
     notice spans the full 480px and erased the ring along its top and both
     sides, leaving a border that stopped and restarted down the rail. A
     positioned pseudo-element paints above in-flow children instead, so the
     ring survives whatever is put inside. It still takes no width from the
     480px, which is why an inset ring was chosen in the first place. */
  position: relative;
}
/* ATT-318 — the emergency stop's confirm body (styleguide section 11b,
   .sgj-stop-* promoted). The count carries weight of its own; the second line
   is what this is NOT, and the cheaper thing to do instead. */
.jh-stop-body { margin: 0 0 12px; font-size: var(--text-body-small-font-size); line-height: var(--text-body-small-line-height); color: var(--text-primary); }
.jh-stop-muted { color: var(--text-muted); }
/* 600, not the UA default. Nothing matched `.jh-stop-body strong`, so it fell
   to `bolder` = 700, and General Sans Variable interpolates straight past
   SemiBold — heavier than any other lead statement in the app. Every
   comparable cluster sets 600 (.cr-banner-copy, .aab-copy, .bpb-copy,
   .set-confirm-text, .jc-branch-label, .jc-continues). */
.jh-stop-body strong { font-weight: 600; }

.jc-rail::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: inset 0 0 0 1px var(--border-primary);
  pointer-events: none;
}
/* ATT-309 — the rail's cross-fade between steps (journey_canvas_controller):
   out in 100ms, then in with the house 140ms fade + 4px rise. */
/* A turbo-frame is inline by default, and transform does nothing on an inline
   element, so the rise would silently not happen. ATT-310: a flex column, so
   the step editor inside can pin its footer while its body scrolls. */
.jc-rail-body > turbo-frame { display: flex; flex-direction: column; flex: 1 1 0; min-height: 0; }
.jc-rail-leaving { opacity: 0; transition: opacity 100ms ease-in; }
@keyframes jc-rail-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
.jc-rail-entering { animation: jc-rail-in 140ms ease-out both; }

/* The rail scrolls inside itself; the page does not. ATT-310: the scrolling
   moved into what fills it, the overview or the step editor, so the editor's
   footer stays put while its fields scroll. */
.jc-rail-body { flex: 1 1 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }

/* ── The overview, when no step is selected ── */
.jc-overview { flex: 1 1 0; min-height: 0; overflow-y: auto; padding: 18px 20px 20px; display: flex; flex-direction: column; gap: 18px; }
.jc-overview-eyebrow {
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--text-caption-font-family);
  font-size:   var(--text-caption-font-size);
  line-height: var(--text-caption-line-height);
  color: var(--text-muted);
}
.jc-overview-title {
  margin: 2px 0 0;
  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);
}
.jc-facts { margin: 0; display: flex; flex-direction: column; gap: 10px; }
.jc-facts > div {
  display: grid;
  grid-template-columns: 90px 1fr;
  gap: 8px;
  font-family: var(--text-body-small-font-family);
  font-size:   var(--text-body-small-font-size);
  line-height: var(--text-body-small-line-height);
}
.jc-facts dt { color: var(--text-muted); }
.jc-facts dd { margin: 0; color: var(--text-primary); display: inline-flex; align-items: center; gap: 6px; }
.jc-facts dd svg { width: 14px; height: 14px; color: var(--text-muted); flex-shrink: 0; }
.jc-overview-preview { align-self: flex-start; }
.jc-overview-preview svg { width: 14px; height: 14px; }
/* The mock's .sgj-section-divider: the DS divider as a small uppercase
   eyebrow (the one capitalization exception), not a heading. */
.jc-overview-divider {
  margin: 0 0 8px;
  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);
  text-transform: uppercase;
  color: var(--text-muted);
}

/* ─── ATT-319: the preview run (.jc-run-*) ───────────────────────────────
   Two surfaces, one prefix: the rail panel that shows a run in progress, and
   the modal that starts it. Promoted from the mock's .sgj-enr-* on
   /styleguide/journey-experience section 8. */

/* The rail panel. Bordered and inset so it reads as a thing that arrived,
   rather than as another section of the rail's own prose. */
.jc-run-panel {
  padding: 14px;
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-small);
  background: var(--bg-subtle);
}
.jc-run-summary { display: flex; flex-direction: column; gap: 2px; margin-bottom: 10px; }
.jc-run-summary-step {
  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);
  color: var(--text-primary);
}
.jc-run-summary-next,
.jc-run-who {
  font-family: var(--text-caption-font-family);
  font-size:   var(--text-caption-font-size);
  line-height: var(--text-caption-line-height);
  color: var(--text-secondary);
}
.jc-run-rows {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding-top: 10px;
  border-top: 1px solid var(--border-primary);
}
.jc-run-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
/* An address is the long part and a pill is the short one, so the address is
   what gives way. */
.jc-run-who { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.jc-run-actions { display: flex; justify-content: flex-end; margin-top: 12px; }

/* The modal that starts a run. */
.jc-run-into {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  padding-bottom: 16px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--border-primary);
}
.jc-run-into-name {
  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);
  color: var(--text-primary);
}
.jc-run-into-meta {
  font-family: var(--text-caption-font-family);
  font-size:   var(--text-caption-font-size);
  line-height: var(--text-caption-line-height);
  color: var(--text-muted);
}
.jc-run-pill-icon svg { width: 12px; height: 12px; }
/* THE FORM CARRIES THE MODAL BODY'S OWN RHYTHM. `.ps-modal-body` is a flex
   column with a 16px gap, which is what spaces the mock's blocks — but this
   form is a single child of that body, so the gap applied to the form and never
   between its fields, and the address hint sat on the Pace label. Mirroring the
   body's own declaration rather than inventing a second value: the gap reaches
   the header card, the fields, the note and the footer at once. */
.jc-run-form { display: flex; flex-direction: column; gap: 16px; }
/* ─── The swap between the preview and the send form ─────────────────────
   ONE FRAME HOLDS TWO PANELS of different sizes: the preview is 880px and tall,
   the send form is 600px and short. A Turbo frame swap REPLACES the element, so
   a `transition` on width or height can never fire — the incoming node has no
   previous value to animate from. What can be animated is its ARRIVAL, and a
   fade with a small rise reads as one panel becoming another rather than as a
   jump. 140ms is the app's popover timing.

   KEYED ON A CLASS THE RENDER OPTS INTO, NOT ON THE FRAME (Arash, 2026-09-22).
   Scoped to `#journey_preview_modal .ps-modal` it fired on EVERY render into
   that frame: once per pager click, so walking a ten-step journey faded ten
   times, and again on every failed submit. A frame swap always builds a new
   element, so CSS alone cannot tell "the panel changed" from "the same panel
   re-rendered" — only the navigation knows. The two links that DO change panel
   (Send journey preview, and Back) say so with `transition=1`; the pager and a
   re-render after validation do not, and so do not animate. */
@keyframes jc-modal-swap {
  from { opacity: 0; transform: translateY(6px) scale(0.985); }
  to   { opacity: 1; transform: none; }
}
.jc-modal-entering {
  animation: jc-modal-swap 140ms ease-out both;
  transform-origin: center top;
}
/* Motion is the whole point here, so removing it leaves the panel as it was. */
@media (prefers-reduced-motion: reduce) {
  .jc-modal-entering { animation: none; }
}
/* Icon and text on one baseline, the shape .jc-preview-note already uses. */
/* --bg-elevated (#fafafa), the mock's own token. It was --bg-subtle, which is
   #f5f3ff — a lavender, so the note read as a branded callout rather than as
   the quiet gray card the mock draws. */
.jc-run-note {
  display: flex;
  gap: 8px;
  padding: 12px 14px;
  border-radius: var(--radius-small);
  background: var(--bg-elevated);
  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-secondary);
}
.jc-run-note p { margin: 0; }
/* 16px with a 2px nudge, so the icon sits on the first line's baseline rather
   than above it. */
.jc-run-note svg { width: 16px; height: 16px; flex-shrink: 0; margin-top: 2px; color: var(--text-muted); }
.jc-rail-suggestion { padding: 10px 0; border-bottom: 1px solid var(--border-primary); }
/* Hidden until "Show N more": guard the attribute against any display rule. */
.jc-rail-suggestion[hidden], .jc-rail-link[hidden] { display: none; }
.jc-rail-suggestion p {
  margin: 6px 0 0;
  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-primary);
}
.jc-rail-suggestion-top { display: flex; align-items: center; justify-content: space-between; }
.jc-rail-link {
  display: inline-block;
  margin-top: 8px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  font-family: var(--text-body-small-font-family);
  font-size:   var(--text-body-small-font-size);
  font-weight: 500;
  line-height: var(--text-body-small-line-height);
  color: var(--brand-primary);
  text-decoration: none;
}
.jc-rail-suggestion .jc-rail-link { margin-top: 0; }
/* ATT-323 — the pair's "Fallback for" / "Paired with" value. */
.jc-facts .jc-rail-link { margin-top: 0; }

/* ══════════════════════════════════════════
   Step editor in the rail (.jc-editor*)
   ATT-310. /styleguide/journey-experience section 4. Replaces ATT-126's
   journey_nodes/_edit_panel, whose inline <style> block applied page-wide once
   loaded (its .evidence-* rules reached the Brief tab's claim cards). The mock's
   page-scoped classes, promoted:
     .sgj-editor               → .jc-editor
     .sgj-editor-head          → .jc-editor-head
     .sgj-editor-head-text     → .jc-editor-head-text
     .sgj-editor-head-actions  → .jc-editor-head-actions
     .sgj-editor-eyebrow       → .jc-editor-eyebrow
     .sgj-editor-title         → .jc-editor-title
     .sgj-editor-body          → .jc-editor-body (+ .jc-editor-panel per tab)
     .sgj-editor-foot          → .jc-editor-foot
     .sgj-two-col              → .jc-two-col
     .sgj-mail*                → .jc-mail* (+ .jc-mail-frame, the rendered email)
     .sgj-phone*, .sgj-bubble  → .jc-phone*, .jc-bubble
     .sgj-suggest*             → .jc-suggest*
   Not promoted: .sgj-editor-bare (chrome: false, for ATT-312's whole-journey
   preview, which will promote it). From the old panel, renamed so they cannot
   collide: .evidence-* → .jc-evidence-*, .review-*-box → .jc-review*,
   .section-divider → .jc-editor-divider.
   ══════════════════════════════════════════ */
.jc-editor { flex: 1 1 0; min-height: 0; display: flex; flex-direction: column; }
/* ATT-318 — the live-journey editing rule, at the top of the step editor
   (styleguide/journey-experience section 11, placement C). Muted rather than
   warning: a save reaching everyone not yet sent to is the intended behaviour,
   not a problem. flex-shrink so it never steals height from the panels below. */
.jc-editor-live { flex-shrink: 0; display: flex; align-items: flex-start; gap: 8px; padding: 10px 20px; background: var(--bg-elevated); border-bottom: 1px solid var(--border-primary); font-size: 12px; line-height: 16px; color: var(--text-secondary); }
.jc-editor-live svg { width: 14px; height: 14px; flex-shrink: 0; margin-top: 1px; color: var(--text-muted); }
.jc-editor-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 18px 20px 12px; }
.jc-editor-head-text { min-width: 0; }
.jc-editor-head-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.jc-editor-head-actions .btn-elevated svg { width: 14px; height: 14px; }
/* ATT-311 — Delete is a button_to, so its form must not add a line box. */
.jc-editor-delete-form { display: inline-flex; margin: 0; }
.jc-editor-delete svg { width: 18px; height: 18px; }
.jc-editor-eyebrow {
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--text-caption-font-family);
  font-size:   var(--text-caption-font-size);
  line-height: var(--text-caption-line-height);
  color: var(--text-muted);
}
.jc-editor-title {
  margin: 2px 0 0;
  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);
  overflow-wrap: anywhere;
}
.jc-editor-tabs { margin: 0 20px; flex-shrink: 0; }
.jc-editor-body { flex: 1 1 0; min-height: 0; overflow-y: auto; padding: 16px 20px; }
.jc-editor-panel { display: flex; flex-direction: column; gap: 16px; }
/* The Preview tab's lazy frame is a turbo-frame, inline by default, so its
   children would skip the panel's gap and touch. */
.jc-editor-panel > turbo-frame { display: flex; flex-direction: column; gap: 16px; }
/* display:flex above beats the hidden attribute's UA display:none, so a
   hidden tab would still show (and still take clicks). */
.jc-editor-panel[hidden], .jc-editor-foot[hidden] { display: none; }
/* The tab that appears eases in (the house 140ms fade + 4px rise). */
.jc-editor-panel-in { animation: jc-rail-in 140ms ease-out both; }
.jc-editor-foot {
  flex-shrink: 0;
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 12px 20px;
  border-top: 1px solid var(--border-primary);
  background: var(--bg-surface);
}
.jc-editor-form { display: flex; flex-direction: column; gap: 16px; }
.jc-two-col { display: grid; grid-template-columns: minmax(0, 1fr) 160px; gap: 12px; }

/* ATT-328 — a check-in's answers and its no-reply path in the step editor
   (/styleguide/journey-experience section 13, .sgj-ans-* promoted). */
.jc-ans { display: flex; flex-direction: column; gap: 8px; }
.jc-ans-hint { margin: -4px 0 2px; }
.jc-ans-rows { display: flex; flex-direction: column; gap: 8px; }
.jc-ans-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: center; }
.jc-ans-row .form-hint { grid-column: 1 / -1; margin: 0; }
/* A larger circle than the DS icon button's 24px default, sized to sit beside
   a form input (Arash, 2026-09-25); the icon keeps the DS 14px. Scoped here, not
   a DS size modifier: a second size belongs in Figma first. */
.jc-ans-remove { width: 32px; height: 32px; }
.jc-ans-add { align-self: flex-start; display: inline-flex; align-items: center; gap: 6px; padding: 4px 2px; border: 0; background: none; font: inherit; font-size: var(--text-body-small-font-size); font-weight: 500; color: var(--brand-primary); cursor: pointer; }
.jc-ans-add svg { width: 14px; height: 14px; }
.jc-ans-add:disabled { color: var(--text-muted); cursor: not-allowed; }
.jc-ans-new-note { display: flex; gap: 8px; margin: 2px 0 0; padding: 10px 12px; border-radius: var(--radius-small); background: var(--bg-elevated); font-size: var(--text-caption-font-size); line-height: 17px; color: var(--text-secondary); }
.jc-ans-new-note[hidden] { display: none; }
.jc-ans-new-note svg { width: 14px; height: 14px; flex-shrink: 0; margin-top: 1px; color: var(--text-muted); }
.jc-noreply { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 4px 12px; align-items: center; margin-top: 8px; padding: 12px 14px; border: 1px solid var(--border-primary); border-radius: var(--radius-small); }
.jc-noreply-text { display: flex; flex-direction: column; }
.jc-noreply-title { font-size: var(--text-body-small-font-size); font-weight: 600; line-height: 18px; color: var(--text-primary); }
.jc-noreply-sub { font-size: var(--text-caption-font-size); line-height: 17px; color: var(--text-muted); }
.jc-noreply-days { grid-column: 2; display: flex; align-items: center; gap: 8px; font-size: var(--text-body-small-font-size); color: var(--text-secondary); max-width: 220px; }
/* A stepper is block-level everywhere else, so it takes its field's width. In
   this row it is a flex item sized by its content, and WebKit gives a number
   input no content width: the input shrank to its own padding and hid the
   value, so Safari could neither see nor change it. A fixed width is the size
   Chrome was drawing it at. */
.jc-noreply-days .input-stepper { flex: none; width: 112px; }
/* The remove confirm: a native dialog in the top layer, wearing .ps-modal. */
.jc-ans-dialog { margin: auto; padding: 0; border: 0; background: transparent; max-width: 600px; width: calc(100% - 48px); overflow: visible; }
.jc-ans-dialog::backdrop { background: var(--bg-tint); }
.jc-ans-dialog .ps-modal { max-width: none; }
.jc-editor-divider {
  margin: 4px 0 -6px;
  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);
  text-transform: uppercase;
  color: var(--text-muted);
}

/* Evidence sources (ATT-130, Figma 237:6405): title + external-link icon,
   kind pill on the right. */
.jc-evidence-list { display: flex; flex-direction: column; gap: 4px; }
.jc-evidence-item { display: flex; align-items: center; justify-content: space-between; gap: 6px; min-height: 26px; }
.jc-evidence-link, .jc-evidence-text-plain {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex: 1 1 0;
  min-width: 0;
  font-family: var(--text-body-small-font-family);
  font-size:   var(--text-body-small-font-size);
  line-height: var(--text-body-small-line-height);
  font-weight: 500;
  color: var(--text-secondary);
  text-decoration: none;
}
.jc-evidence-link { transition: color 0.12s; }
.jc-evidence-link:hover, .jc-evidence-link:focus { color: var(--brand-primary); }
.jc-evidence-text, .jc-evidence-text-plain { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.jc-evidence-text { min-width: 0; }
.jc-evidence-link-icon { width: 14px; height: 14px; flex-shrink: 0; }

/* Preview: email (Klaviyo's render) */
.jc-mail { border: 1px solid var(--border-primary); border-radius: var(--radius-small); overflow: hidden; background: var(--bg-surface); }
.jc-mail-meta {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--border-primary);
  background: var(--bg-elevated);
  font-family: var(--text-body-small-font-family);
  font-size:   var(--text-body-small-font-size);
  line-height: var(--text-body-small-line-height);
}
.jc-mail-from { color: var(--text-secondary); }
.jc-mail-subject { font-weight: 600; color: var(--text-primary); }
.jc-mail-preheader { color: var(--text-muted); }
/* The email is its own document; the frame only needs room. step-editor
   sizes it to the email's height on load. */
.jc-mail-frame { display: block; width: 100%; min-height: 160px; border: 0; background: #fff; }
.jc-mail-foot {
  margin: 0;
  padding: 10px 14px;
  border-top: 1px solid var(--border-primary);
  font-family: var(--text-caption-font-family);
  font-size: 11px;
  line-height: 16px;
  color: var(--text-muted);
}
/* The raw step, when Klaviyo cannot render it. */
.jc-mail-body {
  padding: 14px;
  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-primary);
}
.jc-mail-body p { margin: 0 0 10px; }
.jc-mail-cta {
  display: inline-block;
  margin-top: 4px;
  padding: 6px 12px;
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-small);
  font-weight: 500;
  color: var(--text-secondary);
}
.jc-preview-note {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 10px 12px;
  border-radius: var(--radius-small);
  background: var(--status-info-default);
  color: var(--text-primary);
  font-family: var(--text-body-small-font-family);
  font-size:   var(--text-body-small-font-size);
  line-height: var(--text-body-small-line-height);
}
.jc-preview-note svg { width: 16px; height: 16px; margin-top: 2px; flex-shrink: 0; color: var(--status-info-subtle); }
.jc-preview-note p { margin: 0; }
.jc-preview-note a { color: var(--brand-primary); font-weight: 500; text-decoration: none; }

/* Preview: SMS */
/* Fills the preview panel. The mock drew a 300px phone, which left the text
   in a narrow column inside the 480px rail (Arash, 2026-09-18). */
.jc-phone {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 16px;
  border: 1px solid var(--border-primary);
  border-radius: 28px;
  background: var(--bg-elevated);
}
.jc-phone-from { text-align: center; font-family: var(--text-caption-bold-font-family); font-weight: var(--text-caption-bold-font-weight); font-size: var(--text-caption-bold-font-size); color: var(--text-muted); }
.jc-bubble {
  align-self: flex-start;
  max-width: 85%;
  padding: 10px 12px;
  border-radius: 16px 16px 16px 4px;
  background: var(--palette-neutrals-200);
  font-family: var(--text-body-font-family);
  font-size:   var(--text-body-font-size);
  line-height: 20px;
  color: var(--text-primary);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.jc-phone-meta { text-align: center; font-family: var(--text-caption-font-family); font-size: 11px; line-height: 16px; color: var(--text-muted); }

/* Suggestions tab: the active rewrite */
.jc-suggest { display: flex; flex-direction: column; gap: 10px; padding: 14px; border: 1px solid var(--status-info-subtle); border-radius: var(--radius-small); }
.jc-suggest-head {
  display: flex;
  align-items: center;
  gap: 8px;
  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);
  text-transform: uppercase;
  color: var(--status-info-subtle);
}
.jc-suggest-head svg { width: 16px; height: 16px; }
.jc-suggest-field { 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-primary); }
.jc-suggest-field-label { display: block; font-weight: 500; color: var(--text-secondary); }
.jc-suggest-copy { margin: 0; font-family: var(--text-body-font-family); font-size: var(--text-body-font-size); line-height: var(--text-body-line-height); color: var(--text-primary); white-space: pre-wrap; }
.jc-suggest-why { border-top: 1px solid var(--border-primary); padding-top: 10px; 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-primary); }
.jc-suggest-why p { margin: 2px 0 0; }
.jc-suggest-why-title { font-weight: 500; color: var(--text-secondary); }
.jc-suggest-actions { display: flex; justify-content: flex-end; gap: 8px; }

/* Suggestions tab: the review history, carried from the ATT-126 panel
   (Figma 331:12128 applied, 331:12143 dismissed, 333:12151 auto-fixed,
   335:12299 flag). */
.jc-review { display: flex; flex-direction: column; padding: 16px; border: 1px solid var(--border-primary); border-radius: var(--radius-small); background: var(--bg-elevated); }
.jc-review-applied { border-color: var(--status-success-subtle); }
.jc-review-dismissed { border-color: var(--text-muted); }
.jc-review-fixed { border-color: var(--status-success-subtle); }
.jc-review-warning { border-color: var(--status-warning-subtle); }
.jc-review-head { display: flex; align-items: center; gap: 10px; }
.jc-review-icon { width: 20px; height: 20px; flex-shrink: 0; }
.jc-review-check { width: 20px; height: 20px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; border-radius: 50%; background: var(--palette-neutrals-100); }
.jc-review-check svg { width: 12px; height: 12px; color: var(--status-success-subtle); }
.jc-review-label {
  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);
  text-transform: uppercase;
}
.jc-review-applied .jc-review-label, .jc-review-fixed .jc-review-label { color: var(--status-success-subtle); }
.jc-review-dismissed .jc-review-label { color: var(--palette-neutrals-400); }
.jc-review-warning .jc-review-label { color: var(--status-warning-subtle); }
.jc-review-text { padding-top: 10px; 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-primary); }
.jc-review-dismissed .jc-review-text { color: var(--palette-neutrals-500); }
.jc-review-text + .jc-review-text { border-top: 1px solid var(--border-primary); margin-top: 10px; }
.jc-review-text-label { font-weight: 500; color: var(--text-secondary); }

/* Reduced motion keeps the fades and drops the movement and the scale. */
@media (prefers-reduced-motion: reduce) {
  @keyframes jc-rail-in { from { opacity: 0; } to { opacity: 1; } }
  @keyframes jc-pop-in { from { opacity: 0; } to { opacity: 1; } }
  .jc-tl-dot.is-selected { transform: none; }
  /* The loading mark holds still rather than breathing (ATT-312). */
  .jc-walk-mark { animation: none; opacity: 0.7; }
  .jc-gap-insert .jc-insert-btn, .jc-gap-insert .jc-insert-hint { transform: none; transition: opacity 140ms linear; }
}

/* The header's icon buttons (Preview), from the mock's header. */
.jh-controls .btn-elevated svg { width: 16px; height: 16px; }

/* ══════════════════════════════════════════
   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-420: the in-page callout (.callout-*) ───────────────────────────
   A note that sits IN a page's content: not the flash at the page's top edge
   (.banner), not the banner tucked under a page header (.cr-banner). Built
   from /styleguide/insights section 8; reference at /styleguide/callout.
   Rendered only through shared/_callout. Neutral is the default and should
   be the common case: most in-page notes are information, not alarms. The
   variants take the status tokens .banner uses; the warning fill is
   .cr-banner--warning's. */
.callout {
  /* A size container, so a narrow callout can put its actions on their own
     line (the @container rule below). Safe only because a callout sits in
     block or column flow: an inline-size container in a flex ROW would size to
     nothing. */
  container-type: inline-size;
  padding: 10px 12px 10px 14px;
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-small);
  background: var(--palette-neutrals-50);
  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-secondary);
}
.callout-body { display: flex; align-items: center; gap: 10px; }
.callout-icon { width: 16px; height: 16px; flex: 0 0 auto; align-self: flex-start; margin-top: 2px; color: var(--text-muted); }
.callout-copy { flex: 1 1 auto; min-width: 0; margin: 0; }
.callout-copy strong { font-weight: 600; color: var(--text-primary); }
/* points: — the copy is a <div> holding a <p> and this list. */
.callout-copy > p { margin: 0; }
.callout-points { margin: 4px 0 0; padding-left: 18px; }
.callout-points li + li { margin-top: 2px; }
/* The actions never wrap: the copy gives way first. */
.callout-actions { display: inline-flex; align-items: center; gap: 4px; flex: 0 0 auto; white-space: nowrap; }
.callout-actions form { display: inline; margin: 0; }
.callout-link {
  border: 0;
  background: none;
  padding: 0 6px;
  cursor: pointer;
  font: inherit;
  font-weight: 500;
  color: var(--brand-primary);
  text-decoration: none;
}
.callout-link:hover { text-decoration: underline; }
.callout-dismiss {
  display: inline-flex;
  border: 0;
  background: none;
  padding: 4px;
  cursor: pointer;
  color: var(--text-muted);
  border-radius: 6px;
}
.callout-dismiss:hover { background: color-mix(in srgb, var(--palette-neutrals-200) 50%, transparent); }
.callout-dismiss { flex: 0 0 auto; align-self: flex-start; }
/* Narrow (a card, a modal, a settings column): the actions drop to their own
   line, left-aligned with the copy. 26px is the icon (16) plus the row gap
   (10); the link's own 6px side padding is taken back so its text lines up
   with the copy's. 760px keeps a callout across a full tab or page body on
   one line and moves one inside a settings card or a modal. */
@container (max-width: 760px) {
  .callout-body { flex-wrap: wrap; row-gap: 4px; }
  .callout-copy { flex: 1 1 0; }
  .callout-actions { order: 3; flex: 1 0 100%; padding-left: 20px; }
}
.callout-dismiss svg { width: 14px; height: 14px; }

.callout-info { background: color-mix(in srgb, var(--status-info-default) 45%, var(--bg-surface)); border-color: var(--status-info-default); }
.callout-info .callout-icon { color: var(--status-info-subtle); }
.callout-warning { background: #fffdf6; border-color: var(--status-warning-subtle); }
.callout-warning .callout-icon { color: var(--status-warning-subtle); }
.callout-success { background: var(--status-success-default); border-color: color-mix(in srgb, var(--status-success-subtle) 35%, transparent); }
.callout-success .callout-icon { color: var(--status-success-subtle); }
.callout-error { background: var(--status-error-faint); border-color: var(--status-error-subtle); }
.callout-error .callout-icon,
.callout-error .callout-copy strong { color: var(--status-error-text); }


/* ─── 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)); }
/* Product Show and Collection Show are their own 100vh box (.ps-page) under
   the application layout, with the body's overflow hidden, so the bar pushed
   the page 44px past the fold: the panel's bottom edge was clipped and could
   not be scrolled to. Long-standing, and invisible until ATT-273 pinned the
   Live Status pill to that edge, where it sat flush with the window (found
   2026-10-06, staff session in Chrome). The billing bar's rules below already
   handle .ps-page; this is the acting-as half they noted was missing. */
.is-acting-as .ps-page { 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. 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 offsets .ps-page too since
   2026-10-06; it did not when this was written.)

   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; }
/* A flash in the centered sign-in column (layouts/authentication). The banner
   is built full-bleed, flush under the app header; here it would butt against
   the title and run wider than the form. So: the form's width (.ob-card's 24px
   inset), a full border in the variant's own edge color, rounded, and the
   title's gap below it. */
.ob-wrap > .banner { width: auto; margin: 0 24px 24px; border: 1px solid; border-radius: var(--radius-small); }
.ob-wrap > .banner-error   { border-color: var(--status-error-subtle); }
.ob-wrap > .banner-warning { border-color: var(--status-warning-subtle); }
.ob-wrap > .banner-info    { border-color: var(--status-info-subtle); }
.ob-wrap > .banner-success { border-color: var(--status-success-subtle); }

/* ── 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;
}
/* ATT-304 — the Terms checkbox on the account step and the one-time acceptance
   page. The DS .pcf-check circle beside a body-small sentence of links; the whole
   row is the label, so the text toggles the box too. The circle drops 2px to sit
   on the first line (16px box, 20px line). */
.ob-terms {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  cursor: pointer;
}
.ob-terms .pcf-check-visual { margin-top: 2px; }
.ob-terms-text {
  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-secondary);
}
.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; }

/* ── ATT-304: the legal links, above the wordmark ──
   Caption-size text links, not nav items: they leave the app for attune.to and
   are reference, not a place to work. Inset to the back link's 10px so the three
   line up with the labels above them. Hover turns brand, like .set-back. */
.set-legal {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 0 10px;
}
.set-legal-link {
  font-family: var(--text-caption-font-family);
  font-size: var(--text-caption-font-size);
  line-height: var(--text-caption-line-height);
  color: var(--text-muted);
  text-decoration: none;
  transition: color 0.12s;
}
.set-legal-link:hover { color: var(--brand-primary); }

/* ── 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 / ATT-264 — the invite rows' role control is the DS popover select
   (.ctl-trigger). Each row used to carry its own scoped error rule, SURFACE-
   SCOPED on the reasoning that no other error form rendered this control. ATT-299
   found a third surface and replaced both with the one DS rule beside .ctl-trigger
   (".form-field-error .ctl-trigger"), which reaches these rows unchanged: the row
   IS the .form-field-error.

   The wizard's contrast check still holds for that rule: --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
   the stroke needs no dark correction. */

.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 / ATT-271: Home, at root (.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;
}

/* ATT-271 — the vertical centring above is the unregistered Member's card
   only, so it moves to a modifier. Home itself stacks its sections from the top
   with the mock's 28px between them (/styleguide/home, .sgh-page). */
.db-page-centered { justify-content: center; }
.db-home { gap: 28px; }

/* The greeting: H1, and a body line naming the brand, because a teammate reads
   the same page. */
.db-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);
  color: var(--text-primary);
}
.db-sub {
  margin: 4px 0 0;
  font-family: var(--text-body-font-family);
  font-size: var(--text-body-font-size);
  line-height: var(--text-body-line-height);
  color: var(--text-secondary);
}

/* ATT-262 — Home's sections and the setup card, promoted from the mock's
   .sgh-* (/styleguide/home sections 2 and 2b). */
.db-section { display: flex; flex-direction: column; gap: 12px; }
.db-eyebrow {
  margin: 0;
  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);
}
.db-card { border: 1px solid var(--palette-neutrals-200); border-radius: var(--radius-small); background: var(--bg-surface); }

.db-setup { padding: 20px; display: flex; flex-direction: column; gap: 16px; }
.db-setup-top { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.db-setup-title {
  margin: 0;
  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);
}
.db-progress {
  display: flex; align-items: center; gap: 10px; white-space: nowrap;
  font-family: var(--text-body-small-font-family);
  font-size: var(--text-body-small-font-size);
  color: var(--text-secondary);
}
.db-progress-track { width: 160px; height: 6px; border-radius: 3px; background: var(--palette-neutrals-100); overflow: hidden; }
.db-progress-fill { display: block; width: 0; height: 100%; border-radius: 3px; background: var(--brand-primary); }
/* Every brand has seven steps (Home::Setup), so the fill is one class per count. */
.db-progress-fill-1 { width: calc(100% * 1 / 7); }
.db-progress-fill-2 { width: calc(100% * 2 / 7); }
.db-progress-fill-3 { width: calc(100% * 3 / 7); }
.db-progress-fill-4 { width: calc(100% * 4 / 7); }
.db-progress-fill-5 { width: calc(100% * 5 / 7); }
.db-progress-fill-6 { width: calc(100% * 6 / 7); }
.db-progress-fill-7 { width: 100%; }

.db-next {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 16px; align-items: center;
  padding: 16px; border-radius: var(--radius-small);
  background: color-mix(in srgb, var(--brand-subtle) 55%, transparent);
}
.db-next-label, .db-after-label {
  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);
  text-transform: uppercase;
  color: var(--brand-primary);
}
.db-after-label { color: var(--text-muted); }
.db-next-body { display: flex; flex-direction: column; }
.db-next-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);
}
.db-next-desc, .db-next-note {
  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-secondary);
}
/* A Member's card: where the button would be, who finishes setup. */
.db-next-note { color: var(--text-muted); white-space: nowrap; }
.db-after { display: flex; align-items: center; gap: 16px; padding: 0 16px; }
.db-after-list {
  display: flex; flex-wrap: wrap; gap: 4px 0; margin: 0; padding: 0; list-style: none;
  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-secondary);
}
.db-after-list li + li::before { content: "\2192"; margin: 0 10px; color: var(--palette-neutrals-300); }
.db-done {
  display: flex; align-items: center; gap: 8px; margin: 0; padding: 12px 16px 0;
  border-top: 1px solid var(--border-primary);
  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);
}
.db-done svg { flex: 0 0 auto; width: 16px; height: 16px; color: var(--status-success-subtle); }

/* ATT-423 — Needs attention, promoted from the mock's .sgh-notice*. One list,
   most serious first; status color is right here, because each row IS a
   status. */
.db-notices { list-style: none; margin: 0; padding: 0; }
.db-notice { display: flex; align-items: center; gap: 12px; padding: 12px 16px; }
.db-notice + .db-notice { border-top: 1px solid var(--border-primary); }
.db-notice-icon { display: inline-flex; flex: 0 0 auto; width: 28px; height: 28px; align-items: center; justify-content: center; border-radius: 8px; }
.db-notice-icon svg { width: 16px; height: 16px; }
.db-notice-error .db-notice-icon   { background: var(--status-error-default); color: var(--status-error-subtle); }
.db-notice-warning .db-notice-icon { background: var(--status-warning-default); color: var(--status-warning-subtle); }
.db-notice-info .db-notice-icon    { background: var(--status-info-default); color: var(--status-info-subtle); }
.db-notice-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.db-notice-title {
  font-family: var(--text-body-font-family);
  font-weight: 500;
  font-size: var(--text-body-font-size);
  line-height: var(--text-body-line-height);
}
.db-notice-body {
  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-secondary);
}
.db-notice-form { margin: 0; flex: 0 0 auto; }
.db-link {
  display: inline-flex; align-items: center; gap: 2px; padding: 0; border: 0; background: none; cursor: pointer;
  font-family: var(--text-body-small-font-family);
  font-size: var(--text-body-small-font-size);
  line-height: var(--text-body-small-line-height);
  font-weight: 500; color: var(--text-link); text-decoration: none; white-space: nowrap;
}
.db-link svg { width: 14px; height: 14px; }
/* "Show all": the rows past five, under the same card. */
.db-notices-more { border-top: 1px solid var(--border-primary); }
.db-notices-more > summary { padding: 10px 16px; list-style: none; }
.db-notices-more > summary::-webkit-details-marker { display: none; }
.db-notices-more[open] > summary { display: none; }
.db-notices-more > .db-notices { border-top: 0; }

/* ATT-424 — the Journeys table, promoted from the mock's .sgh-row*. Every row
   is a link to its journey. Columns: name, status, three numbers, chevron. */
.db-section-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.db-eyebrow-note { margin-left: 6px; font-weight: 400; text-transform: none; letter-spacing: 0; }
.db-rows { list-style: none; margin: 0; padding: 0; }
.db-rows > li + li { border-top: 1px solid var(--border-primary); }
.db-row {
  display: grid; grid-template-columns: minmax(0, 1fr) 140px 80px 80px 70px 16px; gap: 12px; align-items: center;
  padding: 12px 16px; color: var(--text-primary); text-decoration: none;
  transition: background-color 140ms ease-out;
}
a.db-row:hover { background: var(--palette-neutrals-50); }
.db-rows > li:last-child > .db-row { border-radius: 0 0 var(--radius-small) var(--radius-small); }
.db-row-head {
  padding-top: 10px; padding-bottom: 6px; border-bottom: 1px solid var(--border-primary);
  font-family: var(--text-caption-font-family);
  font-size: var(--text-caption-font-size);
  line-height: var(--text-caption-line-height);
  color: var(--text-muted);
}
.db-row-name { display: flex; flex-direction: column; min-width: 0; }
.db-row-title {
  font-family: var(--text-body-font-family); font-weight: 500;
  font-size: var(--text-body-font-size); line-height: var(--text-body-line-height);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.db-row-meta {
  font-family: var(--text-caption-font-family);
  font-size: var(--text-caption-font-size);
  line-height: var(--text-caption-line-height);
  color: var(--text-muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.db-num {
  text-align: right; font-variant-numeric: tabular-nums;
  font-family: var(--text-body-font-family); font-size: var(--text-body-font-size);
}
.db-row-head .db-num { font-family: inherit; font-size: inherit; }
.db-muted { color: var(--text-muted); }
.db-chev { width: 16px; height: 16px; color: var(--text-muted); }

/* Recommended: links to where each is done. They never count toward setup. */
.db-recs { display: flex; gap: 10px; flex-wrap: wrap; }
.db-rec {
  display: inline-flex; align-items: center; gap: 8px; padding: 8px 12px;
  border: 1px solid var(--palette-neutrals-200); border-radius: var(--radius-small);
  background: var(--bg-surface);
  font-family: var(--text-body-small-font-family);
  font-size: var(--text-body-small-font-size);
  line-height: var(--text-body-small-line-height);
  font-weight: 500; color: var(--text-primary); text-decoration: none;
  transition: border-color 140ms ease-out, background-color 140ms ease-out;
}
.db-rec:hover { border-color: var(--palette-purple-200); background: color-mix(in srgb, var(--brand-subtle) 40%, var(--bg-surface)); }
.db-rec svg { width: 14px; height: 14px; color: var(--brand-primary); }

/* ─── ATT-177: Settings → Integrations (.int-*) ────────────────────────────
   Promoted from the ATT-284 mock's .sgi-* cluster at /styleguide/integrations.
   Renamed rather than shared, per the prefix-isolation rule: the styleguide
   page keeps its own copy so a change here cannot silently restyle the
   proposal, and a change there cannot reach production.

   THE MOCK-STAGE CLASSES ARE NOT PROMOTED — .sgi-shell-frame, .sgi-stage and
   .sgi-after exist to hold a mock inside a styleguide page and mean nothing on
   a real one. Nor is .sgi-row-planned: it draws a coming-soon provider, and
   this page ships one provider that is not coming soon. */

/* ── The group header ──
   An UPPERCASE eyebrow is the one deliberate exception to sentence case in the
   DS. The count sits on the same line, right-aligned, because it answers the
   question the eyebrow raises: "one active" says the group is exclusive before
   you reach the controls that enforce it. */
.int-group { margin: 0 0 32px; }
.int-group-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; margin: 0 0 4px;
}
.int-eyebrow {
  font-family: var(--font-sans); font-size: 12px; font-weight: 600;
  line-height: 18px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--text-muted);
}
.int-count { font-size: 12px; line-height: 18px; color: var(--text-muted); }
.int-group-sub { font-size: 13px; line-height: 20px; color: var(--text-secondary); margin: 0 0 12px; }

/* ── The provider mark ──
   A monogram tile, NOT a logo. Real provider marks are third-party trademarks
   with their own brand guidelines, they need checking in as assets, and they
   need a dark-mode answer. A monogram is honest about not having them yet and
   reserves the exact footprint one would take, so dropping logos in later is a
   swap rather than a reflow. */
.int-mark {
  flex: 0 0 36px; width: 36px; height: 36px; border-radius: var(--radius-small);
  background: var(--brand-subtle); color: var(--brand-primary);
  display: flex; align-items: center; justify-content: center;
  font-size: 15px; font-weight: 600; line-height: 1;
}
.int-mark-muted { background: var(--bg-elevated); color: var(--text-muted); }

/* ── The row ──
   Flat, with the mark and the actions as DIRECT flex children and only the text
   column allowed to grow. Nesting the fixed cells inside an intermediate
   wrapper is what overflows in Safari, which is why .pc-row and .pi-row are
   flat too. */
.int-list { display: flex; flex-direction: column; gap: 10px; }
.int-row {
  display: flex; align-items: center; gap: 14px;
  padding: 14px 16px;
  background: var(--bg-surface);
  border: 1px solid var(--border-primary); border-radius: var(--radius-default);
}
.int-row:hover { border-color: var(--palette-purple-300); }
.int-row-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.int-row-name-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.int-row-name { font-size: 14px; font-weight: 500; line-height: 20px; color: var(--text-primary); }
/* ATT-432 — the channels a provider sends, as ONE link that opens the Channels
   modal (/styleguide/channel-routing section 1, option C). The pills are the
   ordinary .pill-label; this only makes the group a target. A pill reads as
   static everywhere else in the app, so the hover does the telling: the edit
   icon appears and a dashed pill turns brand. */
.int-channels {
  display: inline-flex; align-items: center; gap: 8px;
  border-radius: var(--radius-default);
  text-decoration: none;
}
.int-channels:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--brand-subtle); }
.int-channels-edit { width: 14px; height: 14px; color: var(--text-muted); opacity: 0; transition: opacity 0.14s ease; }
.int-channels:hover .int-channels-edit,
.int-channels:focus-visible .int-channels-edit { opacity: 1; }
/* A channel the provider can send and isn't: dashed, like the app's other
   "add here" zones. */
.int-channels-add {
  gap: 4px; padding: 0 10px 0 8px; box-sizing: border-box;
  background: transparent; color: var(--text-muted);
  border: 1px dashed var(--palette-neutrals-300);
}
.int-channels-add svg { width: 12px; height: 12px; }
.int-channels:hover .int-channels-add { border-color: var(--brand-primary); color: var(--brand-primary); }
.int-row-sub { font-size: 13px; line-height: 19px; color: var(--text-muted); }
.int-row-actions { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; }

/* ── Capability chips ──
   A connected provider is not one boolean: it has a set of capabilities, each
   its own ticket, each live or not. Klaviyo alone is four tickets, so
   connected-but-not-reading-replies is an ordinary state rather than a fault.
   Muted means Attune does not do it yet. It never means the merchant switched
   something off. */
.int-caps { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 2px; }
.int-cap {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 12px; line-height: 16px; padding: 2px 8px;
  border-radius: 999px; border: 1px solid var(--border-primary);
  color: var(--text-secondary); background: var(--bg-elevated);
}
.int-cap svg { width: 12px; height: 12px; }
.int-cap-on { color: var(--brand-secondary); border-color: var(--brand-secondary); background: transparent; }
.int-cap-off { color: var(--text-muted); }

/* The routing state of a connected channel. Its own line rather than part of
   the description, because it answers a different question: the description
   says what the channel IS, this says whether it is carrying anything now.

   .int-row-state-error IS NEW, not promoted. The mock draws never-connected,
   connected and active, and has no treatment for a connection that has stopped
   working — which ATT-177 requires ("a failing connection says so"). It is the
   same line in the error colour rather than a new component, because it answers
   the same question and only the answer has changed. */
.int-row-state { font-size: 12px; line-height: 18px; color: var(--text-muted); }
.int-row-state-error { color: var(--status-error-text); }

/* ATT-337 — the journey start trigger group, promoted from
   /styleguide/shopify-connection (.sgs-start-due, .sgs-fields,
   .sgs-after-fields). The due ring is the mock's one new treatment: the row
   right after a store connects, while no trigger is chosen. The two dropdowns
   sit side by side; .set-card spaces stacked fields apart, so side by side
   that margin is taken back or they would not line up. */
.int-row-due { border-color: var(--brand-primary); box-shadow: 0 0 0 3px var(--brand-subtle); }
.int-start-fields { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; }
.set-card .int-start-fields .form-field + .form-field { margin-top: 0; }
.int-start-after { margin-top: 20px; }

/* The lead paragraph in the connect modal, and the one piece of spacing its
   body needs. A class, not an inline style. */
.int-modal-lead { margin: 0 0 16px; }
/* ATT-430 — Postscript's connect screen opens with the plan callout, which has
   no margin of its own; the lead under it takes the lead's own 16px. */
.callout + .int-modal-lead { margin-top: 16px; }

/* The scope list in the connect modal. A list, not prose: a merchant is going
   to tick four boxes in another product, and four lines they can check off is
   the shape that matches the task. Reuses .form-hint for type. */
.int-scopes { margin: 0 0 16px; padding-left: 18px; }
.int-scopes li { margin-bottom: 4px; }
.int-scopes strong { color: var(--text-secondary); font-weight: 500; }

/* ATT-439 — what the brand's Postscript message should hold, drawn as
   Postscript draws its merge tags, as chips. Promoted from the approved mock's
   .sgcr-msg (/styleguide/channel-routing section 7). */
.int-msg {
  display: flex; align-items: center; gap: 4px; margin: 0 0 16px; padding: 10px 12px;
  border: 1px solid var(--border-primary); border-radius: var(--radius-small);
  font-size: var(--text-body-small-font-size); line-height: var(--text-body-small-line-height);
  color: var(--text-secondary);
}

/* The irreversible-warning callout. NOT .cr-banner, which carries negative
   margins tuned to bleed to the edges of .ps-content and would tear out of a
   modal body. Same warning palette, laid out for a paragraph inside a card. */
.int-warning {
  display: flex; align-items: flex-start; gap: 8px;
  margin: 0 0 16px; padding: 10px 12px;
  background: #fffdf6;
  border: 1px solid var(--status-warning-subtle);
  border-radius: var(--radius-small);
  color: var(--status-warning-subtle);
}
.int-warning-icon { width: 14px; height: 14px; flex-shrink: 0; margin-top: 3px; }

/* ── ATT-288: the scope table on the Klaviyo connect screen ──
   Promoted from the mock's .sgk-scopes. A three-column question — which scope,
   how much access, what Attune does with it — where the access level is a
   column that can be scanned rather than the tail of a sentence. The merchant
   is about to tick six boxes in another product, in the same alphabetical
   order Klaviyo lists them.

   NOT .int-scopes, which is the bulleted list ATT-177 promoted for the old
   modal and still styles prose elsewhere. */
.int-scope-table { width: 100%; border-collapse: collapse; margin: 14px 0 0; }
.int-scope-table th {
  text-align: left; padding: 0 14px 8px 0;
  border-bottom: 1px solid var(--border-primary);
  font-size: 12px; font-weight: 600; line-height: 18px;
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-muted);
}
.int-scope-table td {
  padding: 12px 14px 12px 0; border-bottom: 1px solid var(--border-primary);
  vertical-align: top; font-size: 13px; line-height: 20px; color: var(--text-secondary);
}
.int-scope-table tr:last-child td { border-bottom: 0; }
.int-scope-table th:last-child, .int-scope-table td:last-child { padding-right: 0; }
.int-scope-name   { font-weight: 500; color: var(--text-primary); white-space: nowrap; }
/* ATT-299: a scope the pasted key lacks. The NAME carries the error colour (the
   same token as the field's error hint below the table), and the access pill
   switches to .pill-label-error with its text unchanged, so the column widths
   do not move. The hint names the scopes in words. */
/* Scoped to the table: `.int-scope-table td` sets the cell colour at (0,1,1),
   which a bare class loses to. The first version did, and rendered grey. */
.int-scope-table .int-scope-name-missing { color: var(--status-error-text); }
.int-scope-access { white-space: nowrap; }

/* .form-hint carries margin:0, which is right tight under a field and wrong
   when the next thing is another field's label. */
.int-scope-reminder { margin: 14px 0 24px; }

/* Air after a .form-field, whose hint has no bottom margin of its own. */
.int-block { margin-top: 22px; }

/* ── ATT-288: a setting row on the connect screen ──
   Label and description left, the DS switch right. The switch itself is the
   shipped .switch-wrap / .switch-input / .switch — a native checkbox with no JS
   — so only the row around it is new.

   PROMOTED FROM THE MOCK'S .sgk-switch-*. Those live in the styleguide page's
   own <style> block and do not exist in application.css, so the real view
   rendered its markup correctly and unstyled: title and description full width,
   switch stranded underneath. No server-side assertion can see that. */
.int-switch-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; }
.int-switch-text { min-width: 0; }
.int-switch-title {
  font-family: var(--font-sans); font-size: 13px; font-weight: 600; color: var(--text-primary);
}
.int-switch-desc {
  font-family: var(--font-sans); font-size: 13px; line-height: 20px;
  color: var(--text-muted); margin: 2px 0 0;
}

/* A section break INSIDE a card, so one card carries several sections and still
   puts its actions last. Same treatment as .set-danger's rule. */
.int-card-split { margin-top: 26px; padding-top: 22px; border-top: 1px solid var(--border-primary); }

/* ATT-432 — "No provider will send SMS": present for a channel this provider
   sends, shown only while its switch is off, so it appears the moment the
   brand turns the channel off and goes when they turn it back on. */
.int-channel-orphan { display: none; }
.int-channel:has(.switch-input:not(:checked)) .int-channel-orphan { display: block; }

/* ─── ATT-293: the consumer response page (.rp-*) ────────────────────────────
   Promoted from /styleguide/response-page, which now renders from these rules.
   STYLED BY THE BRAND, not by Attune: every color here is one of the --rp-*
   properties layouts/response sets from Merchant#brand_palette, never an app
   token, so no Attune purple can reach a brand's page. The white card and the
   type are ours (mock section 2: the brand's name set in our font). */
.rp-body { margin: 0; }
.rp-page { min-height: 100vh; min-height: 100dvh; background: var(--rp-bg); color: var(--rp-text); font-family: var(--font-sans); display: flex; flex-direction: column; align-items: center; padding: 28px 20px 24px; box-sizing: border-box; }
.rp-inner { width: 100%; max-width: 440px; display: flex; flex-direction: column; flex: 1; }
.rp-brand { display: flex; justify-content: center; align-items: center; min-height: 56px; margin: 8px 0 28px; }
.rp-logo { max-height: 44px; max-width: 220px; display: block; }
.rp-wordmark { 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-align: center; margin: 0; }
/* The card is white whatever the brand's background, so its text has its own
   readable color (BrandPalette#card_text). */
.rp-card { background: #fff; color: var(--rp-card-text, var(--rp-text)); border-radius: 20px; padding: 28px 22px 24px; box-shadow: 0 1px 2px rgba(0,0,0,0.05), 0 8px 24px rgba(0,0,0,0.05); }
.rp-eyebrow { font-size: 13px; line-height: 18px; font-weight: 500; opacity: 0.65; margin: 0 0 8px; }
.rp-question { font-family: var(--text-h2-font-family); font-weight: 600; font-size: 22px; line-height: 30px; margin: 0 0 22px; }
/* The message before its question: context, at reading size (2026-09-25). */
.rp-lead { font-size: 16px; line-height: 24px; margin: 0 0 12px; }
.rp-form { margin: 0; }
.rp-answers { display: flex; flex-direction: column; gap: 10px; margin: 0; padding: 0; border: 0; min-width: 0; }
/* 52px tall, above the 44px touch floor. INK, not fill: the primary as outline
   and text, darkened by BrandPalette only when it would not read on white. */
.rp-answer { display: flex; align-items: center; justify-content: center; min-height: 52px; padding: 12px 16px; border-radius: 14px; border: 1.5px solid var(--rp-accent, var(--rp-primary)); background: #fff; color: var(--rp-accent, var(--rp-primary)); font-family: var(--font-sans); font-size: 16px; font-weight: 600; box-sizing: border-box; cursor: pointer; }
/* The radio stays in the page for keyboards and screen readers; the label is
   what is drawn. */
.rp-answer-input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }
.rp-answer-input:focus-visible + .rp-answer { outline: 2px solid var(--rp-accent, var(--rp-primary)); outline-offset: 2px; }
/* A CHOICE, not a second button: a tint of the brand color, a thicker outline
   and a check, so "Send answer" stays the one solid button on the card.
   .rp-answer-selected is the mock's static stand-in for :checked. */
.rp-answer-input:checked + .rp-answer,
.rp-answer-selected { background: color-mix(in srgb, var(--rp-accent, var(--rp-primary)) 10%, #ffffff); border-width: 2.5px; gap: 8px; }
.rp-answer-input:checked + .rp-answer::before,
.rp-answer-selected::before { content: "\2713"; font-weight: 700; }
/* FILL: the brand's exact primary, with whichever of near-black and white
   reads on it (BrandPalette#on_primary). A button here and a link in the
   reply and expired states. */
.rp-send { display: flex; align-items: center; justify-content: center; width: 100%; min-height: 52px; margin-top: 16px; border: 0; border-radius: 14px; background: var(--rp-primary); color: var(--rp-on-primary); font-family: var(--font-sans); font-size: 16px; font-weight: 600; text-decoration: none; box-sizing: border-box; cursor: pointer; }
.rp-send:focus-visible { outline: 2px solid var(--rp-accent, var(--rp-primary)); outline-offset: 2px; }
/* ATT-379 — the review link under a reply's own button (/styleguide/rating-step
   section 3): outlined like an answer, so the reply's button stays the one
   solid action. */
.rp-secondary { display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; min-height: 52px; margin-top: 10px; border-radius: 14px; border: 1.5px solid var(--rp-accent, var(--rp-primary)); background: #fff; color: var(--rp-accent, var(--rp-primary)); font-family: var(--font-sans); font-size: 16px; font-weight: 600; text-decoration: none; box-sizing: border-box; }
.rp-secondary svg { width: 18px; height: 18px; flex-shrink: 0; }
.rp-secondary:focus-visible { outline: 2px solid var(--rp-accent, var(--rp-primary)); outline-offset: 2px; }
.rp-done { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 600; color: var(--rp-accent, var(--rp-primary)); margin: 0 0 16px; }
.rp-done svg { width: 18px; height: 18px; flex-shrink: 0; }
.rp-reply-title { font-family: var(--text-h3-font-family); font-weight: 600; font-size: 19px; line-height: 26px; margin: 0 0 8px; }
.rp-reply-body { font-size: 15px; line-height: 23px; margin: 0; opacity: 0.85; }
.rp-note { font-size: 14px; line-height: 21px; margin: 12px 0 0; opacity: 0.75; }
.rp-note-error { opacity: 1; font-weight: 500; }
.rp-foot { margin-top: auto; padding-top: 24px; text-align: center; font-size: 13px; line-height: 20px; }
/* .rp-page-scoped so no outside link color (the app's purple) ever reaches the brand's page. */
.rp-page .rp-foot a { color: var(--rp-text); font-weight: 500; text-decoration: underline; text-underline-offset: 2px; }
.rp-powered { display: block; margin-top: 10px; font-size: 11px; opacity: 0.5; }
/* ATT-328 — the strip a TEST send's page wears. Neutral, not a brand color:
   it is ours, saying nothing is recorded. */
.rp-test { display: flex; align-items: center; justify-content: center; gap: 6px; margin: -8px -4px 18px; padding: 8px 12px; border-radius: 10px; background: rgba(0, 0, 0, 0.06); font-size: 12px; font-weight: 600; line-height: 16px; }
.rp-test svg { width: 14px; height: 14px; }
.rp-visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }

/* ─── ATT-332: Settings → Brand, the Colors card (.set-colors-*) ─────────────
   /styleguide/response-page section 6 (.sgr-swatch-* promoted). The preview
   inside is the response page's own .rp-* rules, colored by the variables the
   preview partial sets on its frame. */
.set-colors-rows { display: flex; flex-direction: column; gap: 14px; }
.set-colors-row { display: grid; grid-template-columns: 180px minmax(0, 1fr); align-items: center; gap: 4px 16px; }
.set-colors-row .form-hint { grid-column: 2; margin: 0; }
.set-colors-name { font-size: var(--text-body-font-size); font-weight: 500; color: var(--text-primary); }
.set-colors-name small { display: block; font-size: var(--text-caption-font-size); font-weight: 400; color: var(--text-muted); }
.set-colors-ctl { display: flex; align-items: center; gap: 10px; }
/* The native picker, drawn as a swatch. */
.set-colors-swatch { flex: 0 0 auto; width: 36px; height: 36px; padding: 0; border: 1px solid var(--border-primary); border-radius: 10px; background: none; cursor: pointer; }
.set-colors-swatch::-webkit-color-swatch-wrapper { padding: 0; }
.set-colors-swatch::-webkit-color-swatch { border: 0; border-radius: 9px; }
.set-colors-swatch::-moz-color-swatch { border: 0; border-radius: 9px; }
.set-colors-hex { width: 140px; font-family: var(--font-mono, monospace); }
.set-colors-note { display: flex; align-items: flex-start; gap: 8px; margin: 16px 0 0; font-size: var(--text-caption-font-size); line-height: 17px; color: var(--text-secondary); }
.set-colors-note-chip { flex: 0 0 12px; height: 12px; margin-top: 2px; border-radius: 3px; background: var(--rp-accent); }
.set-colors-preview { margin-top: 16px; border: 1px solid var(--border-primary); border-radius: var(--radius-default); overflow: hidden; }
.set-colors-preview .rp-page { min-height: 0; padding: 18px 16px; }
.set-colors-preview .rp-brand { margin: 0 0 14px; min-height: 0; }
.set-colors-preview .rp-card { padding: 18px 16px; }
.set-colors-preview .rp-question { font-size: 17px; line-height: 24px; margin-bottom: 14px; }
.set-colors-actions { margin-top: 18px; }

/* ─── ATT-332: the journey preview's color panel and colors footer (.jc-colors-*)
   /styleguide/response-page section 7 (.sgr-cpanel-* / .sgr-theme-link
   promoted). The dots and the note chip read the variables the step preview
   sets on its own frame. */
.jc-mail-foot-split { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.jc-colors-link { display: inline-flex; align-items: center; gap: 6px; flex-shrink: 0; font-weight: 500; color: var(--brand-primary); text-decoration: none; white-space: nowrap; }
.jc-colors-link:hover { text-decoration: underline; }
.jc-colors-link.is-static { color: var(--text-muted); font-weight: 400; }
.jc-colors-link.is-static:hover { text-decoration: none; }
.jc-color-dots { display: inline-flex; }
.jc-color-dots i { width: 12px; height: 12px; border-radius: 50%; border: 1px solid rgba(0, 0, 0, 0.15); margin-left: -3px; }
.jc-color-dots i:first-child { margin-left: 0; background: var(--dot-1); }
.jc-color-dots i:nth-child(2) { background: var(--dot-2); }
.jc-color-dots i:nth-child(3) { background: var(--dot-3); }
.jc-colors-saved { display: inline-flex; align-items: center; gap: 6px; color: var(--status-success-subtle); }
.jc-colors-saved svg { width: 14px; height: 14px; }
.jc-colors-note { display: flex; gap: 8px; margin: 10px 0 0; font-size: var(--text-caption-font-size); line-height: 17px; color: var(--text-secondary); }
.jc-colors-note-chip { flex: 0 0 12px; height: 12px; margin-top: 2px; border-radius: 3px; background: var(--dot-ink); }
.jc-colors-panel { display: flex; flex-direction: column; gap: 14px; }
.jc-colors-title { margin: 0; font-size: var(--text-body-font-size); font-weight: 600; color: var(--text-primary); }
.jc-colors-desc { margin: 4px 0 0; font-size: var(--text-caption-font-size); line-height: 17px; color: var(--text-muted); }
.jc-colors-row { display: flex; flex-direction: column; gap: 4px; }
.jc-colors-row .form-hint { margin: 0; }
.jc-colors-label { font-size: var(--text-caption-font-size); font-weight: 500; color: var(--text-secondary); }
.jc-colors-ctl { display: flex; align-items: center; gap: 8px; }
.jc-colors-ctl .form-input { width: 100%; min-width: 0; font-family: var(--font-mono); }
.jc-colors-swatch { flex: 0 0 auto; width: 32px; height: 32px; padding: 0; border: 1px solid var(--border-primary); border-radius: 8px; background: none; cursor: pointer; }
.jc-colors-swatch::-webkit-color-swatch-wrapper { padding: 0; }
.jc-colors-swatch::-webkit-color-swatch { border: 0; border-radius: 7px; }
.jc-colors-swatch::-moz-color-swatch { border: 0; border-radius: 7px; }
.jc-colors-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; }

/* ─── ATT-341: product import from Shopify (.imp-*) ───
   The picker (/styleguide/product-import, B1). One bordered list with hairlines
   between rows, neutral whether a row is ticked or not: purple only in the DS
   .pcf-check, and a product already in Attune recedes. The page scrolls, not
   the list; the action bar is sticky, so Continue stays in reach. */
/* The search and the results share the rail's 32px rhythm; the frame is a
   block so its children lay out as before. */
.imp-picker { display: flex; flex-direction: column; gap: 32px; }
.imp-results { display: block; }
.imp-rows-wrap { display: flex; flex-direction: column; gap: 16px; }
.imp-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  font-size: 13px;
  color: var(--text-muted);
}
.imp-empty { margin: 0; font-size: 13px; color: var(--text-muted); }
.imp-list {
  border: 1px solid var(--palette-neutrals-200);
  border-radius: var(--radius-small);
  background: var(--bg-surface);
}
.imp-row {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 12px 16px;
  cursor: pointer;
}
.imp-row + .imp-row { border-top: 1px solid var(--palette-neutrals-200); }
.imp-row .pcf-check-visual { margin-top: 2px; }
.imp-row-off { cursor: default; }
.imp-row-off .imp-row-main { opacity: 0.5; }
.imp-row-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.imp-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-secondary);
}
.imp-row-sub {
  font-size: 12px;
  line-height: 17px;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.imp-more { display: flex; justify-content: center; }
.imp-more:empty { display: none; }
.imp-bar {
  position: sticky;
  bottom: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 0;
  border-top: 1px solid var(--border-primary);
  background: var(--bg-surface);
  font-size: 13px;
  color: var(--text-secondary);
}

/* ATT-341 / ATT-334 — the shared review step (/styleguide/product-import, C1):
   name, page and category as editable columns on one line, in the same single
   list as the picker. 880px, the width ATT-205 settled for a repeating row of
   several controls (.ob-card-wide). Every grid area is named, so no cell can be
   auto-placed into the wrong column (CLAUDE.md: name every area). */
.focus-content.imp-review-content { width: 880px; }
.imp-review { display: flex; flex-direction: column; gap: 16px; }
.imp-attention { margin: 0; font-size: 13px; color: var(--text-secondary); }
.imp-grid-head,
.imp-review-row {
  display: grid;
  column-gap: 12px;
  align-items: center;
  grid-template-columns: 20px minmax(0, 1fr) minmax(0, 1.5fr) 200px;
  grid-template-areas: "check name url cat" ". msg msg msg";
}
.imp-grid-head { padding: 0 16px; font-size: 12px; font-weight: 500; color: var(--text-muted); }
.imp-review-row { padding: 12px 16px; }
.imp-review-row + .imp-review-row { border-top: 1px solid var(--palette-neutrals-200); }
/* display:grid above beats the hidden attribute's own display:none. */
.imp-review-row[hidden] { display: none; }
.imp-c-check { grid-area: check; position: relative; display: flex; align-items: center; cursor: pointer; }
.imp-c-name  { grid-area: name; }
.imp-c-url   { grid-area: url; }
.imp-c-cat   { grid-area: cat; }
.imp-c-name .form-input,
.imp-c-url .form-input { width: 100%; }
.imp-c-msg {
  grid-area: msg;
  margin-top: 6px;
  font-size: 12px;
  line-height: 17px;
  color: var(--status-error-text);
}
.imp-c-msg:empty { display: none; }
/* A note, not a problem: already in Attune, or the same page twice in a file. */
.imp-c-msg.imp-c-msg-muted { color: var(--text-muted); }
/* An unticked row is left out: its fields recede rather than disable, so it can
   be ticked back in. */
.imp-review-off .imp-c-name,
.imp-review-off .imp-c-url,
.imp-review-off .imp-c-cat { opacity: 0.5; }
.imp-footer {
  position: sticky;
  bottom: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 14px 0;
  background: var(--bg-surface);
}
.imp-summary { margin: 0 auto 0 0; font-size: 13px; color: var(--text-secondary); }
.imp-summary strong { font-weight: 600; }

/* ATT-341 — after an import (/styleguide/product-import, D1 and D2): the list
   of what was added, and the import's live status strip. The strip is the
   shared .ls-strip; only its Details open/closed state is new, held on the
   wrapper (disclosure controller) so a broadcast replacing the strip keeps it. */
.imp-added {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 0;
  padding: 14px 16px;
  list-style: none;
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-small);
  font-size: 13px;
}
.imp-added li { display: flex; align-items: center; gap: 8px; }
.imp-added svg { width: 14px; height: 14px; color: var(--brand-secondary); flex-shrink: 0; }
.imp-inline-form { display: inline; margin: 0; }
/* "Email me when the analysis is done.": the DS switch (.switch-sm) beside
   one line, saved the moment it is flipped. */
.imp-notify-form { margin: 0; }
.imp-notify { display: inline-flex; align-items: center; gap: 12px; cursor: pointer; }
.imp-notify-label { font-size: 13px; font-weight: 500; line-height: 20px; color: var(--text-primary); }

/* ATT-334 — the CSV upload (/styleguide/product-import, A1). The file input
   covers the whole dashed zone, invisibly, so a click anywhere opens the picker
   and a dropped file is taken natively; the chosen file's name shows beneath. */
.imp-columns { margin: 0; font-size: 13px; line-height: 20px; color: var(--text-secondary); }
.imp-columns code { font-family: var(--font-mono); font-size: 12px; }
.imp-drop {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 28px 16px;
  border: 1px dashed var(--palette-neutrals-300);
  border-radius: var(--radius-small);
  text-align: center;
  font-size: 13px;
  color: var(--text-muted);
  cursor: pointer;
}
.imp-drop:hover,
.imp-drop:focus-within { border-color: var(--brand-primary); }
.imp-drop svg { width: 22px; height: 22px; color: var(--text-muted); }
.imp-drop strong { color: var(--text-primary); font-weight: 500; }
.imp-drop-input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
.imp-drop-file { font-weight: 500; color: var(--text-primary); }
.imp-drop-file:empty { display: none; }

/* ─── Reporting & Analytics, phase 1 (.an-*) ─────────────────────────────
   The Analytics page and the numbers on a journey's Performance view,
   promoted from /styleguide/analytics (.sgn-*, ATT-380). The page is the
   index-page shape (.jli-page) with a tab bar under its title; the
   Performance view reuses the journey canvas (.jc-*) and adds only the
   numbers on a step.

   ONE ACCENT. Bars are the brand purple or a muted gray, and a change against
   the period before carries NO green or red: whether it is good depends on
   the number (fewer refunds is good, fewer enrollments is not).

   CHARTS ARE INLINE SVG. The app has no chart library and views take no
   inline styles, so a bar's size is an SVG attribute and only its color
   lives here. */
.an-page { display: flex; flex-direction: column; gap: 28px; padding: 32px; min-height: 100%; }
.an-page > .brief-empty-card { width: 100%; }
.an-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.an-header .text-h1 { color: var(--text-primary); margin: 0; }
/* Only the empty state rules its header: with tabs, the tab bar is the rule. */
.an-header-ruled { padding-bottom: 20px; border-bottom: 1px solid var(--border-primary); }
.an-actions { display: flex; align-items: center; gap: 8px; }
.an-actions .btn-elevated svg { width: 16px; height: 16px; }
/* The date range: the DS select at a fixed width, wide enough for its
   longest label, so the header does not shift when the range changes. */
.an-range { margin: 0; width: 160px; }
/* The tab bar: a full-width track, close under the title so the two read as
   the page's head. */
.an-tabs { display: flex; margin-top: -12px; }

.an-section-head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; margin: 0 0 12px; }
.an-section-title {
  margin: 0;
  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);
  color: var(--text-primary);
}
.an-section-sub, .an-quiet {
  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);
}
.an-quiet { margin: 0; }
.an-footnote { margin: 12px 0 0; }

/* Stat tiles. Four across; the value is the H1 token, not a display size:
   the page should read as a report, not a billboard. */
.an-tiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
.an-tile {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 16px 18px;
  border: 1px solid var(--palette-neutrals-200);
  border-radius: var(--radius-small);
  background: var(--bg-surface);
}
.an-tile-label {
  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-secondary);
}
.an-tile-value {
  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);
  font-variant-numeric: tabular-nums;
}
/* The line under a tile's value is Body Small, one step up from the caption
   it was first set in: at 12px in the muted color it was hard to read (Arash,
   QA 2026-10-13). */
.an-tile-foot {
  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);
}
.an-meta {
  font-family: var(--text-caption-font-family);
  font-size:   var(--text-caption-font-size);
  line-height: var(--text-caption-line-height);
  color: var(--text-muted);
}
.an-delta { display: inline-flex; align-items: center; gap: 3px; font-weight: 500; color: var(--text-secondary); }
.an-delta svg { width: 12px; height: 12px; color: var(--text-muted); }

/* One line under the tiles when a message stands out. The .cr-banner warning
   colors, as a card: it sits in the page, not flush under a header. */
.an-attention {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  border: 1px solid var(--status-warning-subtle);
  border-radius: var(--radius-small);
  background: #fffdf6;
  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-secondary);
}
.an-attention svg { width: 16px; height: 16px; flex-shrink: 0; color: var(--status-warning-subtle); }
.an-attention strong { font-weight: 600; color: var(--text-primary); }
.an-attention-copy { flex: 1 1 auto; min-width: 0; }
.an-attention a { color: var(--brand-primary); font-weight: 500; text-decoration: none; white-space: nowrap; }
.an-attention a:hover { color: var(--brand-hover); }

/* A card, and the chart in one: bars as SVG rects, the axis as text under. */
.an-card {
  padding: 18px 20px;
  border: 1px solid var(--palette-neutrals-200);
  border-radius: var(--radius-small);
  background: var(--bg-surface);
}
.an-stack { display: flex; flex-direction: column; gap: 12px; }
.an-card-title {
  margin: 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);
  color: var(--text-primary);
}
/* A check-in's question is the step's whole text; three lines is enough to
   recognize it, and the journey's own page has the rest. */
.an-question { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; }
/* The plot: Y labels in the first column, the drawing in the second, the
   dates under the drawing. Named areas, so nothing is auto-placed. */
.an-chart-plot {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas: "y chart" ". x";
  column-gap: 12px;
  /* Room above the top grid line and under the dates, on top of the card's
     own padding: the lines sat too close to the card's border (QA 2026-10-13). */
  padding: 14px 0 8px;
}
.an-chart { grid-area: chart; display: block; width: 100%; height: 240px; overflow: visible; }
.an-chart-bar { fill: var(--palette-purple-200); }
.an-chart-bar:hover { fill: var(--brand-primary); }
/* non-scaling-stroke: the drawing is stretched sideways, and a line must stay
   1px whatever the stretch. */
.an-chart-grid { stroke: var(--palette-neutrals-200); stroke-width: 1; vector-effect: non-scaling-stroke; }
/* One label per grid line, spread over the drawing's height. The -8px is
   half a caption line (16px), which puts each label's middle ON its line
   rather than its top or bottom edge. */
.an-chart-y {
  grid-area: y;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  margin: -8px 0;
  text-align: right;
  font-family: var(--text-caption-font-family);
  font-size:   var(--text-caption-font-size);
  line-height: 16px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.an-chart-axis {
  grid-area: x;
  display: flex;
  justify-content: space-between;
  margin-top: 8px;
  font-family: var(--text-caption-font-family);
  font-size:   var(--text-caption-font-size);
  line-height: var(--text-caption-line-height);
  color: var(--text-muted);
}

/* Answers: a row per answer. Label, bar, count, rate, as fixed tracks so the
   bars of one card start and end together. */
.an-check-ins { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
.an-products  { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.an-answers { display: flex; flex-direction: column; gap: 8px; }
.an-answer {
  display: grid;
  grid-template-columns: 96px minmax(0, 1fr) 36px 36px;
  align-items: center;
  column-gap: 10px;
  font-family: var(--text-caption-font-family);
  font-size:   var(--text-caption-font-size);
  line-height: var(--text-caption-line-height);
  color: var(--text-secondary);
}
.an-answer-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.an-answer-count, .an-answer-rate { text-align: right; font-variant-numeric: tabular-nums; }
.an-answer-rate, .an-answer-none { color: var(--text-muted); }
/* A refund row's count reads "19 of 612", so its track is wider. */
.an-refund { grid-template-columns: 96px minmax(0, 1fr) 84px 44px; }
.an-bar { display: block; width: 100%; height: 6px; border-radius: 3px; overflow: hidden; }
.an-bar-track { fill: var(--palette-neutrals-100); }
.an-bar-fill  { fill: var(--brand-primary); }
.an-bar-muted .an-bar-fill { fill: var(--palette-neutrals-300); }

/* The journeys table: the index row's shape, with numbers. Fixed tracks as
   DIRECT children of the grid (the flat-row rule), and one template for the
   head and every row, so the columns line up down the list. */
.an-rows { display: flex; flex-direction: column; gap: 8px; }
.an-row, .an-row-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 76px 72px 84px 64px 76px 60px 150px 60px 22px;
  align-items: center;
  column-gap: 16px;
  padding: 12px 18px;
}
.an-row-head {
  padding-top: 0;
  padding-bottom: 0;
  font-family: var(--text-caption-font-family);
  font-size:   var(--text-caption-font-size);
  line-height: var(--text-caption-line-height);
  color: var(--text-muted);
}
.an-row {
  background: var(--bg-surface);
  border: 1px solid var(--palette-neutrals-200);
  border-radius: var(--radius-small);
  text-decoration: none;
  transition: border-color 0.12s;
}
.an-row:hover { border-color: var(--palette-purple-300); }
.an-row:focus-visible { outline: 2px solid var(--brand-primary); outline-offset: 2px; }
.an-row-name { min-width: 0; display: flex; flex-direction: column; }
.an-row-title {
  font-family: var(--text-body-font-family);
  font-weight: 500;
  font-size:   var(--text-body-font-size);
  line-height: var(--text-body-line-height);
  color: var(--text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.an-row-target {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-family: var(--text-caption-font-family);
  font-size:   var(--text-caption-font-size);
  line-height: var(--text-caption-line-height);
  color: var(--text-muted);
}
.an-row-target svg { width: 12px; height: 12px; flex-shrink: 0; }
.an-num {
  text-align: right;
  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-primary);
  font-variant-numeric: tabular-nums;
}
.an-row-head .an-num { font: inherit; color: inherit; }
.an-row-rate { display: grid; grid-template-columns: minmax(0, 1fr) 36px; align-items: center; column-gap: 10px; }
.an-row-none {
  grid-column: 1 / -1;
  font-family: var(--text-caption-font-family);
  font-size:   var(--text-caption-font-size);
  line-height: var(--text-caption-line-height);
  color: var(--text-muted);
}
.an-row-chev { width: 20px; height: 20px; color: var(--text-muted); }
/* ATT-379 — the closing rating on a product's card: the "loved it" share in
   the H1 token, then the spread (/styleguide/rating-step section 6). */
.an-rating-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 8px; }
.an-rating-value { 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); color: var(--text-primary); font-variant-numeric: tabular-nums; }
.an-card-part-title { padding-top: 12px; border-top: 1px solid var(--palette-neutrals-200); 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); color: var(--text-primary); }
/* ATT-298 — a journey with nothing tracked has no click rate to show. */
.an-row-dash { color: var(--text-muted); }

/* ─── ATT-382: a journey's performance page (.jp-*) ───
   /styleguide/journey-performance, approved 2026-10-14. It replaced ATT-381's
   Performance view and its Steps / Performance tab bar, whose .jc-view-tabs and
   .an-step-* rules went with them. Built from the .an-* pieces where they fit
   (tiles, refund rows, bars); .jp-* is only what this page adds. */
/* The focused layout clips its rail (overflow: hidden; the journey page's
   canvas and rail scroll themselves), so this page scrolls its own body below
   the header, or anything past the fold is unreachable. */
.jp-page { flex: 1 1 0; min-height: 0; overflow-y: auto; padding: 24px 32px 48px; }
.jp-body { display: flex; flex-direction: column; gap: 24px; }
.jp-h2 { 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); color: var(--text-primary); margin: 0 0 10px; }
.jp-h2-flush { margin: 0; }
.jp-card { border: 1px solid var(--palette-neutrals-200); border-radius: var(--radius-small); padding: 16px 18px; background: var(--bg-surface); }
.jp-note { margin: 8px 0 0; font-family: var(--text-caption-font-family); font-size: var(--text-caption-font-size); line-height: var(--text-caption-line-height); color: var(--text-muted); }
.jp-range { width: 170px; }

/* The state, a word in front of the sentence. Status colors: the state IS a
   status, unlike a goal's theme. */
.jp-state { display: inline-flex; align-items: center; gap: 6px; margin-bottom: 8px; padding: 2px 8px; border-radius: 999px; 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); }
.jp-state svg { width: 13px; height: 13px; flex-shrink: 0; }
.jp-state-good  { background: var(--status-success-default); color: var(--status-success-subtle); }
.jp-state-mixed { background: var(--status-warning-default); color: #a15c07; }
.jp-state-bad   { background: #fcf1f1; color: var(--status-error-text); }
.jp-state-early { background: var(--palette-neutrals-100); color: var(--text-secondary); }

/* The sentence, and the four facts beside it in H4 (14px Medium): heavier than
   their labels, lighter than the sentence, which carries the emphasis. */
.jp-head { display: grid; grid-template-columns: minmax(0, 1fr) 280px; gap: 24px; align-items: start; }
.jp-verdict { margin: 0; max-width: 760px; font-family: var(--text-h2-font-family); font-weight: 500; font-size: 22px; line-height: 32px; color: var(--text-primary); }
.jp-verdict em { font-style: normal; color: var(--brand-primary); }
.jp-verdict-sub { margin: 10px 0 0; max-width: 760px; font-family: var(--text-body-font-family); font-size: var(--text-body-font-size); line-height: 22px; color: var(--text-secondary); }
.jp-facts { margin: 0; display: flex; flex-direction: column; gap: 10px; }
.jp-facts > div { display: flex; justify-content: space-between; padding-bottom: 8px; border-bottom: 1px solid var(--palette-neutrals-100); font-family: var(--text-body-small-font-family); font-size: var(--text-body-small-font-size); color: var(--text-secondary); }
.jp-facts dd { margin: 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); color: var(--text-primary); font-variant-numeric: tabular-nums; }
.jp-trend { display: inline-flex; align-items: center; gap: 2px; margin-left: 6px; font-family: var(--text-caption-font-family); font-weight: 400; font-size: var(--text-caption-font-size); color: var(--text-muted); }
.jp-trend svg { width: 12px; height: 12px; }

/* Order outcomes: two comparisons and a count. */
.jp-outcomes { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)) 260px; gap: 12px; }
.jp-card-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
.jp-card-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); color: var(--text-primary); }
.jp-verdict-word { font-family: var(--text-body-small-font-family); font-weight: 500; font-size: var(--text-body-small-font-size); white-space: nowrap; }
.jp-good { color: var(--status-success-subtle); }
.jp-bad  { color: var(--status-error-text); }
.jp-flat { color: var(--text-muted); }
.jp-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); color: var(--text-primary); font-variant-numeric: tabular-nums; }

/* Journey progress: an unscaled SVG (no viewBox), so these are CSS pixels and
   read at the mock's sizes at any width. */
.jp-progress { display: block; width: 100%; height: 148px; margin-top: 8px; overflow: visible; }
.jp-progress-line { stroke: var(--palette-neutrals-200); stroke-width: 2; }
.jp-progress-stack { fill: color-mix(in srgb, var(--brand-primary) 22%, white); }
.jp-progress-col.is-flag .jp-progress-stack { fill: #f6dfb3; }
.jp-progress-dot { fill: var(--bg-surface); stroke: var(--brand-primary); stroke-width: 2; }
.jp-progress-col.is-flag .jp-progress-dot { stroke: var(--status-warning-subtle); }
.jp-progress-count { font-family: var(--font-sans); font-size: 11px; font-weight: 600; fill: var(--text-secondary); }
.jp-progress-day { font-family: var(--font-sans); font-size: 11px; fill: var(--text-muted); }
.jp-progress-done-count { font-family: var(--font-sans); font-size: 18px; font-weight: 600; fill: var(--text-primary); }
.jp-progress-done { font-family: var(--font-sans); font-size: 12px; fill: var(--text-secondary); }

/* Recommendations. Amber; red on a page that is not going well. */
.jp-recs { display: flex; flex-direction: column; gap: 8px; }
.jp-rec { display: grid; grid-template-columns: 20px minmax(0, 1fr) auto; gap: 12px; align-items: start; padding: 14px 16px; border-radius: var(--radius-small); background: var(--status-warning-default); }
.jp-rec.is-bad { background: #fcf1f1; }
.jp-rec-icon { width: 18px; height: 18px; margin-top: 1px; color: var(--status-warning-subtle); }
.jp-rec.is-bad .jp-rec-icon { color: var(--status-error-text); }
.jp-rec-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: 20px; color: var(--text-primary); }
.jp-rec-body { font-family: var(--text-body-small-font-family); font-size: var(--text-body-small-font-size); line-height: 20px; color: var(--text-secondary); }
.jp-rec-since { margin-top: 2px; font-family: var(--text-caption-font-family); font-size: var(--text-caption-font-size); color: var(--text-muted); }
/* The same card in the step editor (journey_nodes/_performance_note): no
   button column, since it already is the editor. */
.jp-rec.jp-rec-bare { grid-template-columns: 20px minmax(0, 1fr); }
.jc-editor-recs { display: flex; flex-direction: column; gap: 8px; padding: 0 20px 12px; }
.jc-editor-recs > .jc-rail-link { margin-top: 0; align-self: flex-start; }
.jp-stop { display: flex; align-items: center; gap: 12px; margin-top: 12px; }

.jp-tiles3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }

/* Steps breakdown, folded. */
.jp-steps > summary { list-style: none; cursor: pointer; }
.jp-steps > summary::-webkit-details-marker { display: none; }
.jp-disclose { display: flex; align-items: center; gap: 6px; font-family: var(--text-body-font-family); font-weight: 500; font-size: var(--text-body-font-size); color: var(--text-primary); }
.jp-disclose-icon { width: 16px; height: 16px; color: var(--text-muted); transition: transform 140ms ease; }
.jp-steps[open] .jp-disclose-icon { transform: rotate(90deg); }
.jp-table { width: 100%; margin-top: 12px; border-collapse: collapse; font-family: var(--text-body-small-font-family); font-size: var(--text-body-small-font-size); line-height: 20px; }
.jp-table th { padding: 0 10px 8px; text-align: left; font-weight: 400; font-size: var(--text-caption-font-size); color: var(--text-muted); }
.jp-table td { padding: 10px; border-top: 1px solid var(--palette-neutrals-100); vertical-align: top; color: var(--text-primary); }
.jp-table .jp-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.jp-day { color: var(--text-muted); white-space: nowrap; }
.jp-step-cell { max-width: 460px; }
.jp-step-type { display: inline-flex; align-items: center; gap: 4px; font-size: var(--text-caption-font-size); color: var(--text-muted); }
.jp-step-type svg { width: 12px; height: 12px; }
.jp-step-text { display: block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.jp-table tr.is-flag td { background: #fffaf0; }
.jp-flag-num { color: #a15c07; font-weight: 600; }
.jp-edit { color: var(--brand-primary); font-weight: 500; text-decoration: none; }

/* The Performance section of the journey page's rail overview: rows in the
   rail's own .jc-facts, and a placeholder of the same rhythm (four rows, the
   facts' 10px gap) while its lazy frame loads. */
.jc-facts .jp-state { margin-bottom: 0; }
.jp-rail-summary-loading { display: flex; flex-direction: column; gap: 10px; }
.jp-rail-summary-loading span { display: block; height: 20px; border-radius: 999px; background: var(--palette-neutrals-100); }
.jp-rail-summary-loading span:nth-child(odd)  { width: 180px; }
.jp-rail-summary-loading span:nth-child(even) { width: 140px; }

/* ── Motion on render (Arash, 2026-10-14: "so it doesn't feel static") ──
   Bars grow from their base, the progress columns rise from the line one after
   another, and the figures fade up. Once, short, and off for anyone who asks
   for less motion. The Analytics page's bars share the .an-bar-fill rule. */
@keyframes an-grow-x { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes an-grow-y { from { transform: scaleY(0); } to { transform: scaleY(1); } }
@keyframes an-fade-up { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
.an-bar-fill { transform-box: fill-box; transform-origin: left center; animation: an-grow-x 700ms cubic-bezier(0.2, 0.7, 0.2, 1) both; }
.an-answer:nth-child(2) .an-bar-fill { animation-delay: 80ms; }
.an-answer:nth-child(3) .an-bar-fill { animation-delay: 160ms; }
.an-answer:nth-child(n + 4) .an-bar-fill { animation-delay: 240ms; }
.jp-progress-stack { transform-box: fill-box; transform-origin: center bottom; animation: an-grow-y 600ms cubic-bezier(0.2, 0.7, 0.2, 1) both; }
.jp-progress-count { animation: an-fade-up 300ms ease-out both; animation-delay: 400ms; }
.jp-progress-col:nth-of-type(2) :is(.jp-progress-stack) { animation-delay: 60ms; }
.jp-progress-col:nth-of-type(3) :is(.jp-progress-stack) { animation-delay: 120ms; }
.jp-progress-col:nth-of-type(4) :is(.jp-progress-stack) { animation-delay: 180ms; }
.jp-progress-col:nth-of-type(5) :is(.jp-progress-stack) { animation-delay: 240ms; }
.jp-progress-col:nth-of-type(6) :is(.jp-progress-stack) { animation-delay: 300ms; }
.jp-progress-col:nth-of-type(7) :is(.jp-progress-stack) { animation-delay: 360ms; }
.jp-progress-col:nth-of-type(n + 8) :is(.jp-progress-stack) { animation-delay: 420ms; }
.an-tile-value, .jp-count, .jp-facts dd { animation: an-fade-up 400ms ease-out both; }
@media (prefers-reduced-motion: reduce) {
  .an-bar-fill, .jp-progress-stack, .jp-progress-count, .an-tile-value, .jp-count, .jp-facts dd { animation: none; }
}
