/* ============================================================
 * components.css — reusable building blocks
 *
 * Naming convention:
 *   .btn, .input, .select        — atomic form/control elements
 *   .badge, .tag, .pill, .chip   — status/identity markers
 *   .card, .panel, .surface      — container surfaces
 *   .table, .list                — data display
 *   .drawer, .modal, .toast      — overlays
 *   .res-item, .stat-pill, ...   — domain-specific compounds
 *
 * Modifiers use --suffix:
 *   .btn-primary, .btn-secondary, .btn-ghost, .btn-danger
 *   .badge-success, .badge-warning, .badge-info, .badge-neutral
 *
 * State classes use is-:  .is-active, .is-open, .is-disabled, .is-loading
 * ============================================================ */

/* ====================================================
 * BUTTONS
 * ==================================================== */
.btn {
  height: 30px;
  padding: 0 12px;
  border-radius: var(--radius);
  font-size: var(--fs-base);
  font-weight: 500;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid transparent;
  white-space: nowrap;
  transition: background var(--dur-1), border-color var(--dur-1), color var(--dur-1);
  cursor: pointer;
}
.btn:disabled, .btn.is-disabled {
  opacity: .5;
  cursor: not-allowed;
  pointer-events: none;
}

.btn-primary   { background: var(--primary); border-color: var(--primary); color: var(--text-on-primary); }
.btn-primary:hover { background: var(--primary-600); border-color: var(--primary-600); }
.btn-primary:active { background: var(--primary-700); border-color: var(--primary-700); }

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

.btn-ghost    { background: transparent; border-color: transparent; color: var(--text-2); }
.btn-ghost:hover { background: var(--bg-alt); color: var(--text); }

.btn-danger   { color: var(--danger); }
.btn-danger:hover { background: var(--danger-bg); border-color: var(--danger-border); color: var(--danger); }

.btn-md { height: 32px; padding: 0 12px; }
.btn-lg { height: 36px; padding: 0 16px; font-size: var(--fs-lg); }
.btn-sm { height: 26px; padding: 0 10px; font-size: var(--fs-md); }
.btn-icon-only { padding: 0; width: 30px; justify-content: center; }

/* icon-button (square, transparent affordance) */
.icon-btn {
  width: 32px; height: 32px;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius);
  color: var(--text-2);
  cursor: pointer;
  position: relative;
  transition: background var(--dur-1), border-color var(--dur-1), color var(--dur-1);
}
.icon-btn:hover { background: var(--bg-alt); border-color: var(--border); color: var(--text); }

.bell-count {
  position: absolute; top: 4px; right: 4px;
  min-width: 14px; height: 14px;
  padding: 0 3px;
  border-radius: var(--radius-lg);
  background: var(--primary);
  color: #fff;
  font-size: 9.5px; font-weight: 600;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1.5px solid var(--bg);
  line-height: 1;
}

/* button group — joined controls */
.btn-group { display: inline-flex; }
.btn-group .btn { border-radius: 0; margin-left: -1px; }
.btn-group .btn:first-child { border-radius: var(--radius) 0 0 var(--radius); margin-left: 0; }
.btn-group .btn:last-child { border-radius: 0 var(--radius) var(--radius) 0; }

/* ====================================================
 * FORM CONTROLS
 * ==================================================== */
.input, .select, .textarea {
  height: 30px;
  padding: 0 10px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  font-size: var(--fs-base);
  outline: 0;
  transition: border-color var(--dur-1), box-shadow var(--dur-1);
}
.input:hover, .select:hover, .textarea:hover { border-color: var(--border-strong); }
.input:focus, .select:focus, .textarea:focus {
  border-color: var(--primary);
  box-shadow: var(--ring-focus-soft);
}
.input::placeholder, .textarea::placeholder { color: var(--text-4); }
/* Multi-line / monospace modifier for textareas that hold paste-able
 * lists or technical content (CORS origins, env-style configs, code).
 * Use on .input / .textarea. Pairs with the `text` setting type
 * rendered by pages/settings.html. */
.input--mono, .textarea--mono {
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 1.5;
  resize: vertical;
  height: auto;
  padding: 8px 10px;
}
.input.is-invalid, .select.is-invalid, .textarea.is-invalid {
  border-color: var(--danger);
  box-shadow: var(--ring-focus-danger);
}

/* Editable input affordance — subtle background tint signals "this slot
   is live; click to type." Disabled / readonly inputs fall back to the
   clean --bg surface (or --bg-alt for selects, defined below) so the
   Rule 11 lock state stays unambiguous.

   Pure CSS: when page logic flips el.disabled = true (e.g. invoice
   editor's applyLockMode), the attribute selector picks it up
   automatically — no per-page wiring. Theme-aware via color-mix in
   tokens.css. See ui-design-guidelines.md "Editable input affordance". */
.input:not(:disabled):not([readonly]),
.select:not(:disabled):not([readonly]),
.textarea:not(:disabled):not([readonly]),
.picker-trigger:not(:disabled):not(.is-disabled) {
  background-color: var(--bg-input);
}

.textarea { height: auto; padding: 8px 10px; line-height: var(--lh-base); resize: vertical; min-height: 80px; }

/* .select — canonical dropdown class.
   Apply to EVERY <select> in the app. Inherits height/border/focus from the
   shared `.input, .select, .textarea` rule above; this block only adds the
   chevron and option styling. The chevron URL is a CSS token (--select-chev)
   so it adapts per theme — see tokens.css / themes.css. */
.select {
  padding-right: 26px;
  appearance: none;
  -webkit-appearance: none;
  background-image: var(--select-chev);
  background-repeat: no-repeat;
  background-position: right 8px center;
  cursor: pointer;
  /* native option list inherits these on Chromium/Firefox; Safari ignores most */
  accent-color: var(--primary);
  color-scheme: light dark;
}
.select:disabled { opacity: 0.55; cursor: not-allowed; background-color: var(--bg-alt); }
/* Branded option list — Chromium/Firefox honor these for the open dropdown.
   (Safari uses the system control and ignores option styling.) */
.select option { background: var(--bg); color: var(--text); padding: 4px 8px; }
.select option:checked,
.select option:hover { background: var(--primary-100); color: var(--primary); }
.select option:disabled { color: var(--text-4); }

.input-search {
  width: 240px;
  padding-left: 28px;
  background-image: var(--input-search-icon);
  background-repeat: no-repeat;
  background-position: 9px center;
}

/* form anatomy */
.field { display: flex; flex-direction: column; gap: 6px; }
/* Label sits one tier below the value: --text-3 keeps a clear gap from
 * the entered/selected text (which is --text), so the eye lands on the
 * data, not the chrome. Works in both themes — in dark mode the gap
 * between --text-3 and --text is wider than --text-2 → --text. */
.field-label {
  font-size: var(--fs-md);
  font-weight: 500;
  color: var(--text-3);
}
.field-hint { font-size: var(--fs-sm); color: var(--text-4); }
.field-error { font-size: var(--fs-sm); color: var(--danger); }

/* segmented control (view toggle pattern) */
.seg {
  display: inline-flex;
  height: 30px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  overflow: hidden;
}
.seg button {
  min-width: 32px;
  height: 100%;
  padding: 0 10px;
  border: 0;
  background: transparent;
  color: var(--text-3);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  font-size: var(--fs-base);
  border-right: 1px solid var(--border);
}
.seg button:last-child { border-right: 0; }
.seg button:hover { color: var(--text); background: var(--bg-alt); }
.seg button.is-active { background: var(--primary-50); color: var(--primary); }

/* checkbox / radio (faithful, restrained) */
.check {
  appearance: none;
  width: 16px; height: 16px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--bg);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  position: relative;
  transition: border-color var(--dur-1), background var(--dur-1);
}
.check:checked {
  background: var(--primary);
  border-color: var(--primary);
}
.check:checked::after {
  content: "";
  width: 9px; height: 5px;
  border-left: 2px solid #fff;
  border-bottom: 2px solid #fff;
  transform: rotate(-45deg) translate(1px, -1px);
}
.check[type="radio"] { border-radius: 50%; }
.check[type="radio"]:checked { background: #fff; }
.check[type="radio"]:checked::after {
  content: "";
  width: 8px; height: 8px;
  border: 0;
  border-radius: 50%;
  background: var(--primary);
  transform: none;
}

/* switch */
.switch {
  appearance: none;
  width: 32px; height: 18px;
  background: var(--border-strong);
  border-radius: var(--radius-pill);
  position: relative;
  cursor: pointer;
  transition: background var(--dur-2);
}
.switch::after {
  content: "";
  position: absolute;
  top: 2px; left: 2px;
  width: 14px; height: 14px;
  background: #fff;
  border-radius: 50%;
  box-shadow: var(--shadow-1);
  transition: left var(--dur-2);
}
.switch:checked { background: var(--primary); }
.switch:checked::after { left: 16px; }

/* ====================================================
 * AVATARS
 * ==================================================== */
.avatar {
  width: 24px; height: 24px;
  border-radius: 50%;
  background: var(--primary);
  color: #fff;
  font-size: 10.5px;
  font-weight: 600;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  letter-spacing: 0.02em;
}
.avatar-sm { width: 20px; height: 20px; font-size: 9.5px; }
.avatar-md { width: 28px; height: 28px; font-size: 11.5px; }
.avatar-lg { width: 36px; height: 36px; font-size: 13px; }
.avatar-xl { width: 48px; height: 48px; font-size: 16px; }
/* photo variant — drop-in support for an <img> child */
.avatar.has-photo { padding: 0; overflow: hidden; background: var(--bg); }
.avatar.has-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* For non-contact entities (company, lead, invoice, report, product,
 * article, resource), use the .row-icon / .card-icon / .drawer-icon
 * family — square neutral tile with .has-logo image override and
 * type-specific SVG glyphs. The renderEntityTile() helper in
 * assets/js/ui/data-table.js emits the right markup. The contact
 * avatar (.avatar — round, brand) is reserved for *people*. See
 * docs/ui-design-guidelines.md. */

.avatar-stack {
  display: inline-flex;
  align-items: center;
}
.avatar-stack .avatar {
  border: 2px solid var(--bg);
  margin-left: -6px;
}
.avatar-stack .avatar:first-child { margin-left: 0; }

/* ====================================================
 * BADGES, TAGS, PILLS
 *
 *   .badge — semantic status (success / warning / danger / info / neutral)
 *   .tag   — neutral category marker (e.g. cat-tag)
 *   .pill  — neutral counter / metric pill
 *   .chip  — removable selection chip
 * ==================================================== */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 20px;
  padding: 0 7px;
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  font-weight: 600;
  border: 1px solid;
  line-height: 1;
  white-space: nowrap;
  letter-spacing: 0.01em;
}
.badge-dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  flex-shrink: 0;
  background: currentColor;
}
.badge-success  { background: var(--success-bg);  border-color: var(--success-border);  color: var(--success); }
.badge-warning  { background: var(--warning-bg);  border-color: var(--warning-border);  color: var(--warning); }
.badge-danger   { background: var(--danger-bg);   border-color: var(--danger-border);   color: var(--danger); }
.badge-info     { background: var(--info-bg);     border-color: var(--info-border);     color: var(--info); }
.badge-neutral  { background: var(--bg-alt);      border-color: var(--border);          color: var(--text-2); }

/* Visibility glyph — partner-only (lock, muted) vs public (globe, primary
   tint). Sized to sit beside .badge / .tag-chip without affecting line
   height. Tooltip carries the label; the icon stands alone visually. */
.vis-badge { display: inline-flex; align-items: center; line-height: 1; flex-shrink: 0; }
.vis-badge svg { display: block; }
.vis-badge.is-partner { color: var(--text-3); }
.vis-badge.is-public  { color: var(--primary); }

/* category tag — quiet neutral marker. Truncates with ellipsis past
   ~15 characters so a long user-typed tag doesn't bleed into the next
   column on list pages. */
.cat-tag, .tag {
  height: 18px;
  padding: 0 6px;
  background: var(--bg-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: 11px;
  font-weight: 500;
  color: var(--text-2);
  display: inline-flex;
  align-items: center;
  line-height: 1;
  max-width: 15ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dot-sep { color: var(--text-4); }

/* meta-dot — small round bullet between byline / metadata fields.
   Replaces typographic `·` when we want a crisper, brand-tinted separator. */
.meta-dot {
  display: inline-block;
  width: 4px;
  height: 4px;
  margin: 0 8px;
  border-radius: 50%;
  background: var(--secondary);
  vertical-align: middle;
  flex-shrink: 0;
}

/* Quotation-status tint variants — used by quotation-edit.html and any
   future print/export surface that needs color-coded status badges.
   Mirrors the invoice print page's .pill-draft / .pill-sent pattern. */
.pill-accepted  { background: var(--success-bg); color: var(--success); }
.pill-declined  { background: var(--danger-bg);  color: var(--danger);  }
.pill-expired   { background: var(--warning-bg); color: var(--warning); }

/* metric pill — for numbers / counts (e.g. "3 PDF", "12 items").
   Truncates past ~15 characters; counts are almost always shorter
   but a status label like "Awaiting customer review" needs ellipsis. */
.pill, .stat-pill {
  height: 24px;
  padding: 0 9px;
  background: var(--bg-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--text-2);
  max-width: 15ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-variant-numeric: tabular-nums;
}
.stat-dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  flex-shrink: 0;
}
.stat-dot.pdf { background: var(--pdf); }
.stat-dot.link { background: var(--link); }
.stat-dot.app  { background: var(--installer); }
.stat-dot.primary { background: var(--primary); }
.stat-dot.success { background: var(--success); }
.stat-dot.warning { background: var(--warning); }
.stat-dot.danger  { background: var(--danger); }

/* super-user / brand emphasis tag */
.super-tag {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  height: 18px;
  padding: 0 6px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--text-3);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.super-tag .super-dot {
  width: 4px; height: 4px;
  border-radius: 50%;
  background: var(--secondary);
}

/* removable chip */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  height: 22px;
  padding: 0 4px 0 8px;
  background: var(--primary-50);
  border: 1px solid var(--primary-100);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--primary);
}
.chip button {
  width: 16px; height: 16px;
  border: 0;
  background: transparent;
  color: var(--primary);
  border-radius: var(--radius-sm);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.chip button:hover { background: var(--primary-100); }

/* ====================================================
 * SURFACES — card / panel
 * ==================================================== */
.surface {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}

.panel {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--border);
}
.panel-title {
  font-size: var(--fs-lg);
  font-weight: 600;
  color: var(--text);
}
.panel-sub {
  margin-top: 2px;
  font-size: var(--fs-md);
  color: var(--text-3);
}
.panel-body { padding: var(--space-5); }

/* ============================================================
 * Settings-page section panels — scoped overrides.
 * Addresses Step 0.5 markers 1-4 on /pages/settings.html without
 * touching the global .panel / .panel-head used elsewhere.
 *
 *   M2 — drop per-field border/bg/radius on .setting-row so the
 *        panel-body content isn't nested cards inside the panel.
 *   M3 — restructure .panel-head into a 3-column grid (icon |
 *        text-stack | action) so the icon lives in its own gutter
 *        and title + subtitle share their left edge.
 *   M4 — chrome-tint .panel-head with --bg-alt so it visually
 *        separates from the panel-body content surface.
 *   M1 — demote .setting-row .label weight 600→500, giving the
 *        section title decisive secondary-tier dominance without
 *        relying on size (size scale pending separate change).
 * ============================================================ */
.s-section .panel-head {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: start;
  gap: 4px 14px;
  background: var(--bg-alt);
}
.s-section .panel-head > .panel-title-icon {
  grid-column: 1;
  grid-row: 1 / span 2;
  margin-top: 3px;
  color: var(--text-3);
  align-self: start;
}
.s-section .panel-head > .panel-head-text {
  grid-column: 2;
  min-width: 0;
}
.s-section .panel-head > .info-panel-chev {
  grid-column: 3;
  align-self: center;
}
/* Optional right-slot actions (links, buttons, action groups) — anything
 * that isn't the icon, text-stack, or chev — lands in the same column 3.
 * Centered vertically against the head, like the chev. */
.s-section .panel-head > :not(.panel-title-icon):not(.panel-head-text):not(.info-panel-chev) {
  grid-column: 3;
  align-self: center;
}
/* Title is no longer the icon's parent (icon is now a sibling). Drop
 * inline-flex; tighten tracking so the secondary tier reads decisively
 * against the demoted field labels below it. */
.s-section .panel-title {
  display: block;
  letter-spacing: -0.01em;
}

/* Settings field rows — bare grid cells, no per-row chrome. The panel-
 * body's row-gap already provides separation. Eliminates the
 * bordered-card-inside-bordered-card nesting (M2). */
.s-section .setting-row {
  border: 0;
  border-radius: 0;
  background: transparent;
  padding: 0;
  transition: none;
}
.s-section .setting-row:hover {
  border-color: transparent;
  background: transparent;
}
.s-section .setting-row .label {
  font-weight: 500;
}

