/* ============================================================
 * 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-secondary:active { background: var(--bg-alt); border-color: var(--border-strong); }

.btn-ghost    { background: transparent; border-color: transparent; color: var(--text-2); }
.btn-ghost:hover { background: var(--bg-alt); color: var(--text); }
.btn-ghost:active { 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-danger:active { background: var(--danger-bg); border-color: var(--danger); color: var(--danger); }

/* Canonical "pressed" tactile feedback — translateY(1px) applies to every
 * .btn variant and every .icon-btn. Combined with the per-variant darker-
 * tone backgrounds above, this gives consistent press feedback across the
 * entire portal: a visible nudge down + (where applicable) a darker tint.
 * Adds the transform to the existing background-transition so the press
 * settles in animated, not snapped. */
.btn { transition: background var(--dur-1), border-color var(--dur-1), color var(--dur-1), transform var(--dur-1); }
.btn:active { transform: translateY(1px); }

.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), transform var(--dur-1);
}
.icon-btn:hover { background: var(--bg-alt); border-color: var(--border); color: var(--text); }
.icon-btn:active { background: var(--bg-alt); border-color: var(--border-strong); color: var(--text); transform: translateY(1px); }

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

/* Inter-field rhythm — Step 7 spacing rule (docs/ui-design-guidelines.md):
 * inter-group spacing ≥ 2× intra-group spacing. The .field's internal
 * flex gap is 6px (caption → value → hint). Inter-field spacing is
 * 16px = ~2.7× the intra-field gap, so a caption groups visibly with
 * ITS value, not with the value above. Applies to consecutive
 * .field / .field-row / .field-row-3 siblings inside any of the
 * canonical field-stacking containers (.section-card, .tray-sub,
 * .panel-body). Covers both view mode and edit mode — proximity
 * grouping is layout-truth, not mode-specific. */
.section-card > :is(.field, .field-row, .field-row-3) + :is(.field, .field-row, .field-row-3),
.tray-sub > :is(.field, .field-row, .field-row-3) + :is(.field, .field-row, .field-row-3),
.tray-sub-body > :is(.field, .field-row, .field-row-3) + :is(.field, .field-row, .field-row-3),
.panel-body > :is(.field, .field-row, .field-row-3) + :is(.field, .field-row, .field-row-3) {
  margin-top: var(--space-4);
}

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

/* Overlapping avatar pile — used wherever multiple people are shown in a
 * compact strip (companies list AM column, drawer Account managers row,
 * company hover-card "Account team" stack). Lifted from companies.html's
 * page-scoped block on 2026-06-28. */
.am-stack { display: inline-flex; align-items: center; }
.am-stack .avatar { border: 2px solid var(--bg); margin-left: -6px; }
.am-stack .avatar:first-child { margin-left: 0; }
.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; }

/* ====================================================
 * PEOPLE HOVER CARD — canonical primitive
 *
 * Floats above a person-avatar when hovered / focused / long-pressed.
 * Wired globally by assets/js/ui/people-hover-card.js (side-effect import
 * in components.js). Pages opt in by passing `userId` to
 * renderContactAvatar(); the helper stamps data-user-id on the .avatar
 * so the module finds it. data-no-card on any ancestor opts out
 * (partner bundles set this).
 *
 * Markup the module renders (see people-hover-card.js):
 *
 *   <aside class="hover-card" role="dialog" aria-modal="false">
 *     <div class="hover-card-head">
 *       <span class="avatar avatar-xl">…</span>
 *       <div class="hover-card-id">
 *         <div class="hover-card-name">…</div>
 *         <div class="hover-card-title">…</div>
 *         <span class="hover-card-role">…</span>
 *       </div>
 *     </div>
 *     <div class="hover-card-meta">
 *       <div class="hover-card-meta-row"><span data-icon="mail"></span> …</div>
 *       <div class="hover-card-meta-row"><span data-icon="phone"></span> …</div>
 *     </div>
 *     <div class="hover-card-actions">
 *       <a class="btn btn-primary btn-sm" href="mailto:…">Email</a>
 *       <a class="icon-btn" href="tel:…" title="Call">…</a>
 *       <a class="icon-btn" href="team-member.html?id=…" title="Open profile">…</a>
 *       <button class="icon-btn" data-hc-action="copy-email" title="Copy email">…</button>
 *     </div>
 *   </aside>
 *
 * Pages CONSUME these classes. Pages do NOT redefine them — lint rule 10
 * enforces (CLAUDE.md §18). */