/* Bool/toggle row gap — scope the row-gap tighter than the base
 * `.setting-row.is-bool { gap: 14px }` in pages/settings.html, which
 * was tuned around the previous row padding (14px 16px). With the
 * de-card pattern that drops .setting-row padding to 0, the base 14px
 * row-gap leaves the switch low-centered in a naked vertical void
 * between label-top and desc-bottom. Override row-gap only —
 * column-gap stays at 14px from the base rule via the cascade. */
.s-section .setting-row.is-bool {
  row-gap: 4px;
}

/* M7 — collapsed sections read as a distinct, intentional state.
 * Expanded sections: chrome-tinted `--bg-alt` panel-head + content
 * `--bg` panel-body, raised on the page surface.
 * Collapsed sections: transparent panel-head + softened panel border
 * — the section sits "flat on the page" instead of raised. Title
 * stays at full `--text` so 12-section skim-readability isn't lost;
 * subtitle gets a small opacity reduction to recede.
 * Applied through the shared `.s-section.is-collapsed` selector —
 * propagates to every section automatically. */
.s-section.is-collapsed {
  border-color: color-mix(in srgb, var(--border) 50%, transparent);
}
.s-section.is-collapsed > .panel-head {
  background: transparent;
}
.s-section.is-collapsed .panel-sub {
  opacity: 0.75;
}

/* M2 — Sub-section blocks inside a settings panel-body (currently:
 * Integrations' `.gh-card` connect + target containers) lose their
 * own border + tint. The panel itself frames the section; nested
 * sub-section chrome inside it is redundant, and a `--bg-alt`
 * background on the sub-section collides with the panel-head's
 * `--bg-alt` chrome tint (marker-4 secondary issue).
 *
 * Folded into the shared `.s-section` pattern via `.s-section
 * .gh-card` rather than scoped to `#integrations`: `.gh-card` is
 * a content-shape class that currently lives only in Integrations,
 * but any future settings section that adopts the same "sub-section
 * config card" pattern picks up the same treatment automatically.
 * The class itself is GitHub-specific in name (legacy), but the
 * rule is shape-specific, not section-specific. */
.s-section .gh-card {
  margin-top: var(--space-5);
  padding: 0;
  background: transparent;
  border: 0;
  border-radius: 0;
}
.panel-foot {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-5);
  border-top: 1px solid var(--border);
  background: var(--bg-alt);
}

/* ============================================================
 * EDITABLE — view↔edit toggle for records and panels
 * ============================================================
 *
 *   <section class="panel editable" data-editable="myId">
 *     <div class="panel-head">
 *       <div class="panel-title">Title<span class="editable-state">Editing</span></div>
 *       <div class="editable-controls">
 *         <button class="btn btn-secondary btn-sm editable-trigger" data-editable-trigger="myId">Edit</button>
 *         <div class="editable-actions">
 *           <button class="btn btn-ghost btn-sm" data-editable-cancel="myId">Cancel</button>
 *           <button class="btn btn-primary btn-sm" type="submit" form="myForm">Save</button>
 *         </div>
 *       </div>
 *     </div>
 *     <div class="panel-body">
 *       <dl class="kv-list kv-grid-2" data-editable-view>...</dl>
 *       <form id="myForm" data-editable-edit hidden>...</form>
 *     </div>
 *   </section>
 *
 *   - Auto-wired by Editable controller in components.js.
 *   - Add `is-locked` to hide trigger entirely (no write permission).
 *   - Esc inside an editing panel cancels (with discard-prompt if dirty).
 */
.editable [data-editable-edit] { display: none; }
.editable.is-editing [data-editable-edit] { display: block; }
.editable.is-editing [data-editable-edit][hidden] { display: block; }
.editable.is-editing [data-editable-view] { display: none; }
.editable.is-editing .editable-trigger,
.editable-trigger.is-editing-target { display: none; }

.editable-controls {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
}
.editable-actions {
  display: none;
  align-items: center;
  gap: 6px;
}
.editable.is-editing .editable-actions,
.editable-actions.is-editing-target { display: inline-flex; }

.editable.is-locked .editable-trigger,
.editable.is-locked .editable-actions,
.editable-trigger.is-locked,
.editable-actions.is-locked { display: none !important; }

.editable-trigger {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.editable-trigger svg { color: var(--text-3); transition: color var(--dur-1); }
.editable-trigger:hover svg { color: var(--text); }

.editable-state {
  display: none;
  align-items: center;
  gap: 6px;
  padding: 2px 9px 2px 7px;
  margin-left: 10px;
  font-size: 10.5px;
  font-weight: 600;
  color: var(--primary);
  background: var(--primary-50);
  border: 1px solid color-mix(in srgb, var(--primary) 25%, transparent);
  border-radius: var(--radius-pill);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  vertical-align: middle;
  line-height: 1.4;
}
.editable-state::before {
  content: '';
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--primary);
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--primary) 60%, transparent);
  animation: editable-pulse 1.8s ease-out infinite;
}
@keyframes editable-pulse {
  0%   { box-shadow: 0 0 0 0   color-mix(in srgb, var(--primary) 50%, transparent); }
  70%  { box-shadow: 0 0 0 6px color-mix(in srgb, var(--primary) 0%,  transparent); }
  100% { box-shadow: 0 0 0 0   color-mix(in srgb, var(--primary) 0%,  transparent); }
}
.editable.is-editing .editable-state,
.editable-state.is-editing-target { display: inline-flex; }

/* Rule 11 lock-chip — inline indicator next to status badge on records
   that have left their drafting state. Quiet, muted, with a 12px lock
   glyph. Live alongside the status pill so the user sees state +
   read-only-ness as one signal. Promoted from per-page .inv-lock-hint
   in v1.48 — same shape, canonical home. */
.lock-chip {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 11.5px; color: var(--text-3);
  padding: 3px 8px; background: var(--bg-alt);
  border: 1px solid var(--border); border-radius: var(--radius-lg);
  width: fit-content;
  vertical-align: middle;
}
.lock-chip::before {
  content: ''; display: inline-block; width: 10px; height: 10px;
  background: currentColor; flex-shrink: 0;
  mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>') no-repeat center / contain;
  -webkit-mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>') no-repeat center / contain;
}

/* Ambient signal: tint the panel-head while editing (panel-shaped surfaces only) */
.editable.is-editing.panel > .panel-head {
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--primary) 4%, var(--bg)) 0%,
    var(--bg) 100%);
  border-bottom-color: color-mix(in srgb, var(--primary) 18%, var(--border));
}