.hover-card {
  position: absolute;
  z-index: 9000;
  width: clamp(280px, 30vw, 320px);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-2);
  box-shadow: var(--shadow-lg);
  padding: 16px;
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity 120ms ease, transform 120ms ease;
  pointer-events: none;
}
.hover-card.is-open {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}
.hover-card:focus { outline: none; }
.hover-card:focus-visible {
  box-shadow: var(--ring-focus);
}
.hover-card-head {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
}
.hover-card-id {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  flex: 1;
}
.hover-card-name {
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
  letter-spacing: -0.01em;
  line-height: 1.25;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.hover-card-title {
  font-size: 13px;
  color: var(--text-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.hover-card-role {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  margin-top: 4px;
  padding: 1px 6px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-3);
  background: var(--bg-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.hover-card-meta {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 12px;
}
.hover-card-meta-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text-2);
  min-width: 0;
}
.hover-card-meta-row svg {
  color: var(--text-3);
  flex-shrink: 0;
}
.hover-card-meta-row a {
  color: var(--text-2);
  text-decoration: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
.hover-card-meta-row a:hover { color: var(--primary); text-decoration: underline; }
/* Inline Copy button paired with the email — sized down so it doesn't
 * dominate the row. Uses the canonical .icon-btn so it inherits :hover
 * and :active feedback from the global pressed-state rules. */
.hover-card-meta-row .icon-btn-xs {
  width: 22px; height: 22px;
  margin-left: auto;
  flex-shrink: 0;
}
.hover-card-meta-row .icon-btn-xs svg { width: 14px; height: 14px; }
.hover-card-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}
/* Hover-card primary stays content-sized — modern card UX (Linear, Stripe,
 * Notion) all use compact buttons in cards, not stretched ones. Removed
 * the flex:1 rule that previously forced full-row width. */
.hover-card-actions .icon-btn {
  width: 32px; height: 32px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text-2);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.hover-card-actions .icon-btn:hover  { background: var(--bg-alt); color: var(--text); }
.hover-card-actions .icon-btn:active { background: var(--bg-alt); color: var(--text); transform: translateY(1px); }
.hover-card-actions .btn-primary svg { margin-right: 4px; }
/* Loading + error variants render the same shell with a single body row.
   No spinner — the card opens instantly with placeholder text. */
.hover-card.is-loading .hover-card-name { color: var(--text-3); }
@media (prefers-reduced-motion: reduce) {
  .hover-card { transition: none; }
}

/* ====================================================
 * 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); }
/* Override-tracking pair — semantic states for "this record has an override
 * active" (overridden, primary tint) vs "this record is using the system
 * default" (default, neutral tint). Used by email-templates to label whether
 * a template has been customized; pattern is template-system-agnostic and
 * available for any future override-vs-default tracking surface. Documented
 * in pages/design-system.html#badges. */
.badge-overridden { background: var(--primary-50); border-color: var(--primary-100); color: var(--primary); }
.badge-default    { background: var(--bg-alt);     border-color: var(--border);        color: var(--text-3); }

/* 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); }
/* Chip tone variants — mirror the .badge-* tone set for status chips.
   Add more tones here as consumers need them. */
.chip-success { background: var(--success-bg); border-color: var(--success-border); color: var(--success); }

/* ====================================================
 * 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;
}
/* Glow variant — borderless panel carrying the soft card-glow shadow
   (same --shadow-card-glow token as .kpi tiles). For embossed card
   surfaces like the dashboard tiles. Lifted from a page-scoped
   dashboard re-skin. */
.panel--glow {
  border-color: transparent;
  box-shadow: var(--shadow-card-glow);
}
.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); }
/* Opt-in 2-col grid layout for .panel-body — children lay out in a
 * responsive 2-column grid (column-gap + row-gap 14px), collapsing
 * to a single column below 980px. Used on long settings-style pages
 * where horizontal pairing roughly halves the scroll. Documented in
 * pages/design-system.html#cards under "Panel body layouts." */
.panel-body.is-grid-2 {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: 14px;
  row-gap: 14px;
  padding: 14px 16px;
}
@media (max-width: 980px) {
  .panel-body.is-grid-2 { grid-template-columns: 1fr; }
}

/* ============================================================
 * Section-panel visual treatment — shared between Settings
 * (.s-section) and record-editor pages (.section-panel modifier
 * on .panel). Addresses Step 0.5 markers 1-4 on any full-page
 * editor without touching the bare .panel / .panel-head used by
 * lighter consumers (theme-studio, profile, dashboard, etc.).
 *
 *   M2 — drop per-field border/bg/radius on .setting-row so the
 *        panel-body content isn't nested cards inside the panel.
 *        (Scoped to .s-section since .setting-row is settings-only.)
 *   M3 — restructure .panel-head into a 4-column grid (icon |
 *        text-stack | action | chev) 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.
 *
 * Consumers:
 *   .s-section .panel        — settings page (also picks up settings-
 *                              specific layout rules: scroll-margin,
 *                              sibling margin, .is-hidden, .setting-row).
 *   .panel.section-panel     — record-editor pages (po / invoice /
 *                              quotation-edit / quote-edit / issuer-
 *                              edit / company-edit). Migrated from
 *                              the older .section-card primitive.
 *
 * Canonical reference: pages/design-system.html#section-panel.
 * ============================================================ */
:is(.s-section, .section-panel) .panel-head {
  display: grid;
  /* 4 columns: icon · text · optional action · chev. Pre-fix this was
   * `auto 1fr auto` with action + chev BOTH assigned to column 3 — grid
   * auto-flow placed the chev in row 2 (visible as a wrapped chevron
   * under the action button). Splitting them into distinct columns
   * keeps both on row 1. Column 3 collapses to 0 when no action is
   * present, so the chev still sits flush right. */
  grid-template-columns: auto 1fr auto auto;
  align-items: start;
  gap: 4px 14px;
  background: var(--bg-alt);
}
:is(.s-section, .section-panel) .panel-head > .panel-title-icon {
  grid-column: 1;
  grid-row: 1 / span 2;
  margin-top: 3px;
  color: var(--text-3);
  align-self: start;
}
:is(.s-section, .section-panel) .panel-head > .panel-head-text {
  grid-column: 2;
  min-width: 0;
}
:is(.s-section, .section-panel) .panel-head > .info-panel-chev {
  grid-column: 4;
  align-self: center;
}
/* Optional right-slot actions (links, buttons, action groups) — anything
 * that isn't the icon, text-stack, or chev — sits in column 3, just
 * before the chev. Centered vertically against the head, like the chev. */
:is(.s-section, .section-panel) .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. */
:is(.s-section, .section-panel) .panel-title {
  display: block;
  letter-spacing: -0.01em;
}

/* Sibling spacing for .section-panel (parallel to `.s-section + .s-section`
 * which lives on pages/settings.html as a scoped style). Editor pages
 * compose multiple .section-panel siblings; this gives them the same
 * 20px gutter Settings uses, without each editor page re-declaring it.
 *
 * Top-of-stack spacing for the FIRST section-panel inside an .editable
 * wrapper is provided canonically by `.editable:has(.section-panel)`'s
 * padding-top — see the `.editable` block further below. The gap lives
 * below the mode-indicator bar (.editable::before), not above the
 * first section, so the spacing reads as part of the chrome rather
 * than as a per-section margin. */
.section-panel + .section-panel { margin-top: var(--space-5); }

/* 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. */
:is(.s-section, .section-panel).is-collapsed {
  border-color: color-mix(in srgb, var(--border) 50%, transparent);
}
:is(.s-section, .section-panel).is-collapsed > .panel-head {
  background: transparent;
}
:is(.s-section, .section-panel).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);
  /* Transparent border holds 1px in the box model so :hover swapping the
   * color in (a.kpi:hover, below) doesn't trigger a layout shift. */
  border: 1px solid transparent;
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: 6px;
  box-shadow: var(--shadow-card-glow);
  transition: border-color var(--dur-1), box-shadow var(--dur-2), transform var(--dur-1);
}
/* Clickable variant — `<a class="kpi">` (used by dashboard's KPI strip
 * and any future link-tile surface). Preserves the primary-border
 * affordance on hover; halo intensifies. */
a.kpi:hover {
  border-color: var(--primary);
  box-shadow: var(--shadow-card-glow-hover);
}
a.kpi:active { transform: translateY(1px); }
.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%;
}
/* Sparkline SVG — emitted by renderSpark() in stats-utils.js. Lifted to
 * canonical (was page-scoped in dashboard.html) so every consumer — the
 * dashboard KPI strip + the System Health page — inherits one definition. */
.spark { width: 100%; height: 36px; display: block; }
.spark path.area { fill: var(--primary-50); }
.spark path.line { stroke: var(--primary); stroke-width: 1.5; fill: none; }

/* Launcher card — canonical hub-grid card (studio / website / developer /
 * catalog / reports landing grids). Markup: .launcher-grid > a.launcher-card
 * with -head / -ico / -title / -desc / -meta / -cta / -stat. Lifted from the
 * 5 page-scoped copies; pages consume, do not redefine. */
.launcher-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 16px;
  margin-top: 4px;
}
.launcher-card {
  display: flex; flex-direction: column; gap: 14px;
  padding: 22px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  text-decoration: none; color: inherit;
  transition: border-color var(--dur-1), box-shadow var(--dur-1), transform var(--dur-1);
  position: relative; overflow: hidden;
}
.launcher-card::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0;
  width: 3px; background: var(--primary-100);
  opacity: 0; transition: opacity var(--dur-1);
}
.launcher-card:hover {
  border-color: var(--primary-200, var(--primary-100));
  box-shadow: var(--shadow-card-elevated);
  transform: translateY(-1px);
}
.launcher-card:hover::before { opacity: 1; }
.launcher-card-head { display: flex; align-items: center; gap: 12px; }
.launcher-card-ico {
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; border-radius: var(--radius);
  background: var(--primary-50); color: var(--primary); flex-shrink: 0;
}
.launcher-card-title {
  font-size: 15.5px; font-weight: 600; color: var(--text);
  letter-spacing: -0.005em; line-height: 1.25;
}
.launcher-card-desc { font-size: 13.5px; line-height: 1.55; color: var(--text-3); margin: 0; }
.launcher-card-meta {
  margin-top: auto; display: flex; align-items: center; justify-content: space-between;
  padding-top: 10px; border-top: 1px solid var(--border);
  font-size: 12px; color: var(--text-3); gap: 8px;
}
.launcher-card-cta {
  display: inline-flex; align-items: center; gap: 4px;
  color: var(--primary); font-weight: 500; transition: gap var(--dur-1);
}
.launcher-card:hover .launcher-card-cta { gap: 8px; }
.launcher-card-stat strong { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }

/* Markdown / prose body — canonical typography for server-rendered markdown
 * (Release notes, internal docs, the setup-guide modal). Apply `.markdown-body`
 * to the container the HTML is injected into. Page-scoped layout (max-width)
 * stays on the page wrapper; this primitive owns typography only. Lifted from
 * the .doc-pane / .rel-doc / .markdown-body copies. */
.markdown-body h1 { font-size: 22px; font-weight: 700; margin: 0 0 14px; color: var(--text); letter-spacing: -0.01em; }
.markdown-body h2 { font-size: 17px; font-weight: 700; margin: 24px 0 8px; color: var(--text); padding-top: 18px; border-top: 1px solid var(--border); }
.markdown-body h2:first-of-type { border-top: 0; padding-top: 0; margin-top: 4px; }
.markdown-body h3 { font-size: 14.5px; font-weight: 600; margin: 18px 0 6px; color: var(--text); }
.markdown-body h4 { font-size: 13px; font-weight: 600; margin: 14px 0 4px; color: var(--text-2); }
.markdown-body p, .markdown-body li { font-size: 13.5px; line-height: 1.65; color: var(--text); margin: 0 0 10px; }
.markdown-body ul, .markdown-body ol { padding-left: 22px; margin: 0 0 12px; }
.markdown-body li { margin-bottom: 4px; }
.markdown-body strong { font-weight: 600; color: var(--text); }
.markdown-body em { font-style: italic; color: var(--text-2); }
.markdown-body a { color: var(--primary); text-decoration: underline; text-decoration-color: var(--primary-100); }
.markdown-body a:hover { text-decoration-color: var(--primary); }
.markdown-body code {
  font-family: var(--font-mono); font-size: 12px;
  padding: 1px 5px;
  background: var(--bg-alt); border: 1px solid var(--border);
  border-radius: var(--radius-sm); color: var(--text-2);
}
.markdown-body pre {
  padding: 10px 12px; background: var(--bg-sunken);
  border: 1px solid var(--border); border-radius: var(--radius);
  overflow-x: auto; margin: 0 0 14px;
}
.markdown-body pre code { background: transparent; border: 0; padding: 0; font-size: 12px; color: var(--text); }
.markdown-body blockquote {
  margin: 0 0 14px; padding: 8px 12px;
  border-left: 3px solid var(--primary);
  background: var(--bg-alt); color: var(--text-2);
  border-radius: 0 var(--radius) var(--radius) 0;
}
.markdown-body hr { border: 0; border-top: 1px solid var(--border); margin: 22px 0; }
.markdown-body table { border-collapse: collapse; margin: 0 0 18px; font-size: 13.5px; }
.markdown-body th, .markdown-body td { border: 1px solid var(--border); padding: 6px 10px; text-align: left; }
.markdown-body th { background: var(--bg-alt); font-weight: 600; }
/* Larger reading scale for full-page markdown readers (Release notes, internal
 * docs) vs the compact base used in modals. Add `is-lg` alongside markdown-body. */