/* Subtle entrance for the form when entering edit mode */
.editable.is-editing [data-editable-edit] {
  animation: editable-enter var(--dur-2, 220ms) ease-out;
}
@keyframes editable-enter {
  from { opacity: 0; transform: translateY(-2px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Header strip for editable surfaces inside tab panels (modals) */
.editable-tab-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 0 0 14px;
  margin: 0 0 16px;
  border-bottom: 1px solid var(--border);
}
.editable-tab-head-left {
  display: flex;
  align-items: center;
  min-width: 0;
}
.editable-tab-eyebrow {
  font-size: 11.5px;
  font-weight: 700;
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.editable.is-editing .editable-tab-head {
  border-bottom-color: color-mix(in srgb, var(--primary) 25%, var(--border));
}

/* ============================================================
 * KV-LIST — read-only definition list, mirrors a form-field grid
 * Pair this with .editable so the read mode looks intentional rather than
 * "form, but greyed out."
 * ============================================================ */
.kv-list {
  display: grid;
  gap: 18px;
  margin: 0;
}
.kv-list.kv-grid-2 { grid-template-columns: 1fr 1fr; gap: 18px 16px; }
.kv-list.kv-grid-3 { grid-template-columns: 1fr 1fr 1fr; gap: 18px 14px; }
@media (max-width: 720px) {
  .kv-list.kv-grid-2,
  .kv-list.kv-grid-3 { grid-template-columns: 1fr; }
}
.kv-row { min-width: 0; }
.kv-row.is-span-2 { grid-column: 1 / -1; }
.kv-row > dt {
  font-size: 12.5px;
  font-weight: 500;
  color: var(--text-2);
  margin: 0 0 4px;
}
.kv-row > dd {
  margin: 0;
  font-size: var(--fs-md);
  color: var(--text);
  min-height: 22px;
  font-weight: 500;
  letter-spacing: -0.005em;
  word-break: break-word;
}
.kv-row > dd:empty::after,
.kv-row > dd[data-empty]::after {
  content: attr(data-empty);
  color: var(--text-4);
  font-weight: 400;
  font-style: italic;
  letter-spacing: 0;
}
.kv-row > dd:empty::after { content: 'Not set'; }
.kv-row.is-multiline > dd {
  white-space: pre-wrap;
  line-height: 1.55;
  font-weight: 400;
}
.kv-row.is-mono > dd {
  font-family: var(--font-mono);
  font-size: 12.5px;
  font-weight: 400;
}

/* generic card — like product card from v2 */
.card {
  position: relative;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 18px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  cursor: pointer;
  transition: border-color var(--dur-1), box-shadow var(--dur-1), transform var(--dur-1);
}
.card:hover { border-color: var(--border-strong); box-shadow: var(--shadow-1); }
.card:active { transform: translateY(1px); }
.card:focus-visible { outline: 0; border-color: var(--primary); box-shadow: var(--ring-focus); }

.card-head { display: flex; align-items: flex-start; gap: 12px; }
.card-icon {
  width: 36px; height: 36px;
  border-radius: var(--radius);
  background: var(--bg-alt);
  border: 1px solid var(--border);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text-2);
  font-size: 12.5px;
  font-weight: 600;
  flex-shrink: 0;
  overflow: hidden;
}
/* Logo-aware icon containers — when an <img> goes inside .card-icon /
   .row-icon / .drawer-icon, constrain it to the box and preserve aspect.
   Without this a high-res logo would render at its natural size and blow
   out the layout. Pair with .has-logo to drop the placeholder chrome. */
.card-icon img,
.row-icon img,
.drawer-icon img { width: 100%; height: 100%; object-fit: contain; padding: 2px; border-radius: inherit; display: block; }
.card-icon.has-logo,
.row-icon.has-logo,
.drawer-icon.has-logo { background: var(--bg); padding: 0; }
.card-head-meta { min-width: 0; flex: 1; }
.card-name {
  font-size: var(--fs-lg);
  font-weight: 600;
  color: var(--text);
  margin: 0;
  letter-spacing: -0.005em;
  line-height: 1.3;
}
.card-sub {
  margin-top: 3px;
  font-size: var(--fs-sm);
  color: var(--text-3);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}
.card-kebab {
  width: 26px; height: 26px;
  border-radius: var(--radius);
  background: transparent;
  border: 1px solid transparent;
  color: var(--text-3);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.card-kebab:hover { background: var(--bg-alt); border-color: var(--border); color: var(--text); }
.card-kebab:focus-visible { outline: 0; border-color: var(--primary); box-shadow: var(--ring-focus); }
.card-kebab[disabled] { opacity: 0.35; cursor: default; }
.card-kebab[disabled]:hover { background: transparent; border-color: transparent; color: var(--text-3); }
.card-tagline {
  font-size: var(--fs-base);
  color: var(--text-2);
  margin: 0;
  line-height: 1.5;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  min-height: 39px;
}
.card-stats { display: flex; gap: 6px; flex-wrap: wrap; }
.card-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-top: 12px;
  border-top: 1px solid var(--border);
  font-size: 12px;
}
.card-updated { color: var(--text-3); font-variant-numeric: tabular-nums; }
.view-cta {
  color: var(--primary);
  font-weight: 500;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.card:hover .view-cta { color: var(--primary-700); }

/* "new" / dashed card — call-to-create */
.new-card {
  border: 1px dashed var(--border-strong);
  background: transparent;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 32px 24px;
  color: var(--text-3);
  cursor: pointer;
  border-radius: var(--radius-lg);
  transition: border-color var(--dur-1), color var(--dur-1), background var(--dur-1);
  min-height: 220px;
  gap: 10px;
}
.new-card:hover { border-color: var(--primary); color: var(--primary); background: var(--primary-50); }
.new-card .plus {
  width: 32px; height: 32px;
  border-radius: 50%;
  border: 1px solid currentColor;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.new-card .new-title { font-size: var(--fs-base); font-weight: 600; color: inherit; }
.new-card .new-sub {
  font-size: 12px;
  color: var(--text-4);
  text-align: center;
  max-width: 220px;
  line-height: 1.5;
}
.new-card:hover .new-sub { color: var(--primary); opacity: 0.75; }

/* KPI / metric card */
.kpi {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.kpi-label {
  font-size: var(--fs-md);
  color: var(--text-3);
  font-weight: 500;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.kpi-value {
  font-size: var(--fs-3xl);
  font-weight: 600;
  color: var(--text);
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}
.kpi-delta {
  font-size: var(--fs-sm);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  display: inline-flex;
  align-items: center;
  gap: 3px;
}
.kpi-delta.up { color: var(--success); }
.kpi-delta.down { color: var(--danger); }
.kpi-delta.flat { color: var(--text-3); }
.kpi-spark {
  margin-top: 8px;
  height: 36px;
  width: 100%;
}
/* small caption/footnote line under the value — used by dashboard variants */
.kpi-meta {
  font-size: var(--fs-sm);
  color: var(--text-3);
}

/* tighter, mono-numeric variant (uppercase tracked label, 26px mono value).
 * Used in dashboard strips that pack 4–5 KPIs in a row. */
.kpi.is-dense {
  padding: 14px 16px;
  gap: 4px;
  border-radius: var(--radius);
}
.kpi.is-dense .kpi-label {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-3);
}
.kpi.is-dense .kpi-value {
  font-family: var(--font-mono);
  font-size: 26px;
  line-height: 1.1;
}
.kpi.is-dense .kpi-meta { font-size: 11.5px; }

/* warning surface — value, label, bg, border all flip to the warning palette.
 * Pair with .is-dense for staged-items / pending-review tiles. */
.kpi.is-warning {
  background: var(--warning-bg);
  border-color: var(--warning-border);
}
.kpi.is-warning .kpi-label,
.kpi.is-warning .kpi-value { color: var(--warning); }

/* primary surface — the SINGLE most decision-driving KPI on a page
 * (per design-guidelines Step 2 Level 1). Use sparingly: at most one
 * .kpi.is-primary visible at a time. Examples: "Pending approvals"
 * on dashboard, "Drafts ready" on content list, "Quotes awaiting AM"
 * on quotations. Added v1.46.2. */
.kpi.is-primary {
  background: var(--primary-50);
  border-color: color-mix(in srgb, var(--primary) 22%, transparent);
}
.kpi.is-primary .kpi-label { color: var(--primary-700); }
.kpi.is-primary .kpi-value { color: var(--primary); }

/* ====================================================
 * GRID LAYOUTS
 * ==================================================== */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 14px;
}
.grid-cards-sm {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 8px;
}
.grid-kpi {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-3);
}
.grid-2col { display: grid; grid-template-columns: 2fr 1fr; gap: var(--space-4); }

/* ====================================================
 * TABLES
 * ==================================================== */
.table-wrap {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  /* Horizontal scroll fallback when the rendered table is wider than
     its container (narrow viewports, sidebar expanded, lots of
     columns). Vertical scrolling delegated to the page so a long
     list grows the page rather than introducing a separate scroller.
     Was `overflow-y: hidden` previously — that broke `position:
     sticky` on the kebab column in some browsers AND clipped the
     bottom row's hover ring. */
  overflow-x: auto;
}
.table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-base);
}
.table thead th {
  text-align: left;
  font-weight: 600;
  font-size: var(--fs-md);
  color: var(--text-3);
  letter-spacing: 0.02em;
  padding: 10px 14px;
  background: var(--bg-alt);
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
  /* Positioned to host the resize handle on its right edge. */
  position: relative;
}
/* Column resize handle — owned by data-table.js. 6px-wide hit zone on
   the right edge of every <th data-col>. Stays invisible until hover;
   a thin colored bar appears on hover and during drag. Double-click
   resets the column to its declared default width. Skipped on the
   .col-actions kebab column (44px fixed). */
.table thead th .th-resize {
  position: absolute;
  top: 0;
  right: 0;
  width: 6px;
  height: 100%;
  cursor: col-resize;
  user-select: none;
  z-index: 2;
}
.table thead th .th-resize::after {
  content: '';
  position: absolute;
  top: 25%;
  right: 2px;
  width: 2px;
  height: 50%;
  background: transparent;
  border-radius: var(--radius-sm);
  transition: background var(--dur-1);
}
.table thead th:hover .th-resize::after,
.table thead th .th-resize:hover::after,
.table thead th .th-resize.is-active::after {
  background: var(--primary);
}
/* Suppress hover/text-select while dragging anywhere on the page. */
body.is-col-resizing,
body.is-col-resizing * { cursor: col-resize !important; }
body.is-col-resizing { user-select: none; }
body.is-col-resizing .table tbody tr:hover { background: transparent; }

/* Auto-fit measurement helper — temporarily applied to one column's
   <th> and <td> elements while data-table.js reads their natural
   widths via scrollWidth. Unconstrains the cell + all descendants
   (including .partner-cell-meta's flex:1; min-width:0 which would
   otherwise collapse). Class is added and removed inside a single
   synchronous block, so the browser never paints the unconstrained
   state — no visible flicker. */
.dt-measuring-col,
.dt-measuring-col * {
  max-width: none !important;
  overflow: visible !important;
  text-overflow: clip !important;
  white-space: nowrap !important;
}
.dt-measuring-col .partner-cell-meta {
  flex: 0 0 auto !important;
  min-width: max-content !important;
}
.table tbody td {
  padding: 12px 14px;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
  color: var(--text);
  /* Default to single-line + ellipsis. Multi-line cells (long
     descriptions, address blobs) opt out via `.td-wrap`. The
     `max-width: 0` here is the table-layout-auto truncation trick:
     the browser distributes column width by header content, then
     body cells truncate to whatever space is left rather than
     pushing the column wider. Without it, .truncate inside a cell
     never fires because the cell has expanded to fit the content. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 0;
}
.table tbody td.td-wrap {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  max-width: none;
}
.table tbody tr:last-child td { border-bottom: 0; }
.table tbody tr { transition: background var(--dur-1); }
.table tbody tr:hover { background: var(--bg-alt); }
.table tbody tr.is-selected { background: var(--primary-50); }
.table tbody tr.is-selected:hover { background: #E5E3F2; }
.table .num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  /* Numbers must not truncate — "$1,234.56" → "$1,2…" reads as a
     wrong amount, not a hidden one. Let the column take its content
     width; if the table can't fit, the wrap scrolls horizontally. */
  white-space: nowrap;
  overflow: visible;
  text-overflow: clip;
  max-width: none;
}
/* col-actions — kebab column, ALWAYS rightmost. Sticky so it stays
   visible when the table scrolls horizontally on narrow viewports.
   The left border + box-shadow appear ONLY when the table is actually
   scrollable horizontally; otherwise the sticky cell sits flush with
   the rest of the row. */
.table .col-actions {
  width: 44px;
  max-width: 44px;
  text-align: right;
  padding-left: 4px;
  padding-right: 8px;
  overflow: visible;
  position: sticky;
  right: 0;
  z-index: 1;
  background: var(--bg);
}
.table thead th.col-actions {
  background: var(--bg-alt);
  z-index: 2;
}
.table tbody tr:hover td.col-actions { background: var(--bg-alt); }
.table tbody tr.is-selected td.col-actions { background: var(--primary-50); }
.table tbody tr.is-selected:hover td.col-actions { background: #E5E3F2; }
/* Subtle left edge so the sticky kebab reads as separated from the
   scrolling content underneath when horizontal scroll engages. The
   shadow is keyed to the wrap class so it ONLY shows when the wrap
   actually scrolls — `.table-wrap` always has the rule, but the
   shadow is invisible when nothing is scrolled under it (browsers
   render the shadow against the body background, which matches the
   sticky bg, so the shadow is barely perceptible until scroll). */
.table-wrap .table .col-actions {
  box-shadow: -8px 0 8px -8px rgba(15, 23, 42, 0.08);
}
.table .checkbox-col { width: 28px; }

/* .partner-cell — canonical avatar+name(+sub) cell layout for table rows.
 * Name is historical (originated in quotations); use it for ANY avatar+name
 * cell (contact OR company). Pair the avatar with `.text-strong .truncate`
 * for the primary line and `.text-sm .text-muted .truncate` for the sub.
 * Inner non-avatar block needs `min-width: 0` so the truncation works. */
.partner-cell {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.partner-cell > .partner-cell-meta {
  min-width: 0;
  flex: 1;
}

/* Empty-state cell injected by data-table.js when rows.length === 0.
 * Matches the muted tone of the page-level .empty-state but stays inline.
 * Overrides the default-truncation rule above — empty messages are
 * prose, not tabular data, and should wrap rather than ellipsis. */
.table tbody td.dt-empty {
  padding: 24px 14px;
  text-align: center;
  color: var(--text-3);
  border-bottom: 0;
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  max-width: none;
}
/* Brief de-emphasis during fetch — visual hint that the table is reloading. */
.table.is-loading tbody { opacity: 0.55; transition: opacity var(--dur-1); }

/* Sortable header cells. Whole <th> becomes clickable; an arrow appears
 * on the active sort direction. Used by assets/js/ui/data-table.js. */
.table thead th.sortable {
  cursor: pointer;
  user-select: none;
  transition: color var(--dur-1), background var(--dur-1);
}
.table thead th.sortable:hover {
  color: var(--text);
  background: color-mix(in srgb, var(--bg) 60%, var(--bg-alt));
}
.table thead th.sortable .th-arrow {
  display: inline-block;
  width: 0; height: 0;
  margin-left: 4px;
  vertical-align: middle;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  opacity: 0;
  transition: opacity var(--dur-1), transform var(--dur-1);
}
.table thead th.sortable.is-sorted { color: var(--text); }
.table thead th.sortable.is-sorted .th-arrow { opacity: 1; }
.table thead th.sortable.is-sorted.dir-asc  .th-arrow { border-bottom: 5px solid currentColor; }
.table thead th.sortable.is-sorted.dir-desc .th-arrow { border-top:    5px solid currentColor; }

/* ====================================================
 * Data-table footer: pagination strip
 * Sits below .table-wrap; reusable across list pages.
 * ==================================================== */
.table-pagination {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px 14px;
  margin-top: 12px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  font-size: var(--fs-md);
  color: var(--text-2);
  flex-wrap: wrap;
}
.table-pagination .tp-summary {
  font-variant-numeric: tabular-nums;
  color: var(--text-3);
}
.table-pagination .tp-spacer { flex: 1; min-width: 8px; }
.table-pagination .tp-pages {
  display: flex;
  align-items: center;
  gap: 6px;
  font-variant-numeric: tabular-nums;
  color: var(--text-2);
}
.table-pagination .tp-pages .btn {
  /* Pager arrow buttons — keep them compact + square-ish so the row
     reads as a control group rather than three independent buttons. */
  min-width: 28px;
  height: 28px;
  padding: 0 6px;
  font-size: 14px;
  line-height: 1;
}
.table-pagination .tp-page-input {
  width: 56px;
  height: 28px;
  padding: 0 6px;
  text-align: center;
  font-variant-numeric: tabular-nums;
  background: var(--bg-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  font-size: var(--fs-md);
}
.table-pagination .tp-page-input:focus {
  outline: none;
  border-color: var(--primary);
  background: var(--bg);
  box-shadow: var(--ring-focus);
}
.table-pagination .btn[disabled] {
  opacity: 0.4;
  cursor: not-allowed;
}
/* Rows-per-page cluster: label + select sit together as one inline
   group so they read as a single "Rows per page: 50" control rather
   than two unrelated bits. */
.table-pagination .tp-size-label {
  color: var(--text-3);
  font-size: var(--fs-md);
  margin-left: 4px;
}
.table-pagination select.select {
  height: 28px;
  padding: 0 24px 0 8px;
  font-size: var(--fs-md);
}
@media (max-width: 720px) {
  .table-pagination {
    gap: 8px;
    padding: 8px 10px;
  }
  .table-pagination .tp-spacer { display: none; }
}

/* ====================================================
 * Column-visibility — pure-CSS hide rule.
 * The popover (assets/js/ui/col-vis.js) writes hidden column keys
 * to <table>.dataset.hiddenCols (space-separated). This rule then
 * hides any cell tagged `data-col="<key>"` whose key is in that
 * list. The page render function never has to know about hidden
 * state — it just emits `data-col` on every <th> + <td>.
 * ==================================================== */
.table[data-hidden-cols] [data-col] { /* nothing — concrete keys via [~=] below */ }
/* `~=` matches space-separated tokens. The actual key matching is
   done dynamically (one rule per HIDDEN key would be impractical),
   but data attributes accept this selector pattern. The single rule
   below uses an attribute-selector trick: when `data-hidden-cols`
   contains the same token as a cell's `data-col`, hide it. */
.table-wrap .table th[data-col],
.table-wrap .table td[data-col] { /* base — visible by default */ }

/* The actual hide rule: pages with multiple columns set
   data-hidden-cols="key1 key2 …" on the <table>. We can't
   programmatically reference data-col from data-hidden-cols in pure
   CSS, so col-vis.js writes one inline <style> rule per hidden key
   into the document head when needed. See col-vis.js applyToTable(). */

/* ====================================================
 * Column-visibility popover (data-table)
 * Anchored to the gear/columns button in the toolbar.
 * ==================================================== */
.col-vis-popover {
  position: absolute;
  z-index: 100;
  min-width: 200px;
  padding: 6px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card-elevated);
  font-size: var(--fs-md);
  color: var(--text);
}
.col-vis-popover .cv-header {
  padding: 6px 10px 4px;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-3);
}
.col-vis-popover .cv-row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 6px 10px;
  background: transparent;
  border: 0;
  border-radius: var(--radius);
  text-align: left;
  color: var(--text);
  font-size: var(--fs-md);
  cursor: pointer;
}
.col-vis-popover .cv-row:hover { background: var(--bg-alt); }
.col-vis-popover .cv-row input[type="checkbox"] { margin: 0; }
.col-vis-popover .cv-foot {
  display: flex;
  justify-content: flex-end;
  padding: 4px 6px 0;
  margin-top: 4px;
  border-top: 1px solid var(--border);
}
.col-vis-popover .cv-reset {
  padding: 4px 8px;
  background: transparent;
  border: 0;
  color: var(--text-3);
  font-size: 11.5px;
  cursor: pointer;
}
.col-vis-popover .cv-reset:hover { color: var(--text); }

/* ====================================================
 * LIST ROW PATTERN (v2 product list)
 * Reusable for any "icon · meta · pills · meta · chevron" row
 * ==================================================== */
.list-rows {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.list-row {
  display: grid;
  grid-template-columns: 36px 1fr auto auto 28px;
  gap: 14px;
  align-items: center;
  padding: 12px 14px;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  transition: background var(--dur-1);
}
.list-row:last-child { border-bottom: 0; }
.list-row:hover { background: var(--bg-alt); }
.list-row.is-open { background: var(--primary-50); }
.list-row.is-open:hover { background: #E5E3F2; }

.row-icon {
  width: 32px; height: 32px;
  border-radius: var(--radius);
  background: var(--bg-alt);
  border: 1px solid var(--border);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text-2);
  font-size: 12px;
  font-weight: 600;
}
.list-row.is-open .row-icon {
  background: var(--bg);
  border-color: var(--primary-100);
  color: var(--primary);
}
.row-meta { min-width: 0; }
.row-name {
  font-size: 13.5px;
  font-weight: 500;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.row-tagline {
  margin-top: 2px;
  font-size: 12.5px;
  color: var(--text-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.row-pills {
  display: inline-flex;
  gap: 6px;
  color: var(--text-3);
  font-size: 12px;
  align-items: center;
}
.row-meta-right {
  font-size: 12px;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  min-width: 96px;
  text-align: right;
}
.row-chev {
  width: 24px; height: 24px;
  color: var(--text-3);
  transition: transform 150ms, color var(--dur-1);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.list-row.is-open .row-chev { transform: rotate(90deg); color: var(--primary); }

/* expandable region under a list-row */
.expand {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-3) ease;
  background: var(--bg-alt);
  border-bottom: 1px solid var(--border);
}
.expand.is-open { grid-template-rows: 1fr; }
.expand-inner { overflow: hidden; min-height: 0; }
.expand-pad { padding: 14px 18px 18px 64px; }

/* ====================================================
 * RESOURCE ITEM — file-type cards (PDF / link / installer)
 * ==================================================== */
.res-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
  text-decoration: none;
  color: inherit;
  min-width: 0;
  transition: border-color var(--dur-1), background var(--dur-1);
}
.res-item:hover { border-color: var(--border-strong); background: var(--bg); }

.res-icon {
  width: 28px; height: 28px;
  border-radius: var(--radius-sm);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.03em;
}
.res-icon.pdf       { background: var(--pdf-bg);       color: var(--pdf);       border: 1px solid var(--pdf-border); }
.res-icon.link      { background: var(--link-bg);      color: var(--link);      border: 1px solid var(--link-border); }
.res-icon.installer { background: var(--installer-bg); color: var(--installer); border: 1px solid var(--installer-border); }
.res-icon.installer svg { width: 14px; height: 14px; }

/* The text column inside a .res-item. Must be flex-column with min-width:0
   so its children get a real block-level container that can clip overflow.
   Without `display: flex` here the children render inline, which silently
   defeats text-overflow:ellipsis on .res-sub (text-overflow only applies
   to block-like elements). */
.res-meta {
  min-width: 0;
  flex: 1;
  display: flex;
  flex-direction: column;
}
.res-label {
  font-size: var(--fs-md);
  font-weight: 500;
  color: var(--text);
  display: flex;
  align-items: center;
  gap: 4px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
/* Sub-line (e.g. URL or filename · size). Owns its own truncation rules
   here so callsites don't have to add inline styles that don't work on
   inline spans. */
.res-sub {
  display: block;
  margin-top: 1px;
  font-size: var(--fs-sm);
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
.res-ext-icon { width: 10px; height: 10px; color: var(--text-4); flex-shrink: 0; }

/* ====================================================
 * DRAWER (right slide-over)
 * ==================================================== */
.drawer {
  position: fixed; inset: 0;
  z-index: var(--z-drawer);
  pointer-events: none;
}
.drawer.is-open { pointer-events: auto; }
.drawer-backdrop {
  position: absolute; inset: 0;
  background: var(--backdrop);
  opacity: 0;
  transition: opacity var(--dur-2) ease;
}
.drawer.is-open .drawer-backdrop { opacity: 1; }
.drawer-panel {
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: var(--drawer-w);
  max-width: calc(100vw - 80px);
  background: var(--bg);
  border-left: 1px solid var(--border);
  box-shadow: var(--shadow-drawer);
  transform: translateX(100%);
  transition: transform var(--dur-3) var(--ease-out);
  display: flex;
  flex-direction: column;
}
.drawer.is-open .drawer-panel { transform: translateX(0); }

/* Below 720px the standard 520px drawer + 80px gutter would leave the
   panel cramped (or, on narrow phones, force horizontal scroll inside).
   Drop the gutter and let the drawer go edge-to-edge. The same rule
   applies to centered modals so forms and tables don't get scrunched. */
@media (max-width: 720px) {
  .drawer-panel { max-width: 100vw; border-left: 0; }
  .modal-panel,
  .modal-panel.modal-wide {
    max-width: 100vw;
    width: 100vw;
    max-height: 100vh;
    border-radius: 0;
    border-left: 0;
    border-right: 0;
  }
}

.drawer-head {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px 20px;
  border-bottom: 1px solid var(--border);
}
.drawer-prod {
  display: flex;
  align-items: center;
  gap: 12px;
  flex: 1;
  min-width: 0;
}
.drawer-icon {
  width: 40px; height: 40px;
  border-radius: var(--radius);
  background: var(--primary-50);
  border: 1px solid var(--primary-100);
  color: var(--primary);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  font-weight: 600;
  flex-shrink: 0;
}
.drawer-head-meta { min-width: 0; }
.drawer-name {
  font-size: var(--fs-xl);
  font-weight: 600;
  margin: 0;
  color: var(--text);
  letter-spacing: -0.01em;
}
.drawer-sub {
  margin-top: 3px;
  font-size: 12px;
  color: var(--text-3);
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.drawer-body { flex: 1; overflow-y: auto; padding: 20px; }
.drawer-desc {
  font-size: 13.5px;
  color: var(--text-2);
  margin: 0 0 20px;
  line-height: 1.55;
}
/* ============================================================
 * Detail / drawer panel primitives
 *
 * The canonical implementation lives in pages/leads.html (the lead
 * drawer is the visual reference — see ui-design-guidelines.md
 * Step 10 "Detail page" pattern). These classes are promoted from
 * `.ld-*` so any drawer / modal-tab / detail-page can compose them.
 *
 * Each class signals a SPECIFIC kind of content per Step 4 Rule 1
 * (no two adjacent containers may use the same visual pattern).
 * Don't stack identical .info-panel siblings — vary them with
 * .quote-panel / .timeline / state-tiles / pref-grid / info-rows /
 * disclosure-toggle so the user sees structural cues, not a
 * uniform stack of frames.
 * ============================================================ */

/* Quote panel — for content that is the user's own words
 * (form responses, partner notes, lead intake message). Blue
 * left accent per Rule 3 ("blue = the user's voice"). */
.quote-panel {
  background: var(--info-bg);
  border: 1px solid var(--info-border);
  border-left: 3px solid var(--info);
  border-radius: var(--radius);
  padding: 13px 16px;
  margin-bottom: 10px;
}
.quote-panel-label {
  font-size: 10px; font-weight: 700;
  letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--info);
  margin-bottom: 7px;
}
.quote-panel-text { font-size: 14px; line-height: 1.7; color: var(--text); }

/* Info panel — bordered sub-panel with a small-caps header,
 * optional count badge, and optional collapse chevron.
 * Used for grouped reference data, timelines, list-within-record
 * content, preferences, etc. The HEAD's small-caps + count tells
 * the user what's inside before they read. */
.info-panel {
  background: var(--bg-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: 10px;
  overflow: hidden;
}
.info-panel-head {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 14px;
  border-bottom: 1px solid var(--border);
  background: var(--bg);
  cursor: pointer; user-select: none;
  transition: background var(--dur-1);
}
.info-panel-head:hover { background: var(--bg-alt); }
.info-panel-label {
  flex: 1;
  font-size: 10px; font-weight: 700;
  letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--text-3);
}
.info-panel-count {
  font-size: 11px; font-weight: 600;
  padding: 1px 7px;
  background: var(--bg-alt); border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  color: var(--text-3);
}
.info-panel-chev {
  display: inline-flex; align-items: center;
  color: var(--text-3);
  transition: transform var(--dur-1) ease;
}
.info-panel.is-collapsed .info-panel-chev { transform: rotate(-90deg); }
.info-panel.is-collapsed > *:not(.info-panel-head) { display: none !important; }
.info-panel.is-collapsed .info-panel-head { border-bottom: 0; }
.info-panel-body { padding: 10px 14px; }

/* Collapsibility for `.section-card[data-collapsible]` (full-page
 * editors). Sibling primitive to `.info-panel`: same chevron, same
 * `.is-collapsed` toggle, different DOM shape (the head is the
 * `.section-title` h2 and there's no body wrapper to hide). Behavior
 * lives in assets/js/ui/section-collapse.js. */
.section-card[data-collapsible] > .section-title {
  cursor: pointer;
  user-select: none;
  display: flex; align-items: center; gap: 8px;
}
.section-card[data-collapsible] > .section-title:focus-visible {
  outline: 2px solid var(--ring); outline-offset: 2px;
  border-radius: var(--radius-sm);
}
.section-card[data-collapsible] > .section-title .info-panel-chev {
  margin-left: auto;
}
.section-card.is-collapsed[data-collapsible] > .section-title .info-panel-chev {
  transform: rotate(-90deg);
}
.section-card.is-collapsed > *:not(.section-title) { display: none !important; }
.section-card.is-collapsed { padding-bottom: 14px; }

/* Collapsibility for `.panel[data-collapsible]` (Settings page sub-panels).
 * Same idiom as `.section-card[data-collapsible]` above; different DOM
 * shape (head is `.panel-head` div with title + sub + optional right
 * slot). Behavior lives in assets/js/ui/section-collapse.js.
 *
 * The chevron is appended to the END of `.panel-head` and pushed to the
 * right edge with margin-left: auto — so it sits AFTER any existing
 * right-side button (Refresh, Open Theme Studio, etc.). The
 * isInteractiveTarget check in section-collapse.js prevents button
 * clicks from triggering the panel toggle. */
.panel[data-collapsible] > .panel-head {
  cursor: pointer;
  user-select: none;
  /* Override the global `.panel-head { justify-content: space-between }`.
   * With a 3-child case (title-block + action + chev) space-between
   * floats the action to the middle; with a 2-child case (title-block
   * + chev) it puts a giant gap between them. flex-start + a growing
   * title-block does the right thing in both cases — see the flex-1
   * rule below. */
  justify-content: flex-start;
}
.panel[data-collapsible] > .panel-head:focus-visible {
  outline: 2px solid var(--ring); outline-offset: -2px;
}
/* Title-block grows to fill the remaining width. Pushes action button
 * (if any) and the chev to the right edge of the head. min-width: 0
 * allows long subtitles to truncate instead of forcing the head wider. */
.panel[data-collapsible] > .panel-head > div:first-child {
  flex: 1;
  min-width: 0;
}
/* Chevron sits at the FAR RIGHT of the head row — the canonical
 * placement matching `.info-panel-chev` (info-panel.js) and
 * `.section-card[data-collapsible] > .section-title .info-panel-chev`
 * (section-collapse.js). Appended last by section-collapse.js; natural
 * DOM order + the title-block's flex-grow puts it at the right edge. */
.panel[data-collapsible] > .panel-head > .info-panel-chev {
  color: var(--text-3);
  transition: transform var(--dur-1);
  flex-shrink: 0;
}
.panel.is-collapsed[data-collapsible] > .panel-head > .info-panel-chev {
  transform: rotate(-90deg);
}
.panel.is-collapsed > *:not(.panel-head) { display: none !important; }
.panel.is-collapsed > .panel-head { border-bottom: 0; }

/* Section-icon slot inside .panel-title — small monochrome glyph
 * matching the topbar tab icon for this section (e.g. cog for General,
 * users for Authentication). Populated by a one-shot pass at boot in
 * pages/settings.html that reads the data-icon attribute. */
.panel-title { display: inline-flex; align-items: center; gap: 8px; }
.panel-title-icon { display: inline-flex; align-items: center; color: var(--text-3); flex-shrink: 0; }

/* State tile — repeating sub-unit *inside* an info-panel for
 * list-within-a-record content (e.g. interest categories, line
 * items). Per Step 4 Rule 6: the LEFT STRIPE encodes state.
 *   Default (.state-tile)               → green stripe + full opacity = complete/actionable
 *   .state-tile.is-incomplete           → amber stripe + 70% opacity   = needs follow-up
 *   .state-tile.is-blocked              → gray  stripe                 = inactive/closed */
.state-tile { display: flex; border-top: 1px solid var(--border); }
.state-tile-stripe { width: 4px; flex-shrink: 0; background: var(--success); }
.state-tile-body { flex: 1; padding: 10px 14px; min-width: 0; }
.state-tile-title { font-size: 13px; font-weight: 600; color: var(--text); line-height: 1.3; }
.state-tile-kv {
  margin-top: 5px;
  display: grid; grid-template-columns: 100px 1fr; gap: 2px 10px;
  font-size: 11.5px;
}
.state-tile-kv-key   { color: var(--text-3); padding: 1px 0; }
.state-tile-kv-value { color: var(--text-2); font-weight: 500; padding: 1px 0; }
.state-tile-cta { font-size: 11px; color: var(--text-3); font-style: italic; margin-top: 4px; }
.state-tile.is-incomplete .state-tile-stripe { background: var(--warning); }
.state-tile.is-incomplete .state-tile-body   { opacity: 0.72; }
.state-tile.is-incomplete .state-tile-title  { color: var(--text-3); }
.state-tile.is-blocked    .state-tile-stripe { background: var(--text-3); }
.state-tile.is-blocked    .state-tile-body   { opacity: 0.55; }

/* Timeline — required (Rule 9) for any record with a state field.
 * Vertical line + colored dots + transition rows (label + meta).
 * Mounts inside an .info-panel-body. */
.timeline { padding: 10px 14px 14px; }
.timeline-row {
  display: grid; grid-template-columns: 14px 1fr; gap: 10px;
  position: relative; padding: 4px 0;
}
.timeline-row:not(:last-child)::before {
  content: ''; position: absolute; left: 6px; top: 18px; bottom: -4px;
  width: 2px; background: var(--border);
}
.timeline-dot {
  width: 10px; height: 10px; border-radius: 50%;
  margin-top: 6px; background: var(--bg);
  border: 2px solid var(--text-3);
  position: relative; z-index: 1;
}
.timeline-dot.create  { border-color: var(--info); }
.timeline-dot.advance { border-color: var(--success); }
.timeline-dot.contact { border-color: var(--warning); }
.timeline-dot.close   { border-color: var(--text-3); }
.timeline-dot.assign  { border-color: var(--primary); }
.timeline-label { font-size: 13px; color: var(--text); font-weight: 500; line-height: 1.4; }
.timeline-meta  { font-size: 11px; color: var(--text-3); margin-top: 1px; }

/* Info row — compact reference field row (small-caps key + value).
 * For Level 4 reference metadata in a panel: phone, country,
 * billing email, etc. Mounts inside an .info-panel (no body padding,
 * since each row paints its own padding + top border). */
.info-row {
  display: grid; grid-template-columns: 116px 1fr; gap: 12px;
  padding: 9px 14px;
  border-top: 1px solid var(--border);
  font-size: 13px; align-items: baseline;
}
.info-row-k {
  font-size: 10px; font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--text-3);
  padding-top: 2px;
}
.info-row-v {
  color: var(--text); font-weight: 500;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.info-row-empty { color: var(--text-3); font-weight: 400; font-style: italic; }

/* Pref grid — fixed-group glanceable values. Each item is a tiny
 * card: small-caps key on top, value below. Visually distinct from
 * info-row (which is label-left, value-right). Use for short
 * fixed groups (preferences, capability flags). */
.pref-grid { display: flex; flex-wrap: wrap; gap: 8px; padding: 10px 14px; }
.pref-item {
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 6px 12px; min-width: 80px;
}
.pref-key {
  font-size: 10px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--text-3); margin-bottom: 2px;
}
.pref-value { font-size: 13px; font-weight: 500; color: var(--text); }

/* Disclosure footer — collapsed-by-default container for L4/L5
 * metadata (IPs, source IDs, raw timestamps, debug fields). Sits
 * at the bottom of the body. Per Step 11 forbidden patterns:
 * "empty states as visible rows" — this is the alternative. */
.disclosure-toggle {
  display: flex; align-items: center; gap: 5px;
  font-size: 11.5px; color: var(--text-3);
  background: none; border: 0; cursor: pointer;
  padding: 8px 0; width: 100%; text-align: left;
  transition: color var(--dur-1);
}
.disclosure-toggle:hover { color: var(--text-2); }
.disclosure-toggle .chev { transition: transform var(--dur-1) ease; }
.disclosure-toggle.is-open .chev { transform: rotate(90deg); }
.disclosure-body {
  display: grid; grid-template-columns: 90px 1fr;
  gap: 3px 10px; font-size: 11.5px; padding: 4px 0 12px;
}
.disclosure-k {
  font-size: 10px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--text-3); padding: 2px 0;
}
.disclosure-v { color: var(--text-3); padding: 2px 0; }

/* Inline-edit notes — empty-state placeholder + click-to-edit
 * textarea. Per Step 5: inline edit pattern for a single field.
 * Per Rule 4: empty states are placeholders, not visible rows. */
.notes-edit-display {
  padding: 12px 14px; font-size: 13px; line-height: 1.65;
  color: var(--text-2); cursor: text;
  min-height: 44px;
}
.notes-edit-display.is-empty { color: var(--text-3); font-style: italic; }
.notes-edit-input {
  width: 100%; padding: 12px 14px; border: 0;
  background: var(--bg-alt); color: var(--text);
  font-family: inherit; font-size: 13px; line-height: 1.65;
  resize: vertical; min-height: 80px;
}
.notes-edit-input:focus { outline: 2px solid var(--primary); outline-offset: -2px; }
.notes-edit-hint { font-size: 11px; color: var(--text-3); padding: 0 14px 10px; }

/* Drawer chips row — small chips beneath the drawer-name (status,
 * assignment, count). Use sparingly — this is Level 1 strip content. */
.drawer-chips {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  margin-top: 5px;
}

/* Drawer strip-actions — slot inside .drawer-head between the
 * .drawer-prod identity block and the close X. Holds the primary
 * action button + secondary actions + kebab. Per Step 10 "Detail
 * page" pattern, actions live in the STRIP, not in a footer. */
.drawer-strip-actions {
  display: flex; align-items: center; gap: 4px;
  margin-left: auto;
  flex-shrink: 0;
}

.drawer-section { margin-bottom: 20px; }
.drawer-section:last-child { margin-bottom: 0; }
.drawer-section-label {
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--text-3);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  margin-bottom: 10px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.drawer-section-count {
  font-size: 11px;
  font-weight: 500;
  color: var(--text-4);
  letter-spacing: 0;
  text-transform: none;
  font-variant-numeric: tabular-nums;
}
.drawer-res-list { display: flex; flex-direction: column; gap: 6px; }
.drawer-res-list .res-item { padding: 12px 14px; }
.drawer-foot {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 12px 20px;
  border-top: 1px solid var(--border);
  background: var(--bg-alt);
}
.drawer-foot .spacer { flex: 1; }

/* ====================================================
 * MODAL (centered, smaller than drawer)
 * ==================================================== */
.modal {
  position: fixed; inset: 0;
  z-index: var(--z-modal);
  display: none;
  align-items: center;
  justify-content: center;
  padding: var(--space-6);
}
.modal.is-open { display: flex; }
.modal-backdrop {
  position: absolute; inset: 0;
  background: var(--backdrop-strong);
}
.modal-panel {
  position: relative;
  width: 100%;
  max-width: 480px;
  /* Bound the panel to viewport height so long content scrolls inside
     the body rather than pushing the footer off-screen. Uses dvh where
     supported (mobile browser-chrome aware); --space-6 padding above
     and below leaves a comfortable inset from the viewport edges. */
  max-height: calc(100dvh - var(--space-6) * 2);
  display: flex;
  flex-direction: column;
  background: var(--bg);
  /* Crisp 1px border separates the panel from the backdrop in dark mode
   * (where bg + backdrop sit close in luminance). Layered shadows: a soft
   * brand-tinted glow gives the modal presence without being loud, then
   * the main drop shadow handles depth. */
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-modal-glow);
  overflow: hidden;
  animation: modal-in var(--dur-3) var(--ease-out);
}
/* Many modals wrap head/body/foot in a <form> for native submit
 * semantics. The form must propagate the flex column so the body
 * inside it can scroll while head/foot stay pinned. */
.modal-panel > form {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}
@keyframes modal-in {
  from { opacity: 0; transform: translateY(8px) scale(.985); }
  to   { opacity: 1; transform: none; }
}
.modal-head {
  padding: 16px 20px;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-shrink: 0;
}
.modal-title { font-size: var(--fs-lg); font-weight: 600; }
.modal-body {
  padding: 20px;
  font-size: var(--fs-base);
  color: var(--text-2);
  /* Body absorbs any vertical overflow — head + foot stay pinned. */
  flex: 1 1 auto;
  overflow-y: auto;
  min-height: 0;
}
.modal-foot {
  padding: 12px 20px;
  border-top: 1px solid var(--border);
  background: var(--bg-alt);
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  flex-shrink: 0;
}

/* ====================================================
 * TOAST
 * ==================================================== */
.toast-stack {
  position: fixed;
  bottom: var(--space-4);
  right: var(--space-4);
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  pointer-events: none;
}
.toast {
  pointer-events: auto;
  min-width: 280px;
  max-width: 360px;
  padding: 12px 14px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-left: 3px solid var(--primary);
  border-radius: var(--radius);
  box-shadow: var(--shadow-2);
  font-size: var(--fs-base);
  color: var(--text);
  display: flex;
  align-items: flex-start;
  gap: 10px;
  animation: toast-in var(--dur-3) var(--ease-out);
}
.toast.success { border-left-color: var(--success); }
.toast.warning { border-left-color: var(--warning); }
.toast.danger  { border-left-color: var(--danger); }
.toast-title { font-weight: 600; }
.toast-msg { color: var(--text-2); margin-top: 2px; font-size: var(--fs-md); }
@keyframes toast-in {
  from { opacity: 0; transform: translateX(8px); }
  to   { opacity: 1; transform: none; }
}

/* ====================================================
 * EMPTY / LOADING STATES
 * ==================================================== */
.empty {
  padding: 40px 16px;
  text-align: center;
  color: var(--text-3);
  font-size: var(--fs-md);
}
.empty-state {
  padding: 56px 24px;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}
.empty-state .empty-icon {
  width: 44px; height: 44px;
  border-radius: 50%;
  background: var(--bg-alt);
  border: 1px solid var(--border);
  color: var(--text-3);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 4px;
}
.empty-state .empty-title { font-size: var(--fs-lg); font-weight: 600; }
.empty-state .empty-sub { font-size: var(--fs-md); color: var(--text-3); max-width: 320px; line-height: 1.5; }
/* CTA inside an empty state gets a soft brand glow — the operator
   landed on a blank surface; the next step should pull the eye. */
.empty-state .btn-primary {
  box-shadow: var(--shadow-glow-primary);
}
.empty-state .btn-primary:hover {
  box-shadow: var(--shadow-glow-primary-hover);
}

/* richer empty state — diagrammatic SVG hint at the absent data, then title/sub/CTA */
.empty-rich {
  padding: 36px 24px 32px;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  color: var(--text-3);
}
.empty-rich-svg {
  width: 96px;
  height: 64px;
  color: color-mix(in srgb, var(--primary) 55%, var(--text-4));
  margin-bottom: 6px;
  opacity: 0.85;
}
.empty-rich-svg [data-ghost] { stroke-dasharray: 3 3; opacity: 0.55; }
.empty-rich-title { font-size: var(--fs-lg); font-weight: 600; color: var(--text); letter-spacing: -0.01em; }
.empty-rich-sub { font-size: var(--fs-md); color: var(--text-3); max-width: 360px; line-height: 1.5; }
.empty-rich-action { margin-top: 10px; }

.skeleton {
  background: linear-gradient(90deg, var(--bg-sunken) 0%, var(--bg-alt) 50%, var(--bg-sunken) 100%);
  background-size: 200% 100%;
  animation: skeleton 1.4s infinite linear;
  border-radius: var(--radius-sm);
}
@keyframes skeleton { from { background-position: 200% 0; } to { background-position: -200% 0; } }

/* ====================================================
 * MENU (context menu + dropdown)
 * Used for right-click menus AND user profile dropdown.
 * Positioned absolutely by menu.js.
 * ==================================================== */
.menu {
  position: fixed;
  z-index: var(--z-menu);
  min-width: 220px;
  max-width: 340px;
  /* Tighter outer padding on the menu shell — the breathing room now
     comes from the items themselves and the gap between groups. */
  padding: 6px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-menu);
  font-size: var(--fs-base);
  animation: menu-in 120ms var(--ease-out);
  outline: 0;
}
@keyframes menu-in {
  from { opacity: 0; transform: translateY(-4px) scale(.98); }
  to   { opacity: 1; transform: none; }
}

.menu-item {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  height: 36px;
  padding: 0 12px;
  background: transparent;
  border: 0;
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: var(--fs-base);
  text-align: left;
  cursor: pointer;
  outline: 0;
}
.menu-item:hover, .menu-item:focus { background: var(--bg-alt); }
/* Keyboard focus gets a distinct ring on top of the bg highlight so tabbing
   users can tell focus from hover (mouse :focus already has bg-alt). */
.menu-item:focus-visible {
  background: var(--bg-alt);
  box-shadow: inset 0 0 0 2px var(--primary);
}
.menu-item.is-danger { color: var(--danger); }
.menu-item.is-danger:hover, .menu-item.is-danger:focus {
  background: var(--danger-bg);
}
.menu-item.is-danger:focus-visible {
  box-shadow: inset 0 0 0 2px var(--danger);
}
.menu-item.is-disabled { opacity: 0.45; pointer-events: none; }
.menu-item.is-checked { color: var(--primary); }

.menu-ic {
  width: 16px; height: 16px;
  display: inline-flex; align-items: center; justify-content: center;
  color: currentColor;
  opacity: 0.85;
  flex-shrink: 0;
}
.menu-label { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* 2-line action row — { label, desc } items grow vertical and host the
   secondary muted line below the primary label. */
.menu-item.is-2line { height: auto; padding: 8px 12px; align-items: flex-start; }
.menu-item.is-2line .menu-ic { margin-top: 1px; }
.menu-item.is-2line .menu-kbd,
.menu-item.is-2line .menu-meta,
.menu-item.is-2line .menu-chev { margin-top: 2px; }
.menu-text { display: flex; flex-direction: column; gap: 1px; flex: 1; min-width: 0; }
.menu-text .menu-label { white-space: normal; }
.menu-desc {
  font-size: var(--fs-xs);
  color: var(--text-3);
  line-height: 1.35;
  white-space: normal;
}
.menu-kbd {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--text-3);
  padding: 1px 5px;
  background: var(--bg-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  flex-shrink: 0;
}
.menu-chev {
  width: 14px; height: 14px;
  color: var(--text-3);
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
/* Right-side muted metadata in menu items — current state ("Self"),
   version tags ("v1.32.5"), external-link arrows. Combines with .menu-chev
   when both are present (e.g. "View as · Self ›"). */
.menu-meta {
  font-size: 11px;
  color: var(--text-3);
  flex-shrink: 0;
}
/* Destructive zone gets extra gap above its divider per Step 5b — the
   visual separation should clearly mark a severity transition. */
.menu-sep:has(+ .menu-item.is-danger) { margin-top: 10px; }

.menu-sep {
  height: 1px;
  background: var(--border);
  margin: 6px 4px;
}
.menu-header {
  padding: 10px 12px 6px;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* Record-context block — sits at the top of a row-action menu (or any
   action menu that targets a single record). Two lines:
     1. label + ref  ("Invoice INV-2026-0001") — ref is inline,
        smaller, monospace, muted
     2. avatar + sub ("[AC] Acme Corp") — small logo/initials tile +
        company name
   No hover, not clickable (role=presentation). Bottom border separates
   the "what this acts on" header from the actions below. */
.menu-context {
  padding: 12px 14px;
  margin: -6px -6px 6px;
  background: var(--bg-sunken);
  border-bottom: 1px solid var(--border);
  border-radius: calc(var(--radius-lg) - 1px) calc(var(--radius-lg) - 1px) 0 0;
}
/* In dark mode --bg-sunken is darker than the menu shell's --bg, so the
   band disappears. Flip to --bg-alt (a step UP from --bg) so the band
   reads as a distinct header in dark mode too. */
[data-theme="dark"] .menu-context {
  background: var(--bg-alt);
}
.menu-context-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 8px;
  min-width: 0;
}
.menu-context-label {
  font-size: 12px;
  font-weight: 700;
  color: var(--primary);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  line-height: 1.2;
  flex-shrink: 0;
}
.menu-context-ref {
  font-family: var(--font-mono);
  font-size: 11.5px;
  font-weight: 500;
  color: var(--text-3);
  letter-spacing: -0.005em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
.menu-context-sub-row {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
/* Small avatar tile — square initials by default, image when logoSrc
   is provided. Sized between menu-icon (16) and row-icon (32) so it
   reads as identity without crowding the menu. */
.menu-context-avatar {
  width: 22px;
  height: 22px;
  border-radius: var(--radius-sm);
  background: var(--bg-sunken);
  border: 1px solid var(--border);
  color: var(--text-2);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  flex-shrink: 0;
}
.menu-context-avatar.has-logo {
  background: var(--bg);
  padding: 0;
}
.menu-context-avatar img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: 1px;
}
.menu-context-avatar-initials {
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.02em;
}
.menu-context-sub {
  font-size: 13px;
  color: var(--text-2);
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  letter-spacing: -0.005em;
  min-width: 0;
}

.menu-custom {
  padding: 8px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 4px;
}
.menu-custom:last-child { border-bottom: 0; margin-bottom: 0; }

/* ----- topbar user menu — chrome surface per docs/ui-design-guidelines.md §5b -----
   Zones: Identity → Theme → Workspace (admin) → Account → Help → Destructive.
   Identity zone gets the most padding per Step 5b (most-prominent zone).
   Theme & identity rows live inside .menu-custom (8px padding + border-bottom). */
.user-menu-id {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px;            /* outer .menu-custom adds 8px → ~18px from edge */
}
.user-menu-id .avatar { flex-shrink: 0; }
.user-menu-id-text { min-width: 0; flex: 1; }

/* Name + role chip share one row; email and pulse stack below */
.user-menu-id-row {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.user-menu-id-name {
  font-weight: 600;
  font-size: 13.5px;
  color: var(--text);
  letter-spacing: -0.01em;
  min-width: 0;
}

/* Role chip — info-blue (categorical per Rule 3, never amber/warning) */
.user-menu-id-chip {
  display: inline-flex;
  align-items: center;
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 2px 6px;
  border-radius: var(--radius-sm);
  background: var(--info-bg);
  border: 1px solid var(--info-border);
  color: var(--info);
  line-height: 1.2;
  flex-shrink: 0;
  white-space: nowrap;
}

.user-menu-id-email {
  font-size: 11.5px;
  color: var(--text-3);
  margin-top: 2px;
}

/* Live pulse — green dot + "Online" (Step 5b: identity zone should have a pulse) */
.user-menu-id-pulse {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 11px;
  color: var(--text-3);
  margin-top: 6px;
}
.user-menu-id-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--success);
  flex-shrink: 0;
  box-shadow: var(--ring-focus-success);
}

/* "Theme" label — same visual style as .menu-header but lives inside a
   .menu-custom row alongside the picker, so it inherits .menu-custom's
   8px horizontal padding without double-padding. */
.user-menu-theme-label {
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  padding: 0 0 6px;
}

.theme-picker {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 4px;
  padding: 4px;
  background: var(--bg-alt);
  border-radius: var(--radius-sm);
}
.theme-picker .tp {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 8px 4px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--text-3);
  font-size: 11px;
  font-weight: 500;
  cursor: pointer;
  transition: background var(--dur-1), color var(--dur-1), border-color var(--dur-1);
}
.theme-picker .tp:hover { color: var(--text); background: var(--bg); }
/* Active state — bg switch + primary text + soft border. Dropped the
   redundant box-shadow; bg+border+color already gives a clear "selected". */
.theme-picker .tp.is-active {
  background: var(--bg);
  border-color: var(--primary-100);
  color: var(--primary);
}
.theme-picker .tp:focus-visible {
  outline: none;
  box-shadow: var(--ring-focus-soft);
}
.theme-picker .tp svg { width: 16px; height: 16px; }
.theme-picker .tp-label { line-height: 1; }

/* ====================================================
 * UPLOAD — drag-and-drop / click-to-upload
 *
 * Two flavors:
 *   .upload          — single dropzone (file picker on click)
 *   .upload.compact  — slim row variant for inline use
 *
 * JS lives in upload.js. Markup pattern:
 *
 *   <div class="upload" data-upload data-accept="image/*" data-max="262144">
 *     <div class="upload-zone">…</div>
 *     <ul class="upload-list"></ul>
 *   </div>
 * ==================================================== */
.upload {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.upload-zone {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 28px 20px;
  background: var(--bg-alt);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-lg);
  color: var(--text-3);
  cursor: pointer;
  transition: border-color var(--dur-1), background var(--dur-1), color var(--dur-1);
  text-align: center;
}
.upload-zone:hover { border-color: var(--primary); color: var(--primary); }
.upload.is-drag .upload-zone {
  border-color: var(--primary);
  background: var(--primary-50);
  color: var(--primary);
}
.upload-zone .uz-icon {
  width: 36px; height: 36px;
  border-radius: 50%;
  background: var(--bg);
  border: 1px solid var(--border);
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--text-3);
  margin-bottom: 4px;
}
.upload.is-drag .upload-zone .uz-icon { background: var(--bg); border-color: var(--primary); color: var(--primary); }
.upload-zone .uz-title { font-size: var(--fs-base); font-weight: 600; color: var(--text); }
.upload-zone .uz-sub { font-size: var(--fs-sm); color: var(--text-3); }
.upload.compact .upload-zone { padding: 14px; flex-direction: row; gap: 10px; text-align: left; }
.upload.compact .uz-icon { width: 28px; height: 28px; margin-bottom: 0; }

/* ====================================================
 * RM-SOURCE — Add/Edit resource modal source picker
 *
 * One field group, four states (data-state on .rm-source):
 *   "empty"  — drop zone + URL fallback both visible.
 *   "file"   — compact card with filename / size / MIME.
 *   "url"    — compact card with URL.
 *   "locked" — edit mode; compact card; remove hidden, hint shown.
 *
 * data-files-enabled="false" hides the drop zone (when
 * feature.admin.partner_files is off → URL-only modal).
 *
 * Reuses tokens — no hardcoded colors. Light + dark inherit
 * via [data-theme].
 * ==================================================== */

.rm-source {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
}

/* ---- Drop zone (primary path) ---- */
.rm-dropzone {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-5) var(--space-4);
  background: var(--bg-alt);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-lg);
  color: inherit;
  text-align: left;
  cursor: pointer;
  outline: 0;
  font-family: inherit;
  transition:
    background var(--dur-1),
    border-color var(--dur-1),
    box-shadow var(--dur-1);
}
.rm-dropzone:hover {
  background: var(--primary-50);
  border-color: var(--primary);
  border-style: solid;
}
.rm-dropzone:focus-visible {
  background: var(--primary-50);
  border-color: var(--primary);
  border-style: solid;
  box-shadow: var(--ring-focus);
}
.rm-dropzone.is-dragover {
  background: var(--primary-50);
  border-color: var(--primary);
  border-style: solid;
  box-shadow: var(--ring-focus);
}
.rm-dropzone-icon {
  width: 28px;
  height: 28px;
  flex-shrink: 0;
  color: var(--text-3);
  transition: color var(--dur-1);
}
.rm-dropzone:hover .rm-dropzone-icon,
.rm-dropzone:focus-visible .rm-dropzone-icon,
.rm-dropzone.is-dragover .rm-dropzone-icon {
  color: var(--primary);
}
.rm-dropzone-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.rm-dropzone-text strong {
  font-size: var(--fs-base);
  font-weight: 600;
  color: var(--text);
  line-height: 1.3;
}
.rm-dropzone-meta {
  font-size: var(--fs-sm);
  color: var(--text-3);
  line-height: 1.4;
}

/* ---- Divider with inline label ---- */
.rm-source-divider {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--fs-xs);
  font-weight: 500;
  color: var(--text-4);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.rm-source-divider::before,
.rm-source-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}

/* ---- URL row (secondary path) ---- */
.rm-url-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  height: 38px;
  padding: 0 var(--space-3);
  background: var(--bg-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  transition:
    background var(--dur-1),
    border-color var(--dur-1),
    box-shadow var(--dur-1);
}
.rm-url-row:hover { border-color: var(--border-strong); }
.rm-url-row:focus-within {
  background: var(--bg);
  border-color: var(--primary);
  box-shadow: var(--ring-focus-soft);
}
.rm-url-row svg {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  color: var(--text-4);
}
.rm-url-row input {
  flex: 1;
  min-width: 0;
  height: 100%;
  border: 0;
  outline: 0;
  background: transparent;
  padding: 0;
  font-family: var(--font-mono);
  font-size: var(--fs-md);
  color: var(--text);
}
.rm-url-row input::placeholder { color: var(--text-4); }

/* ---- Compact card (chosen / locked state) ---- */
.rm-source-card {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--bg-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  min-width: 0;
}
.rm-source-card-icon {
  width: 28px;
  height: 28px;
  flex-shrink: 0;
  color: var(--primary);
}
.rm-source-card-meta {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.rm-source-card-name {
  font-size: var(--fs-base);
  font-weight: 600;
  color: var(--text);
}
.rm-source-card-sub {
  font-size: var(--fs-sm);
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.rm-source-card-remove {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius);
  color: var(--text-3);
  cursor: pointer;
  outline: 0;
  transition:
    background var(--dur-1),
    color var(--dur-1),
    border-color var(--dur-1);
}
.rm-source-card-remove:hover {
  background: var(--bg);
  color: var(--danger);
  border-color: var(--danger-border);
}
.rm-source-card-remove:focus-visible {
  box-shadow: var(--ring-focus-soft);
}
.rm-source-card-remove svg {
  width: 14px;
  height: 14px;
}

/* ---- Locked-mode hint + inline error ---- */
.rm-source-locked-hint {
  margin: 0;
  padding: var(--space-2) var(--space-3);
  background: var(--warning-bg);
  border: 1px solid var(--warning-border);
  border-radius: var(--radius);
  color: var(--warning);
  font-size: var(--fs-sm);
  line-height: 1.5;
}
.rm-source-error {
  margin: 0;
  padding: var(--space-2) var(--space-3);
  background: var(--danger-bg);
  border: 1px solid var(--danger-border);
  border-radius: var(--radius);
  color: var(--danger);
  font-size: var(--fs-sm);
  line-height: 1.5;
}

/* ---- Source tabs (Upload / Library / Web Link) ----
   Three peer tabs across the top of the source area, one source at
   a time. Active tab's panel shows; others hide. When the user has
   picked something (file/url/locked state) the whole tab strip is
   hidden and the source-card + Change-source button take over. */
.rm-tabs {
  display: flex; gap: 4px;
  margin-bottom: 12px;
  border-bottom: 1px solid var(--border);
}
.rm-tab {
  appearance: none; background: transparent; border: 0;
  padding: 8px 14px; margin-bottom: -1px;
  font: inherit; color: var(--text-2);
  border-bottom: 2px solid transparent;
  cursor: pointer; white-space: nowrap;
  transition: color var(--dur-1), border-color var(--dur-1);
}
.rm-tab:hover { color: var(--text); }
.rm-tab[aria-selected="true"] {
  color: var(--primary);
  border-bottom-color: var(--primary);
  font-weight: 600;
}
.rm-tab:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--primary);
  border-radius: var(--radius-sm);
}