.markdown-body.is-lg h1 { font-size: 30px; margin: 0 0 18px; }
.markdown-body.is-lg h2 { font-size: 22px; margin: 36px 0 10px; padding-top: 22px; }
.markdown-body.is-lg h3 { font-size: 16px; margin: 20px 0 8px; color: var(--text-2); }
.markdown-body.is-lg h4 { font-size: 13.5px; }
.markdown-body.is-lg p, .markdown-body.is-lg li { font-size: 14.5px; line-height: 1.7; margin: 0 0 12px; }
/* 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;
  /* minmax(0, …) allows the column to shrink below its content's min-content
   * size, so an oversized child (a long badge row, a wide chart) won't push
   * the column past the viewport. */
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  gap: var(--space-4);
}
@media (max-width: 1100px) {
  /* Below this width the 2fr/1fr split starves the right column. Stack. */
  .grid-2col { grid-template-columns: 1fr; }
}

/* ====================================================
 * 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);
}
/* Generic sticky-right opt-in (createDataTable column.sticky === 'right').
   Same pattern as .col-actions but for any column whose contents need to
   stay anchored to the right edge — typically a [Primary button][Kebab]
   pair on a wide list page. The page declares `width:N` to size the
   column; we don't hardcode one here. */
.table th.sticky-right,
.table td.sticky-right {
  position: sticky;
  right: 0;
  z-index: 1;
  background: var(--bg);
}
.table thead th.sticky-right {
  background: var(--bg-alt);
  z-index: 2;
}
.table tbody tr:hover td.sticky-right             { background: var(--bg-alt); }
.table tbody tr.is-selected td.sticky-right       { background: var(--primary-50); }
.table tbody tr.is-selected:hover td.sticky-right { background: #E5E3F2; }
.table-wrap .table .sticky-right {
  box-shadow: -8px 0 12px -8px rgba(15, 23, 42, 0.18);
}
.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; flex: 1; }
.drawer-name {
  font-size: var(--fs-xl);
  font-weight: 600;
  margin: 0;
  color: var(--text);
  letter-spacing: -0.01em;
  /* When the strip-actions slot is wide, the identity column shrinks. Without
   * these three rules + the parent's min-width:0, multi-word titles wrap
   * per-word (and at extreme widths, per-character via word-break:break-all).
   * Pages should also set the `title` attribute to the full text so hover
   * reveals what the ellipsis hides. Additive — no effect when the title
   * already fits. */
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.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; }

/* ============================================================
 * .tray-panel — color-fill grouped surface (no nested borders).
 *
 * Sibling to `.panel` (single-surface) and `.section-card`
 * (form-page rhythm). Distinct because the containment of its
 * sub-units comes from BACKGROUND-FILL CONTRAST instead of a
 * perimeter border:
 *
 *   .tray-panel       → outer recess, fills with --bg-sunken
 *   .tray-sub         → lifted child, fills with --bg
 *
 * In both light and dark themes the sub-panel sits one surface
 * step lighter than the tray, so it reads as "cards floating in
 * a tray" without any 1px stroke. Use this when 2-5 closely-
 * related sub-units belong together and nesting a bordered
 * .panel inside another bordered .panel would read as the 2016
 * "border in a border" admin look (Step 0.5 marker 2).
 *
 * NOT a replacement for .panel (single surface, header + body +
 * foot), .section-card (form-page section rhythm), or
 * .info-panel (drawer's small-caps key/value cluster).
 *
 * Live reference: pages/design-system.html#cards → "Tray panel".
 * ============================================================ */
.tray-panel {
  background: var(--bg-sunken);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
}
.tray-panel-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0 0 var(--space-3);
}
.tray-panel-label {
  flex: 1;
  min-width: 0;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-3);
}
.tray-panel-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.tray-panel-body.is-grid-2 {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--space-3);
  row-gap: var(--space-3);
}
@media (max-width: 720px) {
  .tray-panel-body.is-grid-2 { grid-template-columns: 1fr; }
}

.tray-sub {
  background: var(--bg);
  border-radius: var(--radius);
  padding: var(--space-3) var(--space-4);
}
.tray-sub-head {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}
.tray-sub-head > :first-child { flex: 1; min-width: 0; }
.tray-sub-head:last-child { margin-bottom: 0; }
.tray-sub-label {
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
}
.tray-sub-sub {
  margin-top: 2px;
  font-size: var(--fs-md);
  color: var(--text-3);
}
.tray-sub-body {
  color: var(--text);
  font-size: var(--fs-md);
}
.tray-sub-body > *:first-child { margin-top: 0; }
.tray-sub-body > *:last-child  { margin-bottom: 0; }

/* Page-level "Collapse all / Expand all" toggle for pages that compose
 * many `.section-card[data-collapsible]` blocks. Sits in the gap between
 * the workbar and the first section-card — explicitly NOT inside the
 * workbar (the workbar's right-side action slot is reserved for the
 * state pill / state-transition button / kebab / Edit split per the
 * canonical anatomy in design-system.html → Workbar).
 *
 * Visual treatment: muted text-link affordance, right-aligned, no
 * border, no fill — reads as a meta-control adjacent to the content
 * it governs, not as a chrome button. Hidden in edit mode via the
 * `.editable.is-editing > .collapse-all-toggle` rule (toggle must
 * live as a direct child of the `.editable` wrapper).
 *
 * Wiring: page calls `setSectionsCollapsed(scope, collapsed)` from
 * `assets/js/ui/section-collapse.js`; click handler flips the label
 * between "Collapse all" and "Expand all" and sets `aria-pressed`.
 * Page-inline JS — only a handful of consumers; behavior is
 * `addEventListener` × N, not a global module. Documented in
 * design-system.html under #workbar so any new consumer inherits the
 * same wiring. */
.collapse-all-toggle {
  display: flex;
  justify-content: flex-end;
  /* Asymmetric margins Gestalt-group the toggle with the FIRST section
   * below (6px bottom) rather than the workbar above (.editable's
   * padding-top: 18px provides the bar gap, then we add 8px more here).
   * Reads as: "this control belongs to the section stack." */
  margin: 8px 0 6px;
}
.editable.is-editing > .collapse-all-toggle { display: none; }

/* Icon slot inside the toggle button — flex container so the canonical
 * `mountCollapseAllToggle` helper can swap the SVG via innerHTML without
 * disturbing the label's position. */
.collapse-all-toggle > .btn .collapse-all-toggle-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* 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;
}
/* Actor-link inside the timeline meta line. Plain text by default; the
 * dashed underline animates in on hover/focus to advertise that this
 * element triggers the canonical hover-card. The card itself fires via
 * people-hover-card.js's generalized [data-user-id] selector — no
 * special class required on the consumer's markup. */
.timeline-meta [data-user-id] {
  cursor: pointer;
  border-bottom: 1px dashed transparent;
  transition: border-color var(--dur-1);
}
.timeline-meta [data-user-id]:hover,
.timeline-meta [data-user-id]:focus-visible {
  border-bottom-color: currentColor;
  outline: none;
}
.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; }

/* Collapsed-timeline canonical (Spec §7). Auto-wired by ui/timeline.js
 * when a .timeline has > 5 rows. Shows first 1 + last 4; the divider in
 * between expands on click/Enter. Hover brightens the affordance. */
.timeline-row.timeline-collapsed {
  cursor: pointer;
  user-select: none;
  background: transparent;
  transition: background 120ms ease;
}
.timeline-row.timeline-collapsed:hover { background: var(--bg-alt); }
.timeline-row.timeline-collapsed:focus-visible {
  outline: 0;
  box-shadow: var(--ring-focus);
}
.timeline-dot.timeline-dot-collapsed {
  border: 1px dashed var(--border);
  background: transparent;
}
.timeline-collapsed-label {
  font-size: 12px;
  color: var(--text-3);
  transition: color 120ms ease;
}
.timeline-row.timeline-collapsed:hover .timeline-collapsed-label,
.timeline-row.timeline-collapsed:focus-visible .timeline-collapsed-label {
  color: var(--text);
}
@media (prefers-reduced-motion: reduce) {
  .timeline-row.timeline-collapsed,
  .timeline-collapsed-label { transition: none; }
}

/* PO inbox — readiness banner (Spec §4 / §5). Reuses the canonical
 * .info-panel chrome; the .is-ready / .is-not-ready modifier tints the
 * border + bg with the appropriate semantic token. */
.poi-readiness-banner.is-not-ready {
  background: color-mix(in srgb, var(--warning) 12%, transparent);
  border-color: var(--warning);
}
.poi-readiness-banner.is-ready {
  background: color-mix(in srgb, var(--success) 12%, transparent);
  border-color: var(--success);
}
.poi-readiness-banner .info-panel-body {
  display: flex;
  align-items: center;
  gap: 12px;
}
.poi-readiness-banner .info-panel-body > .btn { margin-left: auto; }

/* 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: 13.5px; align-items: baseline;
}
.info-row-k {
  /* Caption tier — matches the read-only field display principles
     adapted for the compact drawer context. Size stays 10px (drawer
     is narrower than full-page form, L4 reference content) but
     contrast bumps from text-3 → text-2 so the small-caps key
     doesn't whisper. */
  font-size: 10px; font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--text-2);
  padding-top: 2px;
}
.info-row-v {
  /* Value tier — weight 500 → 600 so it carries the row's content
     decisively against the bolder caption. Size 13.5px (modest bump
     from 13px) stays compact for the drawer. */
  color: var(--text); font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.info-row-empty { color: var(--text-3); font-weight: 400; font-style: italic; }

/* ============================================================
 * Field-view — standalone read-only field display.
 *
 * Vertical label-above-value rendering for read-only structured
 * data. Layout-agnostic: drops into any panel/card/section, no
 * `.editable` wrapper or `<input>` required. Solves the gap
 * between `.editable:not(.is-editing) .field` (full-page form
 * view-mode, requires an input) and `.info-row` (horizontal
 * drawer reference rows).
 *
 * Type tier matches the canonical read-only hierarchy defined in
 * docs/ui-design-guidelines.md (read-only field display
 * principles): caption 12/700/text-2/uppercase, value 16/600/
 * text. Empty state via `.is-empty` modifier on the value renders
 * italic "— Not set" in text-3.
 *
 * Demoed at pages/design-system.html#field-view.
 * ============================================================ */
.field-view + .field-view {
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}
.field-view-caption {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-2);
  margin-bottom: 4px;
  display: flex; align-items: center; gap: 8px;
  line-height: 1.2;
}
.field-view-caption-badge {
  font-size: 9.5px; font-weight: 600;
  padding: 1px 6px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-3);
  letter-spacing: 0.04em;
  background: var(--bg-alt);
}
.field-view-value {
  font-size: 16px;
  font-weight: 600;
  color: var(--text);
  line-height: 1.4;
  word-break: break-word;
}
.field-view-value.is-empty {
  font-size: 14px;
  font-weight: 400;
  font-style: italic;
  color: var(--text-3);
}
.field-view-value code {
  font-family: var(--font-mono);
  font-size: 14.5px;
  background: var(--bg-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 1px 6px;
}
.field-view-hint {
  margin-top: 4px;
  font-size: 12px;
  color: var(--text-3);
  line-height: 1.45;
}
/* 2-column grid wrapper — first row inherits no top border/spacing. */
.field-view-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: 32px;
  row-gap: 0;
}
.field-view-grid > .field-view:nth-child(-n+2) {
  margin-top: 0; padding-top: 0; border-top: 0;
}

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

/* Canonical skeleton modifiers — render in HTML during the module / data
 * phase of a page load so empty containers look intentional. Mount functions
 * clear them on first paint. See ui-design-guidelines.md "Loading state". */
.skeleton-text          { display: block; height: 14px; width: 100%; }
.skeleton-text--short   { width: 60%; }
.skeleton-text--shorter { width: 40%; }
.skeleton-title         { display: block; height: 28px; width: 70%; border-radius: var(--radius-md); }
.skeleton-row           { display: block; height: 52px; width: 100%; border-radius: var(--radius-md); }
.skeleton-card          { display: block; height: 140px; width: 100%; border-radius: var(--radius-lg); }
.skeleton-pane          { display: block; width: 100%; min-height: 240px; flex: 1; border-radius: var(--radius-lg); }
.skeleton-stack         { display: flex; flex-direction: column; gap: 12px; }
.skeleton-stack--tight  { gap: 8px; }