/* Library tab content — empty-state framing with a single CTA. */
.rm-library-empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center;
  padding: 24px 16px;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  background: var(--bg-alt);
  color: var(--text-3);
}
.rm-library-empty svg { color: var(--text-3); opacity: 0.7; }

/* ---- Change-source affordance ----
   Inline text-button below the source card. Shown only in file/url
   states (NOT locked — locked is edit-mode of an existing resource
   where source replacement requires delete + re-add). Returns to the
   empty state with one click — same as the X on the card, but
   discoverable as a labeled action. */
.rm-change-source {
  display: inline-flex; align-items: center; gap: 6px;
  margin-top: 8px;
  padding: 4px 0;
  background: transparent; border: 0;
  color: var(--text-3); font: inherit; font-size: 12px;
  cursor: pointer;
}
.rm-change-source:hover { color: var(--primary); }
.rm-change-source svg { opacity: 0.85; }

/* ---- State-driven visibility ----
   data-state ∈ { empty, file, url, locked }.
   data-source-tab ∈ { upload, library, url } drives which tab-panel
   is visible when state is empty. In selected states, the tab strip
   + all panels collapse and the source-card takes over. */
.rm-source-card,
.rm-source-locked-hint,
.rm-change-source      { display: none; }
.rm-source-error       { display: none; }