/* ====================================================
 * GLOBAL TOP PROGRESS BAR + PAGE-READY DIM
 * Mounted once by shell.js; subscribes to portal:fetch-pending events
 * emitted by api.js's request wrapper. 200ms debounce on show so fast
 * requests don't flicker; immediate hide on zero pending. The dim rules
 * disable in-page workbar chrome until the page sets pageState='ready' —
 * sidebar / topbar nav stays clickable (those are navigation away).
 * See ui-design-guidelines.md "Loading state". */
.portal-progress {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 2px;
  z-index: var(--z-tooltip);
  pointer-events: none;
  opacity: 0;
  overflow: hidden;
  transition: opacity 160ms ease;
}
.portal-progress.is-active { opacity: 1; }
.portal-progress::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent 0%, var(--primary) 50%, transparent 100%);
  background-size: 40% 100%;
  background-repeat: no-repeat;
  animation: portal-progress 1.1s infinite linear;
}
@keyframes portal-progress {
  from { background-position: -40% 0; }
  to   { background-position: 140% 0; }
}

body[data-page-state="loading"] .workbar-tab,
body[data-page-state="loading"] .workbar-mobile-trigger,
body[data-page-state="loading"] .workbar-split {
  pointer-events: none;
  opacity: 0.55;
}

/* ====================================================
 * 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;
}
/* Submenu — same surface as .menu, opens to the right of its parent
   item. Parent item with .has-submenu shows a ▶ chev (built by the
   menu.js item builder). When the submenu is open, the parent item
   gets .is-submenu-open which holds the hover background so the
   relationship stays visually anchored. */
.menu-submenu { animation: menu-fade-in 90ms ease-out; }
.menu-item.has-submenu.is-submenu-open { background: var(--bg-alt); }
@keyframes menu-fade-in {
  from { opacity: 0; transform: translateX(-4px); }
  to   { opacity: 1; transform: translateX(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 notifications dropdown — action-first feed ----------
   Direction C, signed off on design-system.html (see #notif-feed).
   Lives inside .menu-custom (bell-icon menu in shell.js). Three-row CSS
   grid: title-row / body-row / optional action-row. LED in column 1
   encodes read state — --primary for unread, --text-3 for read.
   Action chip only renders on unread + has-link rows. Read items stay
   the same 2-row shape (title + body) so the list scans consistently. */
.notif {
  margin: -8px;            /* cancel .menu-custom padding so feed reaches edges */
  display: flex;
  flex-direction: column;
}
.notif-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 14px 8px;
  border-bottom: 1px solid var(--border);
}
.notif-head-title {
  font-size: var(--fs-base);
  font-weight: 600;
  color: var(--text);
  letter-spacing: -0.01em;
}
.notif-head-count {
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--primary);
  background: var(--primary-50);
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  letter-spacing: 0.02em;
}
.notif-head-count.is-zero {
  color: var(--text-3);
  background: transparent;
  font-weight: 500;
  padding: 2px 0;
}
.notif-list {
  max-height: 420px;
  overflow-y: auto;
  padding: 4px 0;
}
.notif-row {
  display: grid;
  grid-template-columns: 14px 1fr auto;
  column-gap: 10px;
  row-gap: 4px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--border);
  color: inherit;
  text-decoration: none;
  transition: background 120ms var(--ease-out);
  cursor: pointer;
}
.notif-row:last-child { border-bottom: 0; }
.notif-row:hover,
.notif-row:focus-visible {
  background: var(--bg-alt);
  outline: 0;
}
.notif-row:focus-visible {
  box-shadow: inset 0 0 0 2px var(--primary);
}
.notif-dot {
  grid-column: 1;
  grid-row: 1;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--primary);
  align-self: center;
  justify-self: center;
}
.notif-row.is-read .notif-dot { background: var(--text-3); }
.notif-title {
  grid-column: 2;
  grid-row: 1;
  font-size: var(--fs-base);
  font-weight: 600;
  color: var(--text);
  line-height: 1.35;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.notif-row.is-read .notif-title {
  font-weight: 400;
  color: var(--text-2);
}
.notif-time {
  grid-column: 3;
  grid-row: 1;
  font-size: var(--fs-xs);
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  align-self: center;
  white-space: nowrap;
}
.notif-body {
  grid-column: 2 / -1;
  grid-row: 2;
  font-size: var(--fs-sm);
  color: var(--text-2);
  line-height: 1.4;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.notif-actions {
  grid-column: 2 / -1;
  grid-row: 3;
  display: flex;
  justify-content: flex-end;
  padding-top: 2px;
}
.notif-action {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg);
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--text);
  cursor: pointer;
  transition: background 120ms var(--ease-out), border-color 120ms var(--ease-out);
}
.notif-action:hover {
  background: var(--bg-alt);
  border-color: var(--border-strong, var(--border));
}
.notif-action svg {
  width: 12px; height: 12px;
  opacity: 0.7;
}
.notif-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 10px 8px 14px;
  border-top: 1px solid var(--border);
  background: var(--bg-sunken);
}
.notif-foot-link {
  background: none;
  border: 0;
  padding: 4px 8px;
  font-size: var(--fs-sm);
  color: var(--text-2);
  cursor: pointer;
  border-radius: var(--radius-sm);
}
.notif-foot-link:hover { background: var(--bg-alt); color: var(--text); }
.notif-foot-link.is-primary { color: var(--primary); font-weight: 500; }
.notif-empty {
  padding: 18px 12px;
  font-size: var(--fs-sm);
  color: var(--text-3);
  text-align: center;
}

/* ----- 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. */
/* 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"] .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%);
}

/* ====================================================
 * ALERT      — warning/info/danger banner
 * SECTION GROUP — uppercase-tracked label + thin divider
 *
 * .stat-card and its children (.stat-card-label/-value/-delta and the
 * .is-compact / .is-down variants) were removed here — they were a
 * parallel canonical primitive to .kpi (line 1031) with no remaining
 * consumers after the v1.80→post-1.80 migration. Use .kpi / .kpi.is-dense
 * for any stat-tile work.
 * ==================================================== */

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

/* ============================================================
 * Phase 1 — per-row price-lock indicator (.product-lines-lock-pill)
 * + override-reason field (.product-lines-override-reason).
 *
 * Lock-pill sits inline inside .product-lines-control--price next
 * to the unit-price input or static span. Informational only;
 * does not affect editability.
 *
 * Override-reason renders BETWEEN the row's head and its controls
 * (consumer opts in via showOverrideReason). When editable
 * (canEditPrice), it's a small text input. When read-only AND a
 * reason exists, it's a muted italic line. When read-only AND no
 * reason, it doesn't render at all (Step 0.5 marker 6).
 * ============================================================ */
.product-lines-lock-pill {
  display: inline-flex;
  align-items: center;
  margin-left: var(--space-1);
  padding: 1px 7px;
  background: var(--bg-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  color: var(--text-3);
  font-size: 10.5px;
  line-height: 1.3;
  vertical-align: middle;
}
.product-lines-override-reason {
  display: block;
  width: 100%;
  margin-top: var(--space-1);
  padding: 5px 8px;
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  font-family: var(--font-mono);
  font-size: 11px;
}
.product-lines-override-reason::placeholder { color: var(--text-3); }
.product-lines-override-reason--ro {
  background: transparent;
  border: 0;
  padding: 5px 0;
  color: var(--text-3);
  font-family: var(--font-mono);
}

/* 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) :is(.section-card, .section-panel) .field input:not([type=checkbox]):not([type=radio]):not([type=button]):not([type=submit]),
.editable:not(.is-editing) :is(.section-card, .section-panel) .field select,
.editable:not(.is-editing) :is(.section-card, .section-panel) .field textarea {
  pointer-events: none;
  background: transparent;
  border-color: transparent;
  padding-left: 0;
  padding-right: 0;
  box-shadow: none;
  /* Value tier — matches the .field-view canonical read-only
     hierarchy (see field-view above + docs/ui-design-guidelines.md
     Step 5e). Confident size + weight so the value reads as primary
     content at a squint, decisively distinct from the demoted
     caption above. */
  font-size: 16px;
  font-weight: 600;
  color: var(--text);
}
.editable:not(.is-editing) :is(.section-card, .section-panel) .field select {
  background-image: none;
  appearance: none;
  -webkit-appearance: none;
}
.editable:not(.is-editing) :is(.section-card, .section-panel) .field .picker-trigger {
  pointer-events: none;
  background: transparent;
  border-color: transparent;
  padding-left: 0;
  padding-right: 0;
}
.editable:not(.is-editing) :is(.section-card, .section-panel) .field .picker-trigger-chev,
.editable:not(.is-editing) :is(.section-card, .section-panel) .field-help {
  display: none;
}
/* Caption tier — matches the .field-view canonical hierarchy. Bumped
   weight (500 → 700) + contrast (text-3 → text-2) so the caption
   reads as a deliberate annotation, not a weak whisper. Size stays
   12px so it's clearly the demoted tier vs the 16px value.
   No margin-bottom: the .field's flex gap (6px, components.css:182)
   already provides intra-field caption-to-value spacing. Adding a
   margin here would stack on the gap and widen the intra-field gap
   relative to the inter-field gap — exactly the Gestalt proximity
   failure the inter-field gap rule below is designed to prevent. */
.editable:not(.is-editing) :is(.section-card, .section-panel) .field-label {
  font-size: 12px;
  font-weight: 700;
  color: var(--text-2);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
/* The required asterisk loses meaning in view mode — strip it. */
.editable:not(.is-editing) :is(.section-card, .section-panel) .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) :is(.section-card, .section-panel) .field:has(input:placeholder-shown) .input,
.editable:not(.is-editing) :is(.section-card, .section-panel) .field:has(textarea:placeholder-shown) .input {
  display: none;
}
.editable:not(.is-editing) :is(.section-card, .section-panel) .field:has(input:placeholder-shown)::after,
.editable:not(.is-editing) :is(.section-card, .section-panel) .field:has(textarea:placeholder-shown)::after {
  content: "— Not set";
  /* Bumped from text-4 → text-3 so the empty state reads as a
     deliberate placeholder, not a forgotten field. Matches the
     .field-view canonical .is-empty rendering. */
  color: var(--text-3);
  font-size: 14px;
  font-style: italic;
}
/* Checkboxes inside .field in view mode: pointer-events off so the user
   can't toggle them without entering edit mode. The rule above excludes
   [type=checkbox] from the text-input lockdown (different visual treatment);
   this companion rule closes the gap for label-wrapped checkboxes in .field
   containers (e.g. company-edit.html PO inbox section). */
.editable:not(.is-editing) :is(.section-card, .section-panel) .field input[type=checkbox],
.editable:not(.is-editing) :is(.section-card, .section-panel) .field input[type=radio] {
  pointer-events: none;
  opacity: 0.6;
}
.editable:not(.is-editing) :is(.section-card, .section-panel) .field label:has(input[type=checkbox]),
.editable:not(.is-editing) :is(.section-card, .section-panel) .field label:has(input[type=radio]) {
  cursor: default;
}

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

/* Breathing room BELOW the mode-indicator bar when the .editable houses
 * a .section-panel stack (theme-studio is the canonical example;
 * record-editor pages like po / invoice / quote-edit also inherit).
 * Scoped via `:has(.section-panel)` so lighter consumers without a
 * section-panel stack (content / catalog / email-templates / profile)
 * keep their existing tighter 6px stripe-adjacent rhythm.
 *
 * The space lives on the bar (chrome), not on `.section-panel:first-child`
 * (content) — so the rule survives if the first content shape changes
 * later, and the gap reads as "below the mode-indicator" rather than
 * "above the first section." Pages just use `.editable` + `.section-panel`;
 * no per-page padding override needed.
 *
 * On workbar-paired editors, the more-specific
 * `.workbar.has-mode-indicator ~ .editable` rule below still applies
 * for parity with their existing 18px tuning. */
.editable:has(.section-panel) {
  padding-top: var(--space-5);
}

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

/* Breathing room between workbar and first content child of .editable on
 * record-editor pages. Without this, pages that don't ship a utility
 * row above their content (e.g. invoice.html / quotation-edit.html,
 * which jump straight from workbar to a grid wrapper) press content
 * flush against the workbar's bottom edge — fails Step 0.5 marker 4
 * (chrome separates from content). Scoped to .has-mode-indicator-paired
 * .editable so non-workbar editor pages (content / catalog / email-
 * templates / profile / theme-studio) keep their tighter 6px stripe-
 * adjacent padding. Pages that DO ship a .collapse-all-toggle as their
 * first child get the same 18px gap from this rule; the toggle's own
 * margin then provides the gap below itself before the first section. */