/* Empty state — show the tabs + the panel that matches data-source-tab. */
.rm-source[data-state="empty"] .rm-tab-panel { display: none; }
.rm-source[data-state="empty"][data-source-tab="upload"]  .rm-tab-panel[data-rm-panel="upload"],
.rm-source[data-state="empty"][data-source-tab="library"] .rm-tab-panel[data-rm-panel="library"],
.rm-source[data-state="empty"][data-source-tab="url"]     .rm-tab-panel[data-rm-panel="url"]      { display: block; }

/* Selected states — hide the whole tab UI, show the source-card. */
.rm-source[data-state="file"]   .rm-source-card,
.rm-source[data-state="url"]    .rm-source-card,
.rm-source[data-state="locked"] .rm-source-card        { display: flex; }
.rm-source[data-state="file"]   .rm-change-source,
.rm-source[data-state="url"]    .rm-change-source      { display: inline-flex; }
.rm-source[data-state="locked"] .rm-source-locked-hint { display: block; }

.rm-source[data-state="file"]   .rm-tabs,
.rm-source[data-state="file"]   .rm-tab-panel,
.rm-source[data-state="url"]    .rm-tabs,
.rm-source[data-state="url"]    .rm-tab-panel,
.rm-source[data-state="locked"] .rm-tabs,
.rm-source[data-state="locked"] .rm-tab-panel { display: none; }

/* Locked state: hide the remove button (the warning hint replaces it). */
.rm-source[data-state="locked"] .rm-source-card-remove { display: none; }

/* feature.admin.partner_files = false → no upload UI. Hide the
   Upload tab + its panel. Default tab flips to "library" via JS. */
.rm-source[data-files-enabled="false"] .rm-tab[data-rm-tab="upload"],
.rm-source[data-files-enabled="false"] .rm-tab-panel[data-rm-panel="upload"] { display: none; }

@media (prefers-reduced-motion: reduce) {
  .rm-dropzone, .rm-url-row, .rm-source-card-remove,
  .rm-dropzone-icon { transition: none; }
}

.upload-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.upload-item {
  display: grid;
  grid-template-columns: 28px 1fr auto auto;
  gap: 10px;
  align-items: center;
  padding: 8px 10px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.upload-item .ui-icon {
  width: 28px; height: 28px;
  border-radius: var(--radius-sm);
  background: var(--bg-alt);
  border: 1px solid var(--border);
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--text-2);
  font-size: 9px; font-weight: 700;
  letter-spacing: 0.03em;
}
.upload-item .ui-meta { min-width: 0; }
.upload-item .ui-name { font-size: var(--fs-md); font-weight: 500; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.upload-item .ui-sub  { font-size: var(--fs-sm); color: var(--text-3); margin-top: 1px; font-variant-numeric: tabular-nums; }
.upload-item .ui-progress {
  width: 80px; height: 4px;
  background: var(--bg-sunken);
  border-radius: var(--radius-pill);
  overflow: hidden;
}
.upload-item .ui-progress > i {
  display: block;
  height: 100%;
  width: 0;
  background: var(--primary);
  border-radius: var(--radius-pill);
  transition: width 120ms linear;
}
.upload-item.is-error { border-color: var(--danger-border); background: var(--danger-bg); }
.upload-item.is-error .ui-name { color: var(--danger); }
.upload-item.is-done .ui-progress > i { background: var(--success); }

/* ====================================================
 * TOOLTIP (data-tip)
 *   Default: above the element.
 *   data-tip-pos="right": to the right (used by collapsed nav).
 * ==================================================== */
[data-tip] { position: relative; }
[data-tip]::after {
  content: attr(data-tip);
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  background: var(--bg-inverse);
  color: var(--bg);
  font-size: 11px;
  padding: 4px 8px;
  border-radius: var(--radius-sm);
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-1);
  z-index: var(--z-tooltip);
}
[data-tip]:hover::after,
[data-tip]:focus-visible::after { opacity: 1; }

/* nav tooltips: only visible when sidebar is collapsed, positioned to the right */
.nav-item[data-tip]::after { display: none; }
.is-nav-collapsed .nav-item[data-tip]::after,
.is-nav-collapsed .nav-collapse-btn[data-tip]::after {
  display: block;
  bottom: auto;
  left: calc(100% + 10px);
  top: 50%;
  transform: translateY(-50%);
}

/* ====================================================
 * STAT CARD — label + tabular value, with optional delta
 * ALERT      — warning/info/danger banner
 * SECTION GROUP — uppercase-tracked label + thin divider
 * ==================================================== */
.stat-card {
  padding: 14px 16px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}
.stat-card-label {
  font-size: 10.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-3);
}
.stat-card-value {
  font-size: 20px;
  font-weight: 600;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}
.stat-card-delta { font-size: 11px; color: var(--success); font-variant-numeric: tabular-nums; }
.stat-card-delta.is-down { color: var(--danger); }
.stat-card.is-compact { padding: 10px 12px; }
.stat-card.is-compact .stat-card-value { font-size: 16px; }

.alert {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  border: 1px solid;
  border-radius: var(--radius-lg);
  font-size: 13px;
}
.alert-icon { flex-shrink: 0; opacity: 0.85; display: inline-flex; }
.alert-body { flex: 1; min-width: 0; }
.alert-title { font-weight: 600; color: var(--text); }
.alert-meta { font-size: 11.5px; color: var(--text-3); margin-top: 2px; }
.alert-action { flex-shrink: 0; font-size: 12px; font-weight: 600; color: inherit; text-decoration: none; white-space: nowrap; }
.alert-action:hover { text-decoration: underline; }
.alert.alert-warning { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning); }
.alert.alert-info    { background: var(--info-bg);    border-color: var(--info-border);    color: var(--info); }
.alert.alert-danger  { background: var(--danger-bg);  border-color: var(--danger-border);  color: var(--danger); }

/* Form-field grid utilities — canonical 2-col / 3-col layouts inside
 * forms (modal bodies, page sections). Use over inline grid styles. */
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.field-row-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; }
@media (max-width: 720px) {
  .field-row, .field-row-3 { grid-template-columns: 1fr; }
}

.section-group { margin-bottom: 24px; }
.section-group:last-child { margin-bottom: 0; }
.section-group-label {
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--text-3);
  margin: 0 0 10px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border);
}

/* ====================================================
 * BUILDER — form + sticky-summary layout
 *
 * Used by long forms that produce a priced output (quote / invoice / PO):
 *
 *   <div class="builder-grid">
 *     <div class="panel">                           <!-- form wrapper -->
 *       <div class="builder-section">
 *         <h3 class="builder-section-title">…</h3>
 *         <p class="builder-section-desc">…</p>
 *         <div class="field">…</div>
 *         <div class="row-2">…</div>                <!-- 2-col field row -->
 *         <div class="line-items-head">…</div>      <!-- editor table -->
 *         <div class="line-items-row">…</div>
 *         <button class="line-items-add">+ Add line</button>
 *       </div>
 *       <div class="builder-foot">…</div>
 *     </div>
 *     <aside class="builder-summary">               <!-- sticky right rail -->
 *       <div class="builder-summary-head">…</div>
 *       <div class="builder-summary-body">
 *         <div class="totals-row"><span class="k">…</span><span class="v">…</span></div>
 *         <div class="totals-divider"></div>
 *         <div class="totals-row is-total">…</div>
 *       </div>
 *       <div class="builder-summary-foot">…</div>
 *     </aside>
 *   </div>
 *
 * The line-items grid template defaults to one wide column + three numeric
 * columns + a 28px remove-button slot; pages that need a different shape
 * override .line-items-head/.line-items-row grid-template-columns inline.
 * ==================================================== */
.builder-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 320px;
  gap: var(--space-5);
  align-items: flex-start;
}

.builder-section {
  padding: 18px 20px;
  border-bottom: 1px solid var(--border);
}
.builder-section:last-child { border-bottom: 0; }

.builder-section-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  margin: 0 0 2px;
}
.builder-section-desc {
  font-size: 12.5px;
  color: var(--text-3);
  margin: 0 0 14px;
}

/* field rhythm scoped to builder — rest of the app keeps .field's default */
.builder-section .field { margin-bottom: var(--space-3); }
.builder-section .field:last-child { margin-bottom: 0; }
.builder-section .input,
.builder-section .select,
.builder-section .textarea { width: 100%; height: 32px; }
.builder-section .textarea { height: auto; min-height: 80px; padding: 8px 10px; line-height: 1.5; }

.row-2 { display: grid; grid-template-columns: 1fr 1fr;     gap: var(--space-3); margin-bottom: var(--space-3); }
.row-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: var(--space-3); margin-bottom: var(--space-3); }
.row-2 > .field, .row-3 > .field { margin-bottom: 0; }

.field-required { color: var(--secondary); font-weight: 700; }

/* footer bar inside a builder panel */
.builder-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 20px;
  background: var(--bg-alt);
  border-top: 1px solid var(--border);
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}

/* line items editor */
.line-items-head,
.line-items-row {
  display: grid;
  grid-template-columns: 1fr 110px 110px 110px 28px;
  gap: 10px;
  align-items: center;
}
.line-items-head {
  padding: 0 4px 8px;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-3);
}
.line-items-head .ralign { text-align: right; }
.line-items-row { padding: 8px 4px; border-top: 1px solid var(--border); }

.line-items-product { display: flex; align-items: center; gap: 10px; min-width: 0; }
.line-items-meta    { min-width: 0; flex: 1; }
.line-items-unit    { font-size: 11.5px; color: var(--text-3); margin-top: 1px; }
.line-items-price,
.line-items-total {
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-size: 13px;
  color: var(--text-2);
}
.line-items-total { color: var(--text); font-weight: 500; }

.line-items-remove {
  width: 24px; height: 24px;
  border: 0;
  background: transparent;
  color: var(--text-4);
  cursor: pointer;
  border-radius: var(--radius-sm);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.line-items-remove:hover { background: var(--bg-alt); color: var(--text-2); }

.line-items-add {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin-top: 8px;
  padding: 8px 10px;
  background: transparent;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  cursor: pointer;
  color: var(--text-2);
  font-size: 12.5px;
  font-weight: 500;
  width: 100%;
  transition: border-color var(--dur-1), color var(--dur-1), background var(--dur-1);
}
.line-items-add:hover { border-color: var(--primary); color: var(--primary); background: var(--primary-50); }

/* sticky right-rail summary used alongside .builder-grid */
.builder-summary {
  position: sticky;
  top: calc(var(--top-h) + 16px);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}
.builder-summary-head {
  padding: 14px 16px;
  border-bottom: 1px solid var(--border);
}
.builder-summary-title { font-size: 13px; font-weight: 600; margin: 0; }
.builder-summary-sub   { font-size: 11.5px; color: var(--text-3); margin: 2px 0 0; }
.builder-summary-body { padding: 12px 16px; }
.builder-summary-foot {
  padding: 12px 16px 16px;
  border-top: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.builder-summary-foot .btn { width: 100%; height: 36px; justify-content: center; }

/* totals — k/v rows inside .builder-summary-body (or anywhere else) */
.totals-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 6px 0;
  font-size: 12.5px;
}
.totals-row .k { color: var(--text-2); }
.totals-row .v {
  color: var(--text);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}
.totals-row.is-total .k { font-weight: 600; color: var(--text); font-size: 13px; }
.totals-row.is-total .v { font-weight: 600; color: var(--text); font-size: 14px; }
.totals-divider { height: 1px; background: var(--border); margin: 6px 0; }
.totals-note {
  margin-top: 10px;
  padding: 10px;
  background: var(--bg-alt);
  border-radius: var(--radius);
  font-size: 11.5px;
  color: var(--text-3);
  line-height: 1.5;
}

/* check-row — clickable label wrapping a .check + primary/sub text */
.check-row {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 6px 0;
  cursor: pointer;
}
.check-row .check { margin-top: 2px; }
.check-row-text { font-size: 12.5px; color: var(--text); }
.check-row-sub  {
  display: block;
  font-size: 11.5px;
  color: var(--text-3);
  margin-top: 1px;
}

/* ====================================================
 * SEG modifier — full-width segmented control
 *
 *   <div class="seg is-full" data-seg="term">
 *     <button class="is-active" data-val="1">1y</button>
 *     <button data-val="2">2y</button>
 *   </div>
 *
 * Inherits everything from .seg above; this just stretches the container
 * and equalizes button widths so it sits inside a builder field.
 * ==================================================== */
.seg.is-full {
  width: 100%;
  height: 32px;
}
.seg.is-full button {
  flex: 1;
  font-weight: 500;
}
.seg.is-full button.is-active { font-weight: 600; }

/* .seg.is-quiet — view-toggle variant. Active button keeps a neutral
 * background and text color; only the icon picks up the secondary
 * accent (gold) via a scoped color override so currentColor on the
 * SVG inherits it. Result: the selected view is signaled by the icon
 * hue without a loud background tint competing with other toolbar
 * controls. (One of the few approved uses of --secondary outside
 * super-user surfaces.) */
.seg.is-quiet button.is-active {
  background: var(--bg-alt);
  color: var(--text);
  font-weight: 500;
}
.seg.is-quiet button.is-active svg { color: var(--secondary); }

/* ====================================================
 * COMBOBOX — searchable, type-ahead picker
 *
 * Use when the source list is large (partners, line items, tags) —
 * native <select class="select"> falls over above ~30 options.
 *
 *   <div class="combobox" data-combobox>
 *     <input class="input combobox-input" type="text" placeholder="Search…" />
 *   </div>
 *
 * Then either:
 *   - Pre-declare items via  data-combobox-items='[{"value":1,"label":"…"}]'
 *   - OR  Combobox.attach(root, { fetchItems: async q => […], onPick: … })
 *
 * Behavior lives in components.js (keyboard nav, ARIA, debounced fetch,
 * click-outside, viewport-aware flip).
 * ==================================================== */
.combobox {
  position: relative;
  display: inline-flex;
  flex-direction: column;
  width: 100%;
}
/* the input itself just inherits .input — no extra rules needed for the
 * default state. The chevron is omitted because the field is type-to-search
 * and the affordance is the placeholder + opened panel. */
.combobox-input { width: 100%; }

.combobox-panel {
  position: absolute;
  left: 0; right: 0;
  top: calc(100% + 4px);
  z-index: var(--z-menu);
  max-height: 240px;
  overflow-y: auto;
  padding: 4px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-menu);
  display: none;
  animation: combobox-in 120ms var(--ease-out);
}
.combobox.is-open .combobox-panel { display: block; }
.combobox-panel.is-flipped {
  top: auto;
  bottom: calc(100% + 4px);
}
@keyframes combobox-in {
  from { opacity: 0; transform: translateY(-2px); }
  to   { opacity: 1; transform: none; }
}

.combobox-option {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 6px 8px;
  background: transparent;
  border: 0;
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: var(--fs-base);
  text-align: left;
  cursor: pointer;
  outline: 0;
}
.combobox-option:hover,
.combobox-option.is-active {
  background: var(--bg-alt);
}
.combobox-option.is-active { color: var(--text); }
.combobox-option-label {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 1;
}
.combobox-option-sub {
  font-size: var(--fs-sm);
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}

.combobox-empty {
  padding: 12px 10px;
  font-size: var(--fs-md);
  color: var(--text-3);
  text-align: center;
}

/* ====================================================
 * product-lines — shared product-grouped line-item editor.
 *
 * Used by quotation.html (catalog-only) and invoice.html
 * (catalog-plus-service). Renamed from the page-inline
 * .qg-* family in v1.34. Behavior lives in
 * assets/js/ui/product-lines.js.
 * ==================================================== */
.product-lines        { display: block; }
.product-lines-list   { display: flex; flex-direction: column; gap: 14px; margin-top: 4px; }
.product-lines-foot   { display: flex; flex-direction: column; gap: 10px; margin-top: 14px; }

/* Each card carries a --pl-accent (set inline at render time
   from the decorative palette in tokens.css). The accent
   drives the 3px left bar + the icon fill + per-card "Show more"
   link. Falls back to --border-strong so a card without an
   accent (Service) still reads as a regular bordered panel. */