.workbar.has-mode-indicator ~ .editable {
  padding-top: 18px;
}

/* 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) :is(.section-card, .section-panel) .ad-hoc-row > span,
.editable:not(.is-editing) :is(.section-card, .section-panel) .ad-hoc-row label:not(:has(input:checked)),
.editable:not(.is-editing) :is(.section-card, .section-panel) .ad-hoc-row label input[type="radio"] {
  display: none;
}
.editable:not(.is-editing) :is(.section-card, .section-panel) .ad-hoc-row {
  background: transparent;
  border: 0;
  padding: 0;
}
.editable:not(.is-editing) :is(.section-card, .section-panel) .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) :is(.section-card, .section-panel) .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; }

/* ============================================================
 * CODE BLOCK
 * Monospace block for showing code/markup snippets inline in
 * documentation pages (e.g. design-system.html). Works with
 * bare <pre class="code">…</pre> or the semantic
 * <pre class="code"><code>…</code></pre> form — styling lives
 * on the <pre>; nested <code> inherits the font.
 *
 * White-space preserved (inherits <pre> default); long lines
 * scroll horizontally so the page layout never breaks.
 *
 * Hand-tuned values (no general token covers them):
 *   line-height: 1.6  — code-reading specific.
 *   padding-x:   14px — half-step over --space-3 (12px) for
 *                       monospace inset symmetry; mono chars
 *                       are narrower than text chars.
 * Everything else flows through tokens.
 * ============================================================ */
.code {
  font-family: var(--font-mono);
  font-size: var(--fs-md);
  line-height: 1.6;
  color: var(--text);
  background: var(--bg-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-3) 14px;
  margin: 0 0 var(--space-3);
  overflow-x: auto;
}

/* ============================================================
 * AUTH STAGE SHELL — the animated full-viewport backdrop shared
 * by the public auth pages. Consumers: login.html + register.html.
 *
 * Lifted here from two byte-identical copies — register
 * deliberately mirrored login's chrome "so the two pages stay
 * visually identical without sharing a file." Canonicalizing kills
 * that drift risk. Page-INTERNAL card markup stays page-scoped:
 * login keeps its rich .login-card / .login-input-shell family;
 * register keeps its .auth-card family.
 *
 * .svg-orbit-fwd / .svg-orbit-rev keep these names deliberately —
 * the operator-overridable login-decoration SVG references them by
 * class, so renaming would break custom decorations.
 * ============================================================ */
.login-stage {
  position: relative;
  width: 100%;
  min-height: 100vh;
  overflow: hidden;
}

/* SVG decoration layer — fills the stage, sits behind everything. */
.login-bg {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 0;
}
.login-bg svg {
  width: 100%;
  height: 100%;
  display: block;
}

/* Soft accent + secondary blooms in opposing corners. */
.login-orb {
  position: absolute;
  pointer-events: none;
  z-index: 1;
  width: 360px; height: 360px;
  border-radius: 50%;
  filter: blur(70px);
  opacity: 0.55;
}
.login-orb.tl {
  top: -140px; left: -140px;
  background: radial-gradient(circle, var(--primary), transparent 70%);
}
.login-orb.br {
  bottom: -160px; right: -160px;
  background: radial-gradient(circle, var(--secondary), transparent 70%);
}

/* SVG orbit animations — referenced by class from inside the
   decoration SVG. Defining keyframes here means the SVG itself
   stays declarative (no inline style attributes). */
@keyframes svg-orbit-fwd { to { transform: rotate(360deg); } }
@keyframes svg-orbit-rev { to { transform: rotate(-360deg); } }
.svg-orbit-fwd {
  transform-origin: 800px 500px;
  animation: svg-orbit-fwd 60s linear infinite;
}
.svg-orbit-rev {
  transform-origin: 800px 500px;
  animation: svg-orbit-rev 90s linear infinite;
}

/* Top bar — wordmark + brand logo. */
.login-top {
  position: absolute; top: 0; left: 0; right: 0;
  padding: 22px 40px;
  display: flex; justify-content: flex-start; align-items: center;
  z-index: 5;
}
.login-wordmark {
  display: inline-flex; align-items: center; gap: 10px;
  color: var(--text);
  text-decoration: none;
}
.login-wordmark .brand-logo {
  display: inline-flex; align-items: center; flex-shrink: 0;
}
.login-wordmark .brand-logo:empty { display: none; }
.login-wordmark .brand-monogram {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px;
  border-radius: var(--radius);
  background: var(--primary-50);
  color: var(--primary);
  font-weight: 700;
  font-size: 14px;
  letter-spacing: -0.02em;
  line-height: 1;
}
.login-wordmark .word {
  font-weight: 700; font-size: 18px;
  letter-spacing: -0.02em;
  color: var(--text);
}
.login-wordmark .dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--secondary);
  align-self: flex-end;
  margin-bottom: 6px;
}

/* Centering wrap for the page's card. */
.login-card-wrap {
  position: relative;
  min-height: 100vh;
  display: flex; align-items: center; justify-content: center;
  padding: 96px 24px 96px;
  z-index: 4;
}
@keyframes login-card-in {
  from { opacity: 0; transform: translateY(10px) scale(.98); }
  to   { opacity: 1; transform: none; }
}

/* Trust strip — hidden when its label is unset. */
.login-trust {
  position: relative;
  padding: 24px 40px 30px;
  z-index: 5;
  text-align: center;
}
.login-trust-label {
  font-size: var(--fs-xs);
  color: var(--text-3);
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.login-trust:empty { display: none; }

@media (max-width: 700px) {
  .login-top { padding: 18px 20px; }
}
/* Reduced-motion: kill the orbit animation. SVG halo gradient stays
   (it's static). Each page's card-entrance animation is killed in its
   own page-scoped reduced-motion block. */
@media (prefers-reduced-motion: reduce) {
  .svg-orbit-fwd, .svg-orbit-rev { animation: none; }
}