.product-lines-card {
  position: relative;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.product-lines-card::before {
  content: '';
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 3px;
  background: var(--pl-accent, var(--border-strong));
  pointer-events: none;
  z-index: 1;
}
.product-lines-card--service::before {
  background: var(--text-3);
}
.product-lines-head {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 12px 14px 12px 16px;
  background: var(--bg-alt);
  border-bottom: 1px solid var(--border);
}
.product-lines-icon {
  width: 34px; height: 34px;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--pl-accent, var(--text-3)) 14%, var(--bg));
  border: 1px solid color-mix(in srgb, var(--pl-accent, var(--border)) 28%, var(--border));
  color: var(--pl-accent, var(--text-2));
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 600;
  flex-shrink: 0;
  letter-spacing: 0.02em;
  margin-top: 1px;
}
.product-lines-icon--service {
  background: var(--bg-sunken);
  border-color: var(--border);
  color: var(--text-2);
}
.product-lines-head-meta { min-width: 0; flex: 1; }
.product-lines-name {
  font-size: 14.5px; font-weight: 600; color: var(--text);
  letter-spacing: -0.005em; line-height: 1.25;
}
.product-lines-desc-wrap { margin-top: 4px; max-width: 60ch; }
.product-lines-desc {
  font-size: 12.5px;
  color: var(--text-2);
  line-height: 1.45;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.product-lines-desc-wrap.is-expanded .product-lines-desc {
  -webkit-line-clamp: unset;
  overflow: visible;
}
.product-lines-desc-toggle {
  display: none;
  margin-top: 4px;
  padding: 0;
  background: transparent;
  border: 0;
  font-size: 11.5px;
  font-weight: 500;
  color: var(--pl-accent, var(--primary));
  cursor: pointer;
  align-items: center;
  gap: 3px;
  letter-spacing: -0.005em;
}
.product-lines-desc-toggle:hover { text-decoration: underline; }
.product-lines-desc-wrap.is-overflowing > .product-lines-desc-toggle { display: inline-flex; }
.product-lines-meta {
  margin-top: 6px;
  display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap;
  font-size: 11.5px; color: var(--text-3);
}
.product-lines-cat-chip {
  display: inline-flex; align-items: center;
  padding: 1px 7px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  font-size: 11px; color: var(--text-2); font-weight: 500;
}
.product-lines-rm {
  width: 30px; height: 30px;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; border: 0; color: var(--text-3);
  border-radius: var(--radius);
  cursor: pointer;
}
.product-lines-rm:hover { background: var(--danger-bg); color: var(--danger); }

.product-lines-body { padding: 0 14px; }
.product-lines-table {
  display: grid;
  gap: 0 14px;
  align-items: center;
}
/* Quote (catalog-only) — 6 columns */
.product-lines-table--catalog {
  grid-template-columns:
    minmax(64px, max-content)   /* code */
    minmax(0, 1fr)              /* name + unit */
    76px                        /* qty */
    82px                        /* list price */
    92px                        /* subtotal */
    28px;                       /* remove */
}
/* Invoice catalog — 8 columns (adds discount + tax) */
.product-lines-table--catalog.product-lines-table--invoice {
  grid-template-columns:
    minmax(64px, max-content)   /* code */
    minmax(0, 1fr)              /* name + unit */
    72px                        /* qty */
    96px                        /* unit price */
    96px                        /* discount */
    52px                        /* tax */
    96px                        /* subtotal */
    28px;                       /* remove */
}
/* Service — 8 columns; description occupies the wide slot */
.product-lines-table--service {
  grid-template-columns:
    minmax(0, 2fr)              /* description */
    minmax(72px, 96px)          /* unit */
    72px                        /* qty */
    96px                        /* unit price */
    96px                        /* discount */
    52px                        /* tax */
    96px                        /* subtotal */
    28px;                       /* remove */
}
.product-lines-thead, .product-lines-row { display: contents; }
.product-lines-thead > * {
  font-size: 10.5px; font-weight: 600; color: var(--text-3);
  text-transform: uppercase; letter-spacing: 0.06em;
  padding: 10px 0 8px;
  border-bottom: 1px solid var(--border);
}
.product-lines-thead .ralign { text-align: right; }
.product-lines-row > * { padding: 10px 0; border-bottom: 1px solid var(--border); }
.product-lines-row:last-of-type > * { border-bottom: 0; }

.product-lines-code {
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--text-2);
  background: var(--bg-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 3px 7px;
  display: inline-block;
  max-width: 160px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  letter-spacing: -0.005em;
}
.product-lines-name-col   { min-width: 0; }
.product-lines-li-name    {
  font-size: 13.5px; color: var(--text); font-weight: 500; line-height: 1.35;
}
.product-lines-li-unit    {
  font-size: 11px; color: var(--text-3);
  margin-top: 2px;
  font-variant-numeric: tabular-nums;
}
.product-lines-qty        { display: flex; }
.product-lines-qty input  { width: 100%; height: 30px; text-align: right; }
.product-lines-price, .product-lines-subtotal {
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-size: 13px;
}
.product-lines-price    { color: var(--text-2); }
.product-lines-subtotal { color: var(--text); font-weight: 600; }
.product-lines-tax      { display: flex; align-items: center; justify-content: center; }
/* Inputs inside the grid: force them to fill their grid cell and to
   accept min-width: 0 so the grid template (e.g. 96px unit-price
   column) wins over the input's browser-default min-content width.
   Without this, type=number inputs impose ~150px each, the 1fr Item
   column gets squeezed, and the grid's columns drift away from the
   thead's columns. */
.product-lines-table input.input {
  width: 100%;
  min-width: 0;
  height: 30px;
  box-sizing: border-box;
}

/* ------------------------------------------------------------------
 * Two-row item block — used by every line item in BOTH modes
 * (quote + invoice). Row 1 = code + name (or description input
 * for service) + remove. Row 2 = labeled controls that wrap on
 * narrow viewports. The control set is mode-aware (quote shows
 * fewer controls); see renderItemBlock() in product-lines.js.
 * ------------------------------------------------------------------ */
.product-lines-itemblock {
  display: flex; flex-direction: column;
  padding: 14px 0;
  gap: 12px;
  border-bottom: 1px solid var(--border);
}
.product-lines-itemblock:last-child { border-bottom: 0; }

/* Head row: catalog row has [code] [name+unit] [×]; service row has
   [description-input] [×]. */
.product-lines-itemblock-head {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr) 28px;
  gap: 12px;
  align-items: start;
}
.product-lines-itemblock-head--service {
  grid-template-columns: minmax(0, 1fr) 28px;
}
.product-lines-itemblock-head .product-lines-code {
  align-self: start;
  margin-top: 2px;
}
.product-lines-itemblock-name {
  min-width: 0;
}
.product-lines-itemblock-name-input {
  width: 100%;
  height: 32px;
  font-size: 13.5px;
  font-weight: 500;
}

/* Controls row: labeled fields that wrap. Subtotal floats to the
   right via margin-left: auto. */
.product-lines-itemblock-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 12px 16px;
  align-items: flex-end;
}
.product-lines-control {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}
.product-lines-control-label {
  font-size: 10.5px;
  font-weight: 600;
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  line-height: 1;
}
.product-lines-control-static {
  font-size: 13px;
  height: 30px;
  display: inline-flex;
  align-items: center;
  color: var(--text);
}
.product-lines-control input.input {
  width: 100%;
  min-width: 0;
  height: 30px;
  box-sizing: border-box;
}
.product-lines-control--qty   { width: 78px; }
.product-lines-control--unit  { width: 88px; }
.product-lines-control--price { width: 110px; }
.product-lines-control--disc  { width: 110px; }
.product-lines-control--tax   { width: 140px; }

/* Tax cluster: checkbox + rate input + "%" suffix on one baseline. */
.product-lines-tax-cluster {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 30px;
}
.product-lines-tax-cluster .check { margin: 0; flex-shrink: 0; }
.product-lines-tax-rate {
  width: 56px;
  height: 30px;
  text-align: right;
  padding-right: 6px;
}
.product-lines-control-suffix {
  font-size: 12px;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}

/* Subtotal sits at the far right of the controls row, regardless of
   how many controls preceded it. Aligned to its label baseline by
   `align-items: flex-end` on the parent. */
.product-lines-control--subtotal {
  margin-left: auto;
  text-align: right;
  align-items: flex-end;
}
.product-lines-control--subtotal .product-lines-subtotal {
  font-size: 14px;
  font-weight: 600;
  height: 30px;
  display: inline-flex;
  align-items: center;
  color: var(--text);
}
.product-lines-table input[type="number"] { text-align: right; }
/* Service description / unit cells use text inputs, not numeric — keep
   the left alignment but inherit the width-fill rule above. */
.product-lines-table--service input[data-pl-field="name"],
.product-lines-table--service input[data-pl-field="unit"] {
  text-align: left;
}
.product-lines-rm-row {
  width: 28px; height: 28px;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; border: 0; color: var(--text-3);
  border-radius: var(--radius);
  cursor: pointer;
  justify-self: end;
}
.product-lines-rm-row:hover { background: var(--danger-bg); color: var(--danger); }

.product-lines-add-line {
  width: 100%;
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  padding: 11px 12px;
  background: transparent;
  border: 0; border-top: 1px dashed var(--border);
  color: var(--text-2);
  font-size: 12.5px; font-weight: 500;
  cursor: pointer;
}
.product-lines-add-line:hover { background: var(--bg-alt); color: var(--text); }
.product-lines-add-line[disabled] { opacity: 0.4; cursor: not-allowed; }
.product-lines-add-line svg { color: var(--text-3); }

.product-lines-add-product,
.product-lines-add-service {
  width: 100%;
  display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  padding: 16px;
  background: var(--bg);
  border: 1.5px dashed var(--border-strong);
  border-radius: var(--radius-lg);
  color: var(--text-2);
  font-size: 14px; font-weight: 500;
  cursor: pointer;
  transition: border-color var(--dur-1), color var(--dur-1), background var(--dur-1);
}
.product-lines-add-product:hover,
.product-lines-add-service:hover {
  border-color: var(--primary);
  color: var(--primary);
  background: var(--primary-50);
}
.product-lines-add-product[disabled],
.product-lines-add-service[disabled] {
  opacity: 0.45; cursor: not-allowed; pointer-events: none;
}
.product-lines-add-product svg,
.product-lines-add-service svg { color: currentColor; }

.product-lines-empty {
  text-align: center;
  padding: 40px 16px;
  background: var(--bg-alt);
  border: 1px dashed var(--border);
  border-radius: var(--radius-lg);
  color: var(--text-3);
  font-size: 13.5px;
}
.product-lines-empty strong {
  display: block; margin-bottom: 4px;
  font-size: 14.5px; color: var(--text-2); font-weight: 600;
}

/* Picker trigger — visually a "select-like" button. Replaces a native
   <select> when the chooser needs richer rows than a flat option list
   (logo + multi-line sub-text + filter). Click → opens showPicker()
   from assets/js/ui/picker.js. The current selection renders inline
   (icon tile + name); empty state shows a placeholder string. */
.picker-trigger {
  width: 100%;
  display: flex; align-items: center; gap: 10px;
  padding: 6px 10px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: 13px;
  text-align: left;
  cursor: pointer;
  min-height: 38px;
  transition: border-color var(--dur-1), background var(--dur-1);
}
.picker-trigger:hover  { border-color: var(--border-strong); }
.picker-trigger:focus  { outline: none; border-color: var(--primary); box-shadow: var(--ring-focus); }
.picker-trigger[disabled],
.picker-trigger.is-disabled { opacity: 0.6; cursor: not-allowed; pointer-events: none; }
.picker-trigger.is-invalid { border-color: var(--danger); }
.picker-trigger-icon { display: inline-flex; flex-shrink: 0; }
.picker-trigger-icon:empty { display: none; }
.picker-trigger-label {
  flex: 1; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  color: var(--text);
}
.picker-trigger-label.is-placeholder { color: var(--text-3); font-weight: 400; }
.picker-trigger-chev {
  flex-shrink: 0;
  color: var(--text-3);
  font-size: 11px;
}

/* Picker modal — list of pickable products / line items */
.product-lines-pick-message { margin-bottom: 10px; }
/* Search input fills the modal body width (overrides the 240px default
   width on .input-search used by list-page toolbars). */
.product-lines-pick-search {
  width: 100%;
  margin-bottom: 8px;
}
/* Category-pill filter row — only present when the picker has 2+
   distinct categories to choose between. Active pill takes the
   primary tint so the current scope is unambiguous. */
.product-lines-pick-filters {
  display: flex; flex-wrap: wrap; gap: 6px;
  margin-bottom: 10px;
}
.product-lines-pick-pill {
  display: inline-flex; align-items: center;
  padding: 4px 10px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  font-size: 12px; font-weight: 500;
  color: var(--text-2);
  cursor: pointer;
  transition: border-color var(--dur-1), background var(--dur-1), color var(--dur-1);
}
.product-lines-pick-pill:hover {
  border-color: var(--primary);
  color: var(--primary);
}
.product-lines-pick-pill.is-active {
  background: var(--primary);
  border-color: var(--primary);
  color: var(--on-primary, #fff);
}
.product-lines-pick-empty {
  padding: 24px 8px;
  text-align: center;
  color: var(--text-3);
  font-size: 13px;
}

/* "+ Create new …" sticky row above the list. Visually distinct from the
   regular picker rows (dashed border + accent text) so it reads as a
   construct, not "the first option in the list". Stays visible regardless
   of search/filter state — it's an action, not data. */
.picker-create-row {
  width: 100%;
  display: flex; align-items: center; gap: 10px;
  padding: 9px 12px;
  margin-bottom: 8px;
  background: var(--bg);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  color: var(--primary);
  font-size: 13px;
  font-weight: 500;
  text-align: left;
  cursor: pointer;
  transition: border-color var(--dur-1), background var(--dur-1);
}
.picker-create-row:hover {
  border-color: var(--primary);
  background: var(--primary-50);
}
.picker-create-icon {
  width: 22px; height: 22px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: var(--primary-50);
  color: var(--primary);
  font-size: 14px; font-weight: 700;
  flex-shrink: 0;
}
.picker-create-label { flex: 1; min-width: 0; }
.product-lines-pick-list {
  display: flex; flex-direction: column; gap: 4px;
  margin-top: 0;
  max-height: 50vh;
  overflow-y: auto;
  /* Trim the right padding so the scrollbar doesn't clip pill borders. */
  padding-right: 2px;
}
.product-lines-pick-row {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 12px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  text-align: left;
  cursor: pointer;
  transition: border-color var(--dur-1), background var(--dur-1);
  width: 100%;
}
.product-lines-pick-row:hover {
  border-color: var(--primary);
  background: var(--primary-50);
}
.product-lines-pick-row .product-lines-icon {
  width: 28px; height: 28px;
  font-size: 11px;
  align-self: flex-start;
  margin-top: 2px;
}
.product-lines-pick-meta { min-width: 0; flex: 1; }
.product-lines-pick-name { font-size: 13.5px; font-weight: 600; color: var(--text); }
.product-lines-pick-desc {
  font-size: 12px; color: var(--text-2);
  line-height: 1.45;
  margin-top: 3px;
  max-width: 60ch;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.product-lines-pick-sub  {
  font-size: 11.5px; color: var(--text-3);
  margin-top: 4px;
  display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
}
.product-lines-pick-price {
  font-size: 12.5px; color: var(--text-2);
  font-variant-numeric: tabular-nums; font-weight: 500;
  flex-shrink: 0;
}
.product-lines-pick-li .product-lines-pick-code {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 600;
  color: var(--text-2);
  background: var(--bg-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 6px 8px;
  width: 110px;
  flex-shrink: 0;
  text-align: center;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  letter-spacing: -0.005em;
}
.product-lines-pick-li:hover .product-lines-pick-code {
  background: var(--bg);
  border-color: color-mix(in srgb, var(--primary) 25%, var(--border));
}

/* Compact view: collapse the catalog grid into 3 logical columns
   on narrow viewports. The invoice + service variants keep horizontal
   scroll instead — too many editable inputs to flatten cleanly. */
@media (max-width: 880px) {
  .product-lines-table--catalog:not(.product-lines-table--invoice) {
    grid-template-columns: 1fr 1fr 32px;
    row-gap: 6px;
  }
  .product-lines-table--catalog:not(.product-lines-table--invoice) .product-lines-thead { display: none; }
  .product-lines-table--catalog:not(.product-lines-table--invoice) .product-lines-row > * { padding: 4px 0; border-bottom: 0; }
  .product-lines-table--catalog:not(.product-lines-table--invoice) .product-lines-row > *:nth-child(1) { grid-column: 1 / -1; padding-top: 12px; }
  .product-lines-table--catalog:not(.product-lines-table--invoice) .product-lines-row > *:nth-child(2) { grid-column: 1 / 4; }
  .product-lines-table--catalog:not(.product-lines-table--invoice) .product-lines-row > *:nth-child(3) { grid-column: 1 / 2; padding-bottom: 12px; }
  .product-lines-table--catalog:not(.product-lines-table--invoice) .product-lines-row > *:nth-child(4) { display: none; }
  .product-lines-table--catalog:not(.product-lines-table--invoice) .product-lines-row > *:nth-child(5) { grid-column: 2 / 3; text-align: right; padding-bottom: 12px; }
  .product-lines-table--catalog:not(.product-lines-table--invoice) .product-lines-row > *:nth-child(6) { grid-column: 3 / 4; padding-bottom: 12px; }
  .product-lines-table--catalog:not(.product-lines-table--invoice) .product-lines-row + .product-lines-row > *:nth-child(1) {
    border-top: 1px solid var(--border); margin-top: 4px;
  }
}

/* ============================================================
 * Filter modal + chip strip (filter-modal.js)
 * Spec: docs/superpowers/specs/2026-05-10-filter-modal-design.md
 * ============================================================ */

/* Toolbar trigger badge */
.toolbar .badge[data-filter-count] {
  margin-left: 6px;
  padding: 1px 6px;
  font-size: 11px;
  font-weight: 600;
  border-radius: var(--radius-lg);
  background: var(--info-bg);
  color: var(--info);
  border: 1px solid var(--border);
}

/* Chip strip */
.filter-chips {
  display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
  margin: 8px 0 12px;
}
/* Applied-filter chip — uses brand primary so the chip strip picks up
   the operator's chosen theme (was --info, which stayed blue regardless
   of brand color). "Currently applied" is an active state, which is
   primary territory per Step 4 Rule 6. Updated v1.46.2. */
.filter-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 4px 3px 10px;
  font-size: 12px;
  background: var(--primary-50);
  color: var(--primary-700);
  border: 1px solid color-mix(in srgb, var(--primary) 22%, transparent);
  border-radius: var(--radius-lg);
  max-width: 320px;
  min-width: 0;
}
.filter-chip-label { font-weight: 600; }
.filter-chip-sep   { color: var(--text-3); }
.filter-chip-value {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  display: inline-block; min-width: 0; max-width: 220px;
}
.filter-chip-x {
  border: 0; background: transparent; cursor: pointer;
  width: 18px; height: 18px; border-radius: 50%;
  font-size: 14px; line-height: 1;
  color: var(--primary-700); display: inline-flex; align-items: center; justify-content: center;
}
.filter-chip-x:hover { background: color-mix(in srgb, var(--primary) 14%, transparent); }
.filter-chip-clear {
  border: 0; background: transparent; cursor: pointer;
  font-size: 12px; color: var(--text-2); padding: 2px 6px;
  text-decoration: underline;
}
.filter-chip-clear:hover { color: var(--text); }

/* Filter-chip TOGGLE group — radio-style segmented filter (exactly one
   chip active at a time). Distinct from the `.filter-chips` strip
   above (plural parent), which is the v1.39 dismissible applied-filter
   pink chip. We scope to `.filter-chip-group` (singular) so the two
   patterns coexist without collision. team.html previously carried this
   as page-scoped CSS; v1.69+ shares the styles here for any list page
   that needs a multi-state filter toggle. */
.filter-chip-group {
  display: inline-flex; gap: 4px;
  background: var(--bg-alt); border: 1px solid var(--border);
  border-radius: var(--radius-pill); padding: 3px;
}
.filter-chip-group .filter-chip {
  appearance: none; border: 0; background: transparent;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 12px; border-radius: var(--radius-pill);
  font-size: 12px; font-weight: 500; color: var(--text-3);
  font-family: inherit; cursor: pointer; min-width: 0; max-width: none;
  transition: background var(--dur-1), color var(--dur-1), box-shadow var(--dur-1);
}
.filter-chip-group .filter-chip:hover { color: var(--text-2); }
.filter-chip-group .filter-chip.is-active {
  background: var(--bg); color: var(--text);
  box-shadow: var(--shadow-card);
}

/* Filter modal panel — overrides default modal width */
.filter-modal-panel {
  width: 720px; max-width: calc(100vw - 32px);
}
.filter-modal-body {
  display: grid;
  grid-template-columns: 170px 1fr;
  min-height: 360px; max-height: 60vh; overflow: hidden;
}
.filter-modal-sidebar {
  list-style: none; margin: 0; padding: 8px 0;
  border-right: 1px solid var(--border);
  background: var(--bg-sunken);
  overflow-y: auto;
}
.filter-modal-cat {
  display: flex; align-items: center; justify-content: space-between;
  padding: 8px 14px; cursor: pointer;
  font-size: 13px; color: var(--text-2);
  border-left: 2px solid transparent;
}
.filter-modal-cat:hover { background: var(--bg-alt); }
.filter-modal-cat.is-active {
  background: var(--bg);
  color: var(--text);
  border-left-color: var(--text);
  font-weight: 600;
}
.filter-modal-cat-badge {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 20px; height: 18px; padding: 0 6px;
  font-size: 11px; font-weight: 600;
  background: var(--info-bg); color: var(--info);
  border-radius: var(--radius-lg);
}
.filter-modal-detail {
  padding: 16px 20px;
  overflow-y: auto;
}
.filter-modal-cat-head h3 {
  margin: 0 0 12px;
  font-size: 14px;
  color: var(--text);
}
.filter-modal-count {
  font-size: 12px; font-weight: 400; color: var(--text-3);
  margin-left: 6px;
}

/* Editors */
.fm-checkboxes { display: flex; flex-direction: column; gap: 6px; }
.fm-checkbox {
  display: inline-flex; align-items: center; gap: 8px;
  cursor: pointer; font-size: 13px;
}
.fm-segs {
  display: flex; flex-wrap: wrap; gap: 6px;
}
.fm-seg, .fm-preset {
  border: 1px solid var(--border); background: var(--bg);
  color: var(--text-2);
  padding: 4px 10px;
  font-size: 12px;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.fm-seg:hover, .fm-preset:hover { background: var(--bg-alt); }
.fm-seg.is-active, .fm-preset.is-active {
  background: var(--text); color: var(--bg);
  border-color: var(--text);
}
.fm-date-presets { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
.fm-date-custom, .fm-amount {
  display: flex; gap: 8px; align-items: end;
}
.fm-field {
  display: flex; flex-direction: column; gap: 4px;
  font-size: 12px; color: var(--text-2);
}
.fm-field .input {
  min-width: 140px;
}
.fm-error {
  margin-top: 8px;
  font-size: 12px; color: var(--danger);
}
.fm-picker {
  margin-bottom: 8px;
}

/* Mobile (≤720px) — modal goes single-column, sidebar collapses to top tabs */
@media (max-width: 720px) {
  .filter-modal-panel { width: 100%; max-width: 100%; }
  .filter-modal-body  { grid-template-columns: 1fr; }
  .filter-modal-sidebar {
    display: flex; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--border);
    padding: 4px;
  }
  .filter-modal-cat {
    border-left: 0; border-bottom: 2px solid transparent;
    white-space: nowrap;
  }
  .filter-modal-cat.is-active {
    border-left: 0; border-bottom-color: var(--text);
  }
}

/* ============================================================
 * Full-page edit form — view-mode chrome strip
 *
 * Pattern: wrap the form area in <section class="editable" data-editable="<id>">,
 * use the Editable primitive (assets/js/ui/editable.js). When NOT
 * editing, .section-card form inputs lose their visible chrome and
 * read as plain values. Click "Edit <entity>" → primitive adds
 * `.is-editing` → inputs revert to normal chrome and become interactive.
 *
 * Used by: invoice.html (v1.39.3). Same pattern is the canonical
 * approach for any other full-page edit form going forward — see
 * docs/ui-design-guidelines.md Step 5.
 * ============================================================ */

.editable:not(.is-editing) .section-card .field input:not([type=checkbox]):not([type=radio]):not([type=button]):not([type=submit]),
.editable:not(.is-editing) .section-card .field select,
.editable:not(.is-editing) .section-card .field textarea {
  pointer-events: none;
  background: transparent;
  border-color: transparent;
  padding-left: 0;
  padding-right: 0;
  box-shadow: none;
  /* Promote the value text so it reads as the primary content of the
     row, distinct from the demoted label above it. */
  font-size: 14.5px;
  font-weight: 500;
  color: var(--text);
}
.editable:not(.is-editing) .section-card .field select {
  background-image: none;
  appearance: none;
  -webkit-appearance: none;
}
.editable:not(.is-editing) .section-card .field .picker-trigger {
  pointer-events: none;
  background: transparent;
  border-color: transparent;
  padding-left: 0;
  padding-right: 0;
}
.editable:not(.is-editing) .section-card .field .picker-trigger-chev,
.editable:not(.is-editing) .section-card .field-help {
  display: none;
}
/* Demote the label so heading > value > caption hierarchy is restored.
   Without this, label and value render at the same visual weight and
   the form looks like a list of identical phrases. */
.editable:not(.is-editing) .section-card .field-label {
  font-size: 11px;
  font-weight: 500;
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-bottom: 2px;
}
/* The required asterisk loses meaning in view mode — strip it. */
.editable:not(.is-editing) .section-card .field-label .text-danger {
  display: none;
}
/* Empty inputs in view mode: hide the input shell and show
   "— Not set" via a pseudo on the .field so empty rows have
   visible content instead of looking like just-a-label. */
.editable:not(.is-editing) .section-card .field:has(input:placeholder-shown) .input,
.editable:not(.is-editing) .section-card .field:has(textarea:placeholder-shown) .input {
  display: none;
}
.editable:not(.is-editing) .section-card .field:has(input:placeholder-shown)::after,
.editable:not(.is-editing) .section-card .field:has(textarea:placeholder-shown)::after {
  content: "— Not set";
  color: var(--text-4);
  font-size: 14px;
  font-style: italic;
}

/* Top accent stripe — view mode = info blue, edit mode = brand color.
   At-a-glance mode indicator without needing to read the pill. */
.editable {
  position: relative;
  padding-top: 6px;
}
.editable::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 3px;
  background: var(--info);
  border-radius: var(--radius-sm);
  transition: background var(--dur-1);
}
.editable.is-editing::before {
  background: var(--primary);
}

/* Suppress the .editable::before line when a canonical .workbar with the
 * .has-mode-indicator opt-in is its same-parent sibling above — that
 * workbar's own ::after already paints the mode-indicator (and recolors
 * on .is-edit-mode), so .editable::before would draw a redundant
 * second line directly below it. Scoped via the existing
 * .has-mode-indicator signal (NOT a new flag) so the 5 .editable
 * consumers that have no workbar indicator (content, catalog, email-
 * templates, profile, theme-studio) are unaffected — their ::before
 * keeps painting and keeps its info → primary edit-mode flip. */
.workbar.has-mode-indicator ~ .editable::before {
  display: none;
}

/* Mode pill in the page-head: paired view/edit pills, only one visible. */
.mode-pill {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 10px;
  font-size: 11px; font-weight: 600;
  border-radius: var(--radius-lg);
  letter-spacing: 0.02em;
}
.mode-pill::before {
  content: "•";
  font-size: 14px; line-height: 1;
}
.mode-pill-view {
  background: var(--info-bg);
  color: var(--info);
}
.mode-pill-edit {
  background: color-mix(in srgb, var(--primary) 18%, var(--bg));
  color: var(--primary);
}

/* Sticky save-bar — canonical bottom action strip for full-page edit
   forms (invoice.html v1.39.6+, settings.html). Lives inside the
   .editable section so .is-editing toggles which children show.
   Carries: state pill ("Editing") + read-hint OR pending-msg + Edit
   button (view) or Cancel + Save (edit). */
.editable .save-bar {
  position: sticky;
  bottom: 0;
  z-index: 2;
  margin-top: 24px;
  background: var(--bg-alt);
  border-top: 1px solid var(--border);
  padding: 12px 20px;
  display: flex;
  align-items: center;
  gap: 12px;
  box-shadow: 0 -4px 12px rgba(15,23,42,0.04);
  border-radius: var(--radius-lg);
}
.editable .save-bar-readhint {
  font-size: 13px;
  color: var(--text-3);
  flex: 1;
}
.editable .save-bar-readhint strong { color: var(--text-2); font-weight: 600; }
.editable .save-bar-msg {
  font-size: 13px;
  color: var(--text-2);
  flex: 1;
  display: none;
}
.editable.is-editing .save-bar-readhint { display: none; }
.editable.is-editing .save-bar-msg { display: block; }

/* In view mode, replace the customer_mode radio buttons with the
   selected option rendered as a pill. The unselected radio + helper
   text collapse out of the layout. */
.editable:not(.is-editing) .section-card .ad-hoc-row > span,
.editable:not(.is-editing) .section-card .ad-hoc-row label:not(:has(input:checked)),
.editable:not(.is-editing) .section-card .ad-hoc-row label input[type="radio"] {
  display: none;
}
.editable:not(.is-editing) .section-card .ad-hoc-row {
  background: transparent;
  border: 0;
  padding: 0;
}
.editable:not(.is-editing) .section-card .ad-hoc-row label:has(input:checked) {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 11px;
  font-size: 12.5px; font-weight: 500;
  background: var(--info-bg);
  color: var(--info);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}
.editable:not(.is-editing) .section-card .ad-hoc-row label:has(input:checked)::before {
  content: "•";
  font-size: 14px; line-height: 1;
}

/* ============================================================
   Resource Library picker (assets/js/ui/resource-library-picker.js)
   Modal-shaped picker over the unified file_assets store
   (migration 039). Tab bar across the top groups items by where
   they live (Product files / Article images / Links).
   ============================================================ */
.rl-tabs {
  display: flex; gap: 4px;
  padding: 12px 18px 0;
  border-bottom: 1px solid var(--border);
  overflow-x: auto;
}
.rl-tab {
  appearance: none; background: transparent; border: 0;
  padding: 8px 12px; margin-bottom: -1px;
  font: inherit; color: var(--text-2);
  border-bottom: 2px solid transparent;
  cursor: pointer; white-space: nowrap;
  transition: color var(--dur-1), border-color var(--dur-1);
}
.rl-tab:hover { color: var(--text); }
.rl-tab.is-active {
  color: var(--primary);
  border-bottom-color: var(--primary);
  font-weight: 600;
}
.rl-toolbar { padding: 12px 18px; }
.rl-list {
  padding: 0 8px 8px;
  max-height: 50vh;
  overflow-y: auto;
}
.rl-row {
  display: flex; align-items: center; gap: 12px;
  width: 100%;
  padding: 10px 12px;
  margin: 2px 0;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: transparent;
  cursor: pointer; text-align: left;
  transition: background var(--dur-1), border-color var(--dur-1);
}
.rl-row:hover { background: var(--bg-alt); }
.rl-row.is-selected { background: var(--primary-50); border-color: var(--primary-100); }
.rl-meta { flex: 1; min-width: 0; }
.rl-label {
  font-weight: 500; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rl-sub {
  font-size: 12px; color: var(--text-3);
  margin-top: 2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rl-empty {
  padding: 32px 16px; text-align: center; color: var(--text-3);
}

/* ============================================================
 * Article editor — view-mode lock extension for EasyMDE (v1.46.0).
 *
 * The base view-mode lock lives in pages/content.html — it targets
 * <input>, <select>, <textarea> directly. EasyMDE replaces the
 * textarea with a CodeMirror-managed div, so the base lock doesn't
 * reach it. These rules extend the lock so EasyMDE behaves the same.
 * ============================================================ */
#articleEditable:not(.is-editing) .CodeMirror {
  pointer-events: none;
  background: transparent;
  border-color: transparent;
  box-shadow: none;
}
#articleEditable:not(.is-editing) .editor-toolbar {
  display: none;
}
#articleEditable:not(.is-editing) .CodeMirror-cursor {
  display: none !important;
}

/* ============================================================
 * Lifecycle timeline — read-only state-transition history in
 * the article editor (v1.46.0). Lightweight; not the full
 * audit-trail timeline primitive.
 * ============================================================ */
.lifecycle-timeline {
  padding: 4px 0;
  font-size: 13px;
}
.lifecycle-timeline .lt-row {
  display: grid;
  grid-template-columns: 14px 1fr auto;
  gap: 10px;
  align-items: center;
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
}
.lifecycle-timeline .lt-row:last-child { border-bottom: 0; }
.lifecycle-timeline .lt-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--text-3);
  margin: 0 auto;
}
.lifecycle-timeline .lt-dot.is-draft     { background: var(--text-3); }
.lifecycle-timeline .lt-dot.is-scheduled { background: var(--warning); }
.lifecycle-timeline .lt-dot.is-published { background: var(--success); }
.lifecycle-timeline .lt-dot.is-archived  { background: var(--text-3); opacity: 0.6; }
.lifecycle-timeline .lt-meta {
  color: var(--text-2);
  min-width: 0;
}
.lifecycle-timeline .lt-meta strong {
  color: var(--text);
  font-weight: 600;
}
.lifecycle-timeline .lt-when {
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  font-size: 12px;
  white-space: nowrap;
}
.lifecycle-timeline .lt-empty {
  color: var(--text-3);
  font-style: italic;
  padding: 8px 0;
}

/* ============================================================
   .btn-split — canonical split-button pill (v1.67.0).
   Single brand-primary fill with a 1px hairline separator between
   the main action and the caret. Use on any Edit / Publish / Issue /
   Send button that wants a secondary-actions dropdown.
   Markup:
     <div class="btn-split">
       <button class="btn-split-main"  type="button">…Edit</button>
       <button class="btn-split-caret" type="button" aria-label="More">▾</button>
     </div>
   The caret is functional but optional — pages can hide it via
   `.btn-split--solo` when there's no secondary menu.
   ============================================================ */
.btn-split {
  display: inline-flex;
  align-items: stretch;
  height: 34px;
  border-radius: var(--radius);
  background: var(--primary);
  color: var(--text-on-primary, #fff);
  overflow: hidden;
  cursor: pointer;
}
.btn-split:hover { background: color-mix(in srgb, var(--primary) 88%, #000); }
.btn-split-main,
.btn-split-caret {
  appearance: none;
  background: transparent;
  border: 0;
  color: inherit;
  font: 500 13px/1 var(--font-sans);
  cursor: pointer;
  padding: 0 12px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  box-shadow: none;
}
.btn-split-main { padding: 0 12px 0 14px; }
.btn-split-main svg { width: 14px; height: 14px; color: currentColor; }
.btn-split-caret {
  border-left: 1px solid rgba(255, 255, 255, 0.22);
  padding: 0 10px;
  font-size: 20px;
  line-height: 1;
}
.btn-split--solo .btn-split-caret { display: none; }
