/* ─────────────────────────────────────────────────────────────────────
   THEME TOKENS
   ─────────────────────────────────────────────────────────────────────
   Every value below is EXACTLY the hex literal it replaced, so defining
   these changed nothing about how the site looks. That is the whole
   safety property of the change that introduced them: 1,849 literals
   across 15,000 lines became var(--token, #same-literal), and the
   fallback means a stylesheet loaded without these definitions still
   renders identically.

   ⚠️ EDIT THE DEFAULTS HERE, NOT THE USE SITES. An admin's palette is
   served as an override block from GET /theme.css, which redefines a
   subset of these on :root -- so a token changed here is the default
   somebody's override starts from.

   Light is the base. Dark redefines the same names under
   [data-theme="dark"] and under prefers-color-scheme for visitors who
   have expressed no preference of their own.

   ⚠️ THE BRAND HUES ARE LIGHTENED IN DARK, NOT SWAPPED. A brand colour
   that changes identity between modes has stopped being a brand colour;
   what changes is its lightness against a dark ground.
   ───────────────────────────────────────────────────────────────────── */
:root {
  --c-surface: #ffffff;  /* Page and card background */
  /* The paper an email is previewed on. FIXED WHITE IN BOTH MODES,
     deliberately: a preview renders the sender's own HTML, which
     assumes the white background every mail client gives it, and
     flipping it dark would make the preview lie about what the
     recipient sees. It is a token so a white-label can reach it,
     not so the theme can. */
  --c-paper: #ffffff;
  --c-surface-subtle: #f8fafc;  /* Subtle background (panel heads, hover) */
  --c-surface-sunken: #f1f5f9;  /* Sunken background (tracks, wells) */
  --c-border: #e2e8f0;  /* Default border */
  --c-border-strong: #cbd5e1;  /* Stronger border */
  --c-border-soft: #e5e7eb;  /* Softer border */
  --c-text: #0f172a;  /* Primary text */
  --c-text-2: #334155;  /* Secondary text */
  --c-text-3: #475569;  /* Tertiary text */
  --c-text-muted: #64748b;  /* Muted text */
  --c-text-dim: #94a3b8;  /* Dim text and placeholders */
  --c-ink: #1a1a1a;  /* Near-black ink */
  --c-brand: #6366f1;  /* Brand colour */
  --c-brand-strong: #4f46e5;  /* Brand, pressed/strong */
  --c-brand-deep: #4338ca;  /* Brand text on a tinted background */
  --c-brand-alt: #8b5cf6;  /* Brand accent (gradient end) */
  --c-brand-tint: #f5f3ff;  /* Brand tint background */
  --c-brand-tint-2: #eef2ff;  /* Brand tint, alternate */
  --c-brand-border: #c7d2fe;  /* Brand-tinted border */
  --c-success: #16a34a;  /* Success */
  --c-success-alt: #10b981;  /* Success, alternate */
  --c-success-text: #166534;  /* Success text */
  --c-success-tint: #f0fdf4;  /* Success background */
  --c-success-border: #bbf7d0;  /* Success border */
  --c-warning: #f59e0b;  /* Warning */
  --c-warning-text: #92400e;  /* Warning text */
  --c-warning-tint: #fffbeb;  /* Warning background */
  --c-warning-border: #fde68a;  /* Warning border */
  --c-danger: #ef4444;  /* Danger */
  --c-danger-strong: #dc2626;  /* Danger, strong */
  --c-danger-deep: #b91c1c;  /* Danger, deepest */
  --c-danger-darkest: #991b1b;  /* Danger text */
  --c-danger-tint: #fef2f2;  /* Danger background */
  --c-danger-border: #fecaca;  /* Danger border */
  --c-on-accent: #ffffff;  /* Text on a coloured or dark surface */
  --c-chrome-text: #cbd5e1;  /* Header and nav label */
  --c-chrome-dim: #94a3b8;  /* Muted label on dark chrome */
  --c-page: #f4f5f7;  /* App page background */
  --c-info: #2563eb;  /* Info / link blue */
  --c-info-text: #1d4ed8;  /* Info text */
  --c-info-tint: #eff6ff;  /* Info background */
  --c-info-tint-2: #dbeafe;  /* Info background, deeper */
  --c-info-border: #bfdbfe;  /* Info border */
  --c-violet: #7c3aed;  /* Violet accent */
  --c-violet-text: #6d28d9;  /* Violet text */
  --c-violet-tint: #faf9ff;  /* Violet background */
  --c-violet-tint-2: #ede9fe;  /* Violet background, deeper */
  --c-violet-border: #ddd6fe;  /* Violet border */
  --c-brand-tint-3: #e0e7ff;  /* Brand tint, deepest */
  --c-success-tint-2: #dcfce7;  /* Success background, deeper */
  --c-warning-tint-2: #fef3c7;  /* Warning background, deeper */
  --c-danger-tint-2: #fee2e2;  /* Danger background, deeper */
  --c-text-soft: #555555;  /* Soft body text */
  --c-text-faint: #999999;  /* Faint text */
  --c-inverse-surface: #0f172a;  /* Inverted surface (dark topbars, panels) */
  --c-inverse-surface-2: #1e293b;  /* Inverted surface, raised */
  --c-on-fill: #ffffff;  /* Text on a light-in-dark fill */
  --c-on-status: #0f172a;  /* Text on a saturated status fill */
  --c-chrome: #17121f;  /* Header gradient, start */
  --c-chrome-2: #241a3d;  /* Header gradient, middle */
  --c-chrome-3: #1a1329;  /* Header gradient, end */
  --c-chrome-control: #27272a;  /* Header control */
  --c-chrome-control-hover: #3f3f46;  /* Header control, hover */
  --c-chrome-border: #4b5563;  /* Header control border */
  --c-chrome-chip: #374151;  /* Header chip and rule */
  --c-chrome-select: #2a2050;  /* Linxi switcher */
  --c-chrome-select-border: #4c3b7a;  /* Linxi switcher border */

  /* ── Header, and ONLY the header (REQ-0046) ─────────────────────────
     ⚠️ A SEPARATE FAMILY FROM --c-chrome-*, DELIBERATELY. Those are shared
     with the E2E Secure module, the legal hero, the landing sections, the
     AI panel and the Secure Clearance pages, all of which are dark in BOTH
     modes on purpose -- flipping the family would take every one of them
     with it, and CLAUDE.md records that a token changing which way it
     leans is exactly how this stylesheet ended up with black text on
     black. The header alone follows the theme; --c-chrome-* is untouched.

     LIGHT values here; the dark block and the data-theme="dark" block
     both carry today's --c-chrome-* values verbatim, so DARK MODE IS
     UNCHANGED. */
  --c-head-1: #ffffff;            /* Header gradient, start */
  --c-head-2: #f8f8fc;            /* Header gradient, middle */
  --c-head-3: #f1f2f8;            /* Header gradient, end */
  --c-head-ink: #0f172a;          /* Strong header text */
  --c-head-text: #475569;         /* Nav label */
  --c-head-control: #ffffff;      /* Header control */
  /* The nav tabs' hover fill. #eef0f6 was too close to the #f8f8fc ground
     to register. The header's BUTTONS (bell, user menu, balance chip,
     admin trigger, mobile toggle) do not use it: they take the same brand
     gradient every button in the app takes on hover -- see the header
     hover block near the end of the file. */
  --c-head-control-hover: #e4e7f0;
  /* The account-type pill beside the username. Was --c-head-chip with
     --c-head-ink on it: a mid grey with near-black text, which reads as
     dark-on-dark on the light header. Indigo tint, indigo ink. */
  --c-head-pill: #eef2ff;
  --c-head-pill-ink: #3730a3;
  /* ⚠️ 3:1 against the header ground. #cbd5e1 measured 1.40:1 -- a
     control whose edge you cannot see. Non-text UI needs 3:1. */
  --c-head-border: #7c8798;       /* Header control border */
  /* Doubles as the nav button's border and the role badge's fill, so
     it has to clear 3:1 as a border AND carry ink as a background. */
  --c-head-chip: #838ea0;         /* Header chip and rule */
  --c-head-select: #eef2ff;       /* Linxi switcher */
  /* ⚠️ Deep enough to be seen against the header, not just against the
     switcher's own tint. #c7d2fe measured 1.41:1 on the ground. */
  --c-head-select-border: #6f7ff0;
  /* ⚠️ A light header needs an edge or it floats off the page. In dark it
     is transparent -- the gradient already separates it. */
  --c-head-edge: #e2e8f0;
  --c-head-drawer: #ffffff;      /* Mobile nav drawer */
  /* Status colours that read on THIS header's ground: dark amber and red
     on the light header, the pale on-dark tints on the dark one. */
  --c-head-warning: #b45309;
  --c-head-danger: #b91c1c;

  --c-on-dark-brand: #a5b4fc;  /* Brand accent on a dark panel */
  --c-on-dark-success: #6ee7b7;  /* Success accent on a dark panel */
  --c-on-dark-warning: #fcd34d;  /* Warning accent on a dark panel */
  --c-on-dark-danger: #fca5a5;  /* Danger accent on a dark panel */
  --c-on-dark-info: #93c5fd;  /* Info accent on a dark panel */
  --c-purple: #a855f7;  /* Purple accent */
  --c-pink: #ec4899;  /* Pink accent */
  --c-sky: #0ea5e9;  /* Sky accent */
  --c-cyan: #06b6d4;  /* Cyan accent */
  --c-teal: #0d9488;  /* Teal accent */
  --c-orange: #ea580c;  /* Orange accent */
  --c-warning-strong: #d97706;  /* Warning, strong */
  /* A QR code must sit on white to scan, in every theme. Same value in
     both modes on purpose -- see --c-chrome-* for the other colours that
     are tokens so a white-label can reach them, not so the theme can. */
  --c-qr-ground: #ffffff;
  --c-shadow: #000000;  /* Shadow colour */
  --c-highlight: #ffffff;  /* Highlight over a dark fill */
  --c-scrim: #0f172a;  /* Modal backdrop */
}

/* Dark, by explicit choice. `[data-theme]` is stamped on <html> by
   js/theme.js from the user's own preference, so it wins over the media
   query below in both directions -- somebody who has chosen Light on a
   dark-set machine gets Light. */
:root[data-theme="dark"] {
  --c-surface: #0f172a;
  /* The paper an email is previewed on. FIXED WHITE IN BOTH MODES,
     deliberately: a preview renders the sender's own HTML, which
     assumes the white background every mail client gives it, and
     flipping it dark would make the preview lie about what the
     recipient sees. It is a token so a white-label can reach it,
     not so the theme can. */
  --c-paper: #ffffff;
  --c-surface-subtle: #1e293b;
  --c-surface-sunken: #243044;
  --c-border: #334155;
  --c-border-strong: #475569;
  --c-border-soft: #2b3a4f;
  --c-text: #e2e8f0;
  --c-text-2: #cbd5e1;
  --c-text-3: #b6c2d1;
  --c-text-muted: #94a3b8;
  --c-text-dim: #7d8da3;
  --c-ink: #e5e7eb;
  --c-brand: #818cf8;
  --c-brand-strong: #6366f1;
  --c-brand-deep: #c7d2fe;
  --c-brand-alt: #a78bfa;
  --c-brand-tint: #26243f;
  --c-brand-tint-2: #1f2340;
  --c-brand-border: #3f3f7a;
  --c-success: #22c55e;
  --c-success-alt: #34d399;
  --c-success-text: #86efac;
  --c-success-tint: #0f2a1c;
  --c-success-border: #1f5136;
  --c-warning: #fbbf24;
  --c-warning-text: #fcd34d;
  --c-warning-tint: #2b2110;
  --c-warning-border: #5c451a;
  --c-danger: #f87171;
  --c-danger-strong: #ef4444;
  --c-danger-deep: #fca5a5;
  --c-danger-darkest: #fecaca;
  --c-danger-tint: #2b1414;
  --c-danger-border: #5c2626;
  --c-on-accent: #ffffff;
  --c-chrome-text: #cbd5e1;
  --c-chrome-dim: #94a3b8;
  --c-page: #0b1220;
  --c-info: #60a5fa;
  --c-info-text: #93c5fd;
  --c-info-tint: #16233c;
  --c-info-tint-2: #1d3557;
  --c-info-border: #24406b;
  --c-violet: #a78bfa;
  --c-violet-text: #c4b5fd;
  --c-violet-tint: #1d1a33;
  --c-violet-tint-2: #2c2554;
  --c-violet-border: #4a3b7f;
  --c-brand-tint-3: #262e57;
  --c-success-tint-2: #14361f;
  --c-warning-tint-2: #3a2c10;
  --c-danger-tint-2: #3b1a1a;
  --c-text-soft: #b6c2d1;
  --c-text-faint: #8291a5;
  --c-inverse-surface: #1e293b;
  --c-inverse-surface-2: #273449;
  --c-on-fill: #0f172a;
  --c-on-status: #0f172a;
  --c-chrome: #17121f;
  --c-chrome-2: #241a3d;
  --c-chrome-3: #1a1329;
  --c-chrome-control: #27272a;
  --c-chrome-control-hover: #3f3f46;
  --c-chrome-border: #4b5563;
  --c-chrome-chip: #374151;
  --c-chrome-select: #2a2050;
  --c-chrome-select-border: #4c3b7a;

  /* Header tokens, dark. ⚠️ These are today's --c-chrome-* values verbatim,
     so the dark header is byte-for-byte the header that shipped. */
  --c-head-1: #17121f;
  --c-head-2: #241a3d;
  --c-head-3: #1a1329;
  --c-head-ink: #ffffff;
  --c-head-text: #cbd5e1;
  --c-head-warning: #fcd34d;
  --c-head-danger: #fca5a5;
  --c-head-pill: #374151;
  --c-head-pill-ink: #ffffff;
  --c-head-control: #27272a;
  --c-head-control-hover: #3f3f46;
  --c-head-border: #4b5563;
  --c-head-chip: #374151;
  --c-head-select: #2a2050;
  --c-head-select-border: #4c3b7a;
  --c-head-edge: transparent;
  --c-head-drawer: #1e293b;

  --c-on-dark-brand: #a5b4fc;
  --c-on-dark-success: #6ee7b7;
  --c-on-dark-warning: #fcd34d;
  --c-on-dark-danger: #fca5a5;
  --c-on-dark-info: #93c5fd;
  --c-purple: #c084fc;
  --c-pink: #f472b6;
  --c-sky: #38bdf8;
  --c-cyan: #22d3ee;
  --c-teal: #2dd4bf;
  --c-orange: #fb923c;
  --c-warning-strong: #f59e0b;
  --c-shadow: #000000;
  --c-highlight: #ffffff;
  --c-scrim: #0f172a;
}

/* Dark, by system preference, for anybody who has NOT chosen. Guarded
   on :not([data-theme="light"]) so an explicit Light choice is not
   overridden by the machine's setting. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --c-surface: #0f172a;
    /* The paper an email is previewed on. FIXED WHITE IN BOTH MODES,
       deliberately: a preview renders the sender's own HTML, which
       assumes the white background every mail client gives it, and
       flipping it dark would make the preview lie about what the
       recipient sees. It is a token so a white-label can reach it,
       not so the theme can. */
    --c-paper: #ffffff;
    --c-surface-subtle: #1e293b;
    --c-surface-sunken: #243044;
    --c-border: #334155;
    --c-border-strong: #475569;
    --c-border-soft: #2b3a4f;
    --c-text: #e2e8f0;
    --c-text-2: #cbd5e1;
    --c-text-3: #b6c2d1;
    --c-text-muted: #94a3b8;
    --c-text-dim: #7d8da3;
    --c-ink: #e5e7eb;
    --c-brand: #818cf8;
    --c-brand-strong: #6366f1;
    --c-brand-deep: #c7d2fe;
    --c-brand-alt: #a78bfa;
    --c-brand-tint: #26243f;
    --c-brand-tint-2: #1f2340;
    --c-brand-border: #3f3f7a;
    --c-success: #22c55e;
    --c-success-alt: #34d399;
    --c-success-text: #86efac;
    --c-success-tint: #0f2a1c;
    --c-success-border: #1f5136;
    --c-warning: #fbbf24;
    --c-warning-text: #fcd34d;
    --c-warning-tint: #2b2110;
    --c-warning-border: #5c451a;
    --c-danger: #f87171;
    --c-danger-strong: #ef4444;
    --c-danger-deep: #fca5a5;
    --c-danger-darkest: #fecaca;
    --c-danger-tint: #2b1414;
    --c-danger-border: #5c2626;
    --c-on-accent: #ffffff;
    --c-chrome-text: #cbd5e1;
    --c-chrome-dim: #94a3b8;
    --c-page: #0b1220;
    --c-info: #60a5fa;
    --c-info-text: #93c5fd;
    --c-info-tint: #16233c;
    --c-info-tint-2: #1d3557;
    --c-info-border: #24406b;
    --c-violet: #a78bfa;
    --c-violet-text: #c4b5fd;
    --c-violet-tint: #1d1a33;
    --c-violet-tint-2: #2c2554;
    --c-violet-border: #4a3b7f;
    --c-brand-tint-3: #262e57;
    --c-success-tint-2: #14361f;
    --c-warning-tint-2: #3a2c10;
    --c-danger-tint-2: #3b1a1a;
    --c-text-soft: #b6c2d1;
    --c-text-faint: #8291a5;
    --c-inverse-surface: #1e293b;
    --c-inverse-surface-2: #273449;
    --c-on-fill: #0f172a;
    --c-on-status: #0f172a;
    --c-chrome: #17121f;
    --c-chrome-2: #241a3d;
    --c-chrome-3: #1a1329;
    --c-chrome-control: #27272a;
    --c-chrome-control-hover: #3f3f46;
    --c-chrome-border: #4b5563;
    --c-chrome-chip: #374151;
    --c-chrome-select: #2a2050;
    --c-chrome-select-border: #4c3b7a;

  /* Header tokens, dark. ⚠️ These are today's --c-chrome-* values verbatim,
     so the dark header is byte-for-byte the header that shipped. */
  --c-head-1: #17121f;
  --c-head-2: #241a3d;
  --c-head-3: #1a1329;
  --c-head-ink: #ffffff;
  --c-head-text: #cbd5e1;
  --c-head-warning: #fcd34d;
  --c-head-danger: #fca5a5;
  --c-head-pill: #374151;
  --c-head-pill-ink: #ffffff;
  --c-head-control: #27272a;
  --c-head-control-hover: #3f3f46;
  --c-head-border: #4b5563;
  --c-head-chip: #374151;
  --c-head-select: #2a2050;
  --c-head-select-border: #4c3b7a;
  --c-head-edge: transparent;
  --c-head-drawer: #1e293b;

    --c-on-dark-brand: #a5b4fc;
    --c-on-dark-success: #6ee7b7;
    --c-on-dark-warning: #fcd34d;
    --c-on-dark-danger: #fca5a5;
    --c-on-dark-info: #93c5fd;
    --c-purple: #c084fc;
    --c-pink: #f472b6;
    --c-sky: #38bdf8;
    --c-cyan: #22d3ee;
    --c-teal: #2dd4bf;
    --c-orange: #fb923c;
    --c-warning-strong: #f59e0b;
    --c-shadow: #000000;
    --c-highlight: #ffffff;
    --c-scrim: #0f172a;
  }
}

* { box-sizing: border-box; }

/* ── Scrollbars ───────────────────────────────────────────────────────
 *
 * ⚠️ DERIVED FROM THE PALETTE, NOT NEW TOKENS TO KEEP IN STEP. The three
 * names below are aliases for tokens that already move with the theme,
 * so an admin recolouring the site recolours its scrollbars and there is
 * nothing extra for the theme editor to know about or for a white-label
 * to set twice.
 *
 * ⚠️ TWO MECHANISMS, AND BOTH ARE NEEDED. `scrollbar-color` is the
 * standard property (Firefox, and Chrome from 121); `::-webkit-scrollbar`
 * is what every older Chrome and Safari understands. Setting only one
 * leaves half the browsers with the operating system's own scrollbar in
 * the middle of a dark app.
 *
 * ⚠️ scrollbar-color IS INHERITED, which is why it is set once on :root
 * and overridden on the dark modules rather than listed per scrolling
 * element. The ::-webkit- pseudo-elements are NOT inherited and do need
 * the universal selector.
 *
 * ⚠️⚠️ AND THIS HAS A COST ON macOS. Styling ::-webkit-scrollbar turns
 * an overlay scrollbar -- the kind that fades out and takes no width --
 * into a classic one that is always visible and occupies about 10px.
 * That is the trade for having them match the theme at all, and it is
 * why the width is modest and the thumb is inset rather than filling
 * the track.
 */
:root {
  --c-scroll-thumb: var(--c-border-strong, #cbd5e1);
  --c-scroll-track: var(--c-surface-subtle, #f8fafc);
  --c-scroll-thumb-hover: var(--c-brand, #6366f1);
}

:root {
  scrollbar-width: thin;
  scrollbar-color: var(--c-scroll-thumb) var(--c-scroll-track);
}

*::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}

*::-webkit-scrollbar-track {
  background: var(--c-scroll-track);
}

*::-webkit-scrollbar-thumb {
  background: var(--c-scroll-thumb);
  border-radius: 8px;
  /* A transparent border with background-clip is the only way to inset a
     webkit thumb; padding does nothing on a scrollbar part. */
  border: 2px solid transparent;
  background-clip: content-box;
}

*::-webkit-scrollbar-thumb:hover {
  background: var(--c-scroll-thumb-hover);
  background-clip: content-box;
}

/* Where two scrollbars meet. Left unstyled it is a pale square in the
   corner of a dark panel. */
*::-webkit-scrollbar-corner {
  background: var(--c-scroll-track);
}

/* ⚠️ FORCED COLOURS DRAWS ITS OWN. Overriding there replaces a scrollbar
   the user's own settings chose with one they did not. */
@media (forced-colors: active) {
  :root {
    scrollbar-color: auto;
  }
}

body {
  font-family: -apple-system, Segoe UI, Roboto, Arial, sans-serif;
  margin: 0;
  background: var(--c-page, #f4f5f7);
  color: var(--c-ink, #1a1a1a);
}

/* ── Text-entry controls, everywhere ──────────────────────────────────
 *
 * ⚠️⚠️ THIS APP DECLARES NO `color-scheme`, SO AN UNSTYLED CONTROL IS A
 * WHITE BOX WITH BLACK TEXT IN DARK MODE. The browser has no signal that
 * the page is dark and paints its own light defaults. Measured before
 * this rule existed: 103 rules in this file styled a control's border,
 * padding or size and stated NEITHER a background nor a colour, so every
 * one of those fields was a white box on a #0f172a card.
 *
 * It had already been fixed twice, locally and identically -- once for
 * [data-admin-panel-id] and #tab-forms, once for the /linxis reply box.
 * This is the same declaration promoted to the base so the next field
 * added anywhere gets it without anybody remembering.
 *
 * ⚠️ --c-surface-subtle AS THE GROUND, LIFTING TO --c-surface ON FOCUS.
 * Most fields sit on a --c-surface card, so a field painted --c-surface
 * is invisible but for its border; the subtle token is a real pair in
 * both modes (#f8fafc on white, #1e293b on #0f172a) and reads as a well
 * you type into. Focus lifts it, so the active field is the brightest
 * thing on the card rather than merely the outlined one.
 *
 * ⚠️ THE TRADE-OFF, STATED: a field sitting ON a --c-surface-subtle
 * panel head is now the same colour as its ground and leans on its
 * border alone. The border is --c-border-strong and reads against
 * either, so nothing disappears -- but that is the case where this
 * choice is weakest, and the alternative (--c-surface) has the mirror
 * problem on every card in the app, which is far commoner.
 *
 * ⚠️ In LIGHT mode this moves fields from #ffffff to #f8fafc, which is
 * a real change and a small one. The colour half in DARK is the bug.
 *
 * ⚠️ THE TYPE LIST IS POSITIVE, NEVER `:not(...)` OF THE ONES TO SKIP.
 * This file's own rule, recorded several times: a fourth input type that
 * is "not a checkbox" is silently included, and the failure is a control
 * with its native chrome painted over. A colour input becomes a grey box
 * with a smear in it, a range loses its track, a checkbox becomes a large
 * empty square, and a file input loses its button. Those carry their own
 * chrome and are not named here.
 *
 * ⚠️ NO padding, NO width, NO font-size. The admin block above learned
 * each of those: several panels lay their fields out themselves, a
 * blanket width breaks them, and font-size collides both with the dozens
 * of scoped sizes in this file and with the mobile 16px !important rule
 * that stops iOS zooming on focus. font-FAMILY only -- controls do not
 * inherit the page font, which is the one that is always wrong.
 *
 * ⚠️ PLACED HERE, IN THE RESET, ON PURPOSE. `input[type="text"]` is
 * (0,1,1) and so is `.reply-box textarea` -- a tie, resolved by source
 * order. Every scoped rule in this file comes after this one and
 * therefore still wins. Moving this block down would silently take over
 * fields that had been deliberately styled.
 */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="search"],
input[type="tel"],
input[type="url"],
input[type="number"],
input[type="date"],
input[type="datetime-local"],
input[type="time"],
input:not([type]),
textarea,
select {
  font-family: inherit;
  background-color: var(--c-surface-subtle, #f8fafc);
  color: var(--c-text, #0f172a);
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 8px;
}

input[type="text"]::placeholder,
input[type="email"]::placeholder,
input[type="password"]::placeholder,
input[type="search"]::placeholder,
input[type="tel"]::placeholder,
input[type="url"]::placeholder,
input[type="number"]::placeholder,
input:not([type])::placeholder,
textarea::placeholder {
  color: var(--c-text-dim, #94a3b8);
}

/* ⚠️ :focus-visible, not :focus -- a ring on every mouse click is noise,
   and this file's button rules and the admin block already draw that
   line. A text field matches :focus-visible whenever it is focused,
   however it was focused, so nothing is lost by clicking into one. */
input[type="text"]:focus-visible,
input[type="email"]:focus-visible,
input[type="password"]:focus-visible,
input[type="search"]:focus-visible,
input[type="tel"]:focus-visible,
input[type="url"]:focus-visible,
input[type="number"]:focus-visible,
input[type="date"]:focus-visible,
input[type="datetime-local"]:focus-visible,
input[type="time"]:focus-visible,
input:not([type]):focus-visible,
textarea:focus-visible,
select:focus-visible {
  outline: none;
  background-color: var(--c-surface, #ffffff);
  border-color: var(--c-brand, #6366f1);
  box-shadow: var(--btn-ring);
}

/* ⚠️ A box-shadow IS DISCARDED IN FORCED COLOURS, so every `outline:
   none` above needs the outline back in the one mode that cannot draw
   the ring. The admin block had the same gap and is covered here too. */
@media (forced-colors: active) {
  input:focus-visible,
  textarea:focus-visible,
  select:focus-visible,
  [data-admin-panel-id] input:focus-visible,
  #tab-forms input:focus-visible,
  [data-admin-panel-id] textarea:focus-visible,
  #tab-forms textarea:focus-visible {
    outline: 2px solid Highlight;
    outline-offset: 1px;
  }
}

header {
  /* REQ-0046: the header follows the theme. --c-head-* is its OWN family --
     see the note where it is declared -- so the E2E module, the legal hero,
     the landing sections and the Secure Clearance pages stay dark, which
     they are on purpose. */
  background: linear-gradient(135deg, var(--c-head-1, #17121f) 0%, var(--c-head-2, #241a3d) 55%, var(--c-head-3, #1a1329) 100%);
  color: var(--c-head-ink, #ffffff);
  /* ⚠️ A light header needs an edge or it floats off the page. Transparent
     in dark, where the gradient already separates it. */
  border-bottom: 1px solid var(--c-head-edge, transparent);
  padding: 1rem 1.5rem;
}

.header-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

header h1 {
  margin: 0;
  font-size: 1.25rem;
}

/* ⚠️ Both brand marks are <a> now, and there is no global `a {}` rule in
   this stylesheet -- so without these two declarations the site name
   renders as UA-default blue and underlined, in the header of every page.
   `color: inherit` also keeps the brand-name colour overlay working: that
   sets `color` on the inner [data-site-brand], and a link colour on the
   ancestor is not what it is overriding. */
.header-brand {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
  color: inherit;
  text-decoration: none;
}

/* ⚠️ ONE VALUE, because the admin panel PREVIEWS this size and the two
   drifted. The light strip's "in the header" tile was 60px (the generic
   -lg) and the dark strip's was 40px (the real one), so the same row in
   two sections showed the same logo at two sizes. Both read the token
   now, and there is a check that they resolve to the same number. */
:root { --header-logo-size: 40px; }

.header-logo {
  width: var(--header-logo-size, 40px);
  height: var(--header-logo-size, 40px);
  object-fit: contain;
  flex-shrink: 0;
}

.user-info {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-size: 0.9rem;
}

/* Hidden by default (desktop shows the full #main-nav tab row directly)
   -- shown only under the mobile breakpoint below, where #main-nav
   collapses into a toggled drawer instead. */
.mobile-nav-toggle {
  display: none;
  background: transparent;
  border: 1px solid var(--c-head-border, #4b5563);
  color: var(--c-head-ink, #ffffff);
  font-size: 1.1rem;
  line-height: 1;
  padding: 0.3rem 0.55rem;
  border-radius: 6px;
  cursor: pointer;
}

.role-badge {
  background: var(--c-head-pill, #374151);
  color: var(--c-head-pill-ink, #ffffff);
  border-radius: 999px;
  padding: 0.15rem 0.6rem;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.role-badge.role-super_admin {
  background: var(--c-warning-strong, #d97706); color: var(--c-on-fill, #ffffff);
}

.role-badge.role-team_admin {
  background: var(--c-info, #2563eb); color: var(--c-on-fill, #ffffff);
}

.plan-usage-badge {
  background: var(--c-brand, #6366f1);
  color: var(--c-on-fill, #ffffff);
  border-radius: 999px;
  padding: 0.15rem 0.65rem;
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.plan-usage-track {
  display: inline-block;
  width: 70px;
  height: 6px;
  border-radius: 999px;
  background: var(--c-chrome-chip, #374151);
  overflow: hidden;
}

.plan-usage-fill {
  display: block;
  height: 100%;
  background: var(--c-brand, #6366f1);
  border-radius: 999px;
}

.plan-usage-fill.plan-usage-fill-warn {
  background: var(--c-warning, #f59e0b);
}

.plan-usage-fill.plan-usage-fill-danger {
  background: var(--c-danger-strong, #dc2626);
}

/* Two independent columns, not a grid that pairs cards into rows --
   align-items: start keeps each column's own height (a short column
   never gets stretched to match a tall one), and each column stacks its
   own cards with the same 1.5rem gap `main` uses between tabs. */
.account-columns {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.5rem;
  align-items: start;
}

.account-col-left,
.account-col-right {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

@media (max-width: 900px) {
  .account-columns {
    grid-template-columns: 1fr;
  }
}

.account-plan-header {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 1rem;
}

.account-usage-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.4rem 0;
}

.account-usage-label {
  width: 100px;
  font-size: 0.9rem;
  color: var(--c-text-2, #334155);
}

.account-usage-track {
  width: 220px;
  height: 8px;
}

.account-usage-text {
  font-size: 0.85rem;
  color: var(--c-text-muted, #64748b);
  font-variant-numeric: tabular-nums;
}

.landing-back-to-app-bar {
  background: var(--c-brand-tint-2, #eef2ff);
  border-bottom: 1px solid var(--c-brand-border, #c7d2fe);
  padding: 0.6rem 1.5rem;
  text-align: center;
}

.landing-back-to-app-bar button {
  background: none;
  border: none;
  color: var(--c-brand-deep, #4338ca);
  font-weight: 600;
  font-size: 0.85rem;
  cursor: pointer;
  padding: 0;
}

.upgrade-modal-comparison {
  display: flex;
  gap: 1rem;
  margin: 1rem 0;
}

.upgrade-modal-comparison > div {
  flex: 1;
  background: var(--c-page, #f4f5f7);
  border-radius: 8px;
  padding: 0.75rem 1rem;
  font-size: 0.85rem;
}

.upgrade-modal-comparison h4 {
  margin: 0 0 0.4rem;
  font-size: 0.85rem;
}

#main-nav {
  display: flex;
  /* Every tab now carries an emoji (REQ-0024), which is ~1.2em of extra
     width across eleven buttons. Without wrapping, a narrow desktop
     window shrinks the flex items until the labels break mid-word, or
     pushes the row past the viewport. Wrapping keeps the header honest
     at any width; the mobile breakpoint below still stacks it. */
  flex-wrap: wrap;
  row-gap: 0.35rem;
  gap: 0.5rem;
  margin-top: 0.75rem;
}

/* The glyph is decorative and aria-hidden; this only stops it being
   squeezed or wrapped away from its label. */
.nav-tab-btn > span[aria-hidden="true"] { font-style: normal; }

.nav-tab-btn {
  background: transparent;
  color: var(--c-chrome-text, #cbd5e1);
  border: 1px solid var(--c-chrome-chip, #374151);
  box-shadow: none;
}

.nav-tab-btn:hover:not(:disabled) {
  background: color-mix(in srgb, var(--c-text-dim, #94a3b8) 14%, transparent);
  border-color: var(--c-chrome-border, #4b5563);
  color: var(--c-on-accent, #ffffff);
  box-shadow: none;
}

/* ⚠️ EVERY .nav-tab-btn IS AN <a href> NOW (index.html), and the href is
   the whole point: a <button> can never show its destination in the
   status bar, so once the tabs had real addresses the nav still gave no
   sign of it -- nothing to hover, nothing to middle-click, nothing to
   "copy link address" on.

   initTabs() (auth.js) preventDefaults a plain left click, so nothing
   navigates: the tab switches in place exactly as before and router.js
   writes the URL. A modifier click is deliberately let through, which is
   what opens the tab in a new window.

   ⚠️ :where() CONTRIBUTES ZERO SPECIFICITY, AND THAT IS WHY IT IS HERE.
   This has to restate what the shared `button {}` base gave these
   elements for free and an anchor does not inherit -- but written as
   `a.nav-tab-btn` it would be (0,1,1) and start OUT-RANKING the variant
   classes that have always overridden the button base at (0,1,0):
   .user-menu-item (its own font-weight 500, block layout and padding --
   see CLAUDE.md, which records that row weight being wrong once
   already), .view-toggle-btn (0.82rem, no radius) and .header-cog-btn.
   Every dropdown row and both view toggles would have silently taken
   button padding, centring and weight. At :where()'s (0,0,1) they beat
   this exactly as they beat `button {}` today.

   font-family: inherit is the load-bearing declaration: buttons do NOT
   inherit the page font, so without it the nav renders in the browser's
   UI typeface -- the same reason .ws-rail-btn states it. */
a:where(.nav-tab-btn) {
  background: var(--btn-grad);
  color: var(--c-on-accent, #ffffff);
  border: none;
  border-radius: var(--btn-radius);
  padding: 0.5rem 1rem;
  font-family: inherit;
  font-size: 0.9rem;
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: 0.01em;
  cursor: pointer;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  box-shadow: var(--btn-shadow);
  transition: background-image 0.18s ease, background-color 0.18s ease,
              box-shadow 0.18s ease, transform 0.12s ease, border-color 0.18s ease;
}

/* The three interaction states, at the same specificity their `button`
   twins carry, so .nav-tab-btn:hover keeps winning the colours and only
   the transform and the focus ring come from here. */
a:where(.nav-tab-btn):hover {
  box-shadow: var(--btn-shadow-hover);
  transform: translateY(-1px);
}

a:where(.nav-tab-btn):active {
  transform: translateY(0);
  box-shadow: var(--btn-shadow);
}

a:where(.nav-tab-btn):focus-visible {
  outline: none;
  box-shadow: var(--btn-shadow), var(--btn-ring);
}

/* The rail rows and the Ads sub-tabs are <a href> for the same reason the
   nav tabs are, and :where() for the same reason too -- .ws-rail-btn,
   .ws-rail-btn-danger, .ads-tab and .ads-tab.active all sit at (0,1,0)
   and must keep winning, exactly as they already win over `button {}`.

   Only two declarations are needed here, because both classes already
   restate almost the whole button base themselves: .ws-rail-btn declares
   its own display, padding, colour and `font-family: inherit`, and
   .ads-tab declares `font: inherit`. Neither sets letter-spacing (which
   the `font` shorthand does not cover) and neither has any reason to
   have thought about text-decoration until now, when these stopped being
   buttons and an anchor's default underline would have drawn a line
   under every row in every rail. */
a:where(.ws-rail-btn),
a:where(.ads-tab) {
  display: inline-flex;
  align-items: center;
  letter-spacing: 0.01em;
  text-decoration: none;
}

.nav-tab-btn.active,
.nav-tab-btn.active:hover {
  background: var(--btn-grad);
  color: var(--c-on-accent, #ffffff);
  border-color: transparent;
  box-shadow: var(--btn-shadow);
}

main {
  max-width: 1280px;
  margin: 0 auto;
  padding: 1.5rem;
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

.card {
  background: var(--c-surface, #fff);
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 8px;
  padding: 1.25rem 1.5rem;
}

.card h2 {
  margin-top: 0;
  font-size: 1.1rem;
}

.ws-linxi-number-display {
  margin: 0 0 0.75rem;
  text-align: center;
  font-size: 1.6rem;
  font-weight: 700;
  color: var(--c-brand, #6366f1);
}

.muted {
  color: var(--c-text-muted, #64748b);
  font-size: 0.9rem;
}

table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 0.75rem;
}

th, td {
  text-align: left;
  padding: 0.5rem 0.6rem;
  border-bottom: 1px solid var(--c-border-soft, #e5e7eb);
  font-size: 0.9rem;
}

th {
  color: var(--c-text-soft, #555555);
  font-weight: 600;
}

.table-toolbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 1rem;
  margin-top: 0.5rem;
}

#contacts-search {
  padding: 0.45rem 0.6rem;
  font-size: 0.9rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
  min-width: 220px;
}

/* --- Button system -------------------------------------------------
   One place that decides what every button in the app looks like.
   Tokens rather than literals so a future palette change is one edit.

   The gradient lives on the BARE `button` rule on purpose. Every button
   that needs to look different -- .secondary, .danger, .nav-tab-btn, the
   feed/profile icon buttons -- already declares its own background, and
   a class always beats an element selector, so those are untouched by
   this. Checked before writing it: of 336 buttons in the app, 107 fall
   through to this rule and 62 of those are plain primary actions; the
   rest are the Quill toolbar, explicitly neutralised further down.

   The shadow is deliberately NEUTRAL, not an indigo glow. Buttons that
   override the background but not the shadow inherit this one, and a
   coloured glow under a grey or transparent button looks like a mistake. */
:root {
  --btn-grad: linear-gradient(135deg, var(--c-brand, #6366f1) 0%, var(--c-brand-alt, #8b5cf6) 100%);
  --btn-grad-hover: linear-gradient(135deg, var(--c-brand-strong, #4f46e5) 0%, #7c3aed 100%);
  --btn-danger-grad: linear-gradient(135deg, var(--c-danger, #ef4444) 0%, var(--c-danger-strong, #dc2626) 100%);
  --btn-danger-grad-hover: linear-gradient(135deg, var(--c-danger-strong, #dc2626) 0%, var(--c-danger-deep, #b91c1c) 100%);
  --btn-surface: linear-gradient(180deg, var(--c-surface, #ffffff) 0%, var(--c-surface-sunken, #f1f5f9) 100%);
  --btn-surface-hover: linear-gradient(180deg, var(--c-surface-subtle, #f8fafc) 0%, var(--c-border, #e2e8f0) 100%);
  --btn-radius: 8px;
  --btn-shadow: 0 1px 2px color-mix(in srgb, var(--c-scrim, #0f172a) 10%, transparent);
  --btn-shadow-hover: 0 3px 10px color-mix(in srgb, var(--c-scrim, #0f172a) 16%, transparent);
  --btn-ring: 0 0 0 3px color-mix(in srgb, var(--c-brand-alt, #8b5cf6) 35%, transparent);
}

button {
  background: var(--btn-grad);
  color: var(--c-on-accent, #ffffff);
  border: none;
  border-radius: var(--btn-radius);
  padding: 0.5rem 1rem;
  font-size: 0.9rem;
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: 0.01em;
  cursor: pointer;
  /* Keeps an emoji or icon optically centred against its label instead
     of sitting on the text baseline. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  box-shadow: var(--btn-shadow);
  transition: background-image 0.18s ease, background-color 0.18s ease,
              box-shadow 0.18s ease, transform 0.12s ease, border-color 0.18s ease;
}

/* ⚠️⚠️ :where(), AND THAT IS THE WHOLE FIX. This was
   `button:hover:not(:disabled)` -- specificity (0,2,1), which OUT-SPECIFIES
   a bare `.foo:hover` at (0,2,0). So a button restyled with a light
   background and dark text kept its dark text and had the indigo gradient
   painted behind it on hover: unreadable, and the author's own :hover rule
   lost silently. This file records that happening at least five times, each
   time patched locally by adding `:not(:disabled)` to one more selector.

   :where() contributes ZERO specificity, so the selector is now (0,1,1) and
   ANY class-level hover rule wins on its own. The well-written ones --
   `.secondary:hover:not(:disabled)` at (0,3,0) -- are unaffected; the 60-odd
   bare ones that were losing now take effect, which is what their authors
   wrote them for.

   ⚠️ AND THE COLOUR IS PAIRED WITH THE BACKGROUND. The gradient is dark, so
   a button that reaches this rule needs light text -- and pairing them was
   unsafe at the old specificity, because it would have overridden those same
   60-odd rules. At (0,1,1) it cannot: anything with its own hover wins
   outright, and only a button with NO hover rule of its own lands here,
   where the two now agree. */
button:hover:where(:not(:disabled)) {
  background: var(--btn-grad-hover);
  color: var(--c-on-accent, #ffffff);
  box-shadow: var(--btn-shadow-hover);
  transform: translateY(-1px);
}

button:active:not(:disabled) {
  transform: translateY(0);
  box-shadow: var(--btn-shadow);
}

/* :focus-visible, not :focus -- a ring on every mouse click is noise,
   but keyboard users had no focus indicator here at all before this. */
button:focus-visible {
  outline: none;
  box-shadow: var(--btn-shadow), var(--btn-ring);
}

button:disabled {
  background: var(--c-surface-sunken, #f1f5f9);
  color: var(--c-text-dim, #94a3b8);
  box-shadow: none;
  transform: none;
  cursor: not-allowed;
}

/* Quill builds its toolbar out of <button> elements, so without this
   every formatting control becomes a gradient pill. Quill's own CDN
   stylesheet sets their size and icon fill; this only strips what the
   rule above would otherwise add. */
.ql-toolbar button,
.ql-toolbar button:hover,
.ql-toolbar button:active,
.ql-toolbar button:focus-visible {
  background: none;
  box-shadow: none;
  transform: none;
  font-weight: 400;
  border-radius: 4px;
  gap: 0;
}

.row-delete-btn,
.danger {
  background: var(--btn-danger-grad);
  color: var(--c-on-accent, #ffffff);
}

.row-delete-btn:hover:not(:disabled),
.danger:hover:not(:disabled) {
  background: var(--btn-danger-grad-hover);
}

.row-action-btn-sm {
  padding: 0.2rem 0.55rem;
  font-size: 0.75rem;
  border-radius: 5px;
}

/* The quiet counterpart to the gradient primary: a light surface with a
   real border, so the two read as a pair rather than as two unrelated
   button styles. */
.secondary {
  background: var(--btn-surface);
  color: var(--c-text-2, #334155);
  border: 1px solid var(--c-border, #e2e8f0);
}

.secondary:hover:not(:disabled) {
  background: var(--btn-surface-hover);
  border-color: var(--c-border-strong, #cbd5e1);
  color: var(--c-text, #0f172a);
}

/* !important, and it is load-bearing.
   ─────────────────────────────────────────────────────────────────────
   `.hidden` is a single class declared near the top of this file, so ANY
   later single-class rule that sets `display` beats it on the
   source-order tiebreak and the element cannot be hidden at all. That is
   not theoretical: this file already carries SEVENTEEN separate
   `.something.hidden { display: none }` workarounds, each with its own
   comment rediscovering the same gotcha -- .modal-overlay, .nav-avatar,
   .bell-badge, .bulk-action-group, .webmail-folder-btn, #reply-channel-
   toggle and the rest. Every one of them is this bug, found one element
   at a time, usually after it shipped.

   The trigger for fixing it properly: the Linxi Settings carrier picker
   stayed on screen next to its own "🔒 Locked" badge, because
   `.carrier-grid { display: grid }` is declared 8000 lines below
   `.hidden` and quietly won. A sweep for the same shape found sixteen
   more elements authored with `class="... hidden"` that have never been
   able to hide -- including the compliance sign-off banner added days
   ago, and the "Change carrier" Danger Zone entry.

   Checked before doing this, because !important deserves it:
     - All 26 rules in this file mentioning .hidden set display: none.
       Nothing anywhere re-shows a hidden element, so there is no
       intentional override for this to break.
     - The four places JS sets style.display are all on elements it just
       created, which never carry .hidden (realtime.js's .nav-unread-badge,
       the two notification row builders) or set display:none anyway
       (pwa.js's install banner). No inline style is overridden.

   The individual .X.hidden overrides scattered below are now redundant.
   They are left in place: they are harmless, they agree with this rule,
   and deleting seventeen rules is more risk than it removes. Do not add
   an eighteenth. */
.hidden {
  display: none !important;
}

select {
  font-size: 0.9rem;
  padding: 0.25rem 0.4rem;
}

.result {
  min-height: 1.2em;
  font-size: 0.9rem;
  margin: 0;
}

.error {
  color: var(--c-danger-strong, #dc2626);
}

.success {
  color: var(--c-success, #16a34a);
}

.login-main {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
}

.login-card {
  width: 100%;
  max-width: 340px;
  padding-top: 1rem;
  padding-bottom: 1.1rem;
}

/* Desktop-only two-column register layout: alternate sign-up paths
   (logo/title, Google, demo, "already have an account") on the left,
   the actual email/password form on the right. Below this width it's
   just .login-card's normal single-column stack, in the same order the
   markup already has it in -- register.html doesn't duplicate any
   markup for the two breakpoints, this only reorders the SAME four
   blocks (.register-brand/.register-form-wrap/.register-alt/
   .register-footer) via grid-template-areas, min-width because this is
   an enhancement layered on an already-correct mobile-first default,
   not a narrow-screen fallback off a desktop default (every other media
   query in this file is max-width for that reason -- this is the one
   deliberate exception). */
@media (min-width: 820px) {
  .register-card {
    max-width: 760px;
    padding-left: 2rem;
    padding-right: 2rem;
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-areas:
      "brand  form"
      "alt    form"
      "footer form";
    align-items: start;
    gap: 0.75rem 3rem;
  }

  .register-brand { grid-area: brand; }
  .register-form-wrap { grid-area: form; }
  .register-alt { grid-area: alt; }
  .register-footer { grid-area: footer; align-self: end; }
}

/* Placeholder for a real logo -- swap the emoji in login.html/
   register.html for an <img> once one exists, no CSS change needed. */
.nav-avatar {
  display: inline-flex;
}

/* Explicit override, not just relying on source order against the global
   .hidden rule (elsewhere in this file) -- same equal-specificity gotcha
   this codebase has hit before (see the account tab layout fix), so this
   is spelled out directly rather than left to depend on where in the
   cascade .hidden happens to sit. */
.nav-avatar.hidden {
  display: none;
}

.nav-avatar img {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  object-fit: cover;
  vertical-align: middle;
}

.social-login {
  margin-bottom: 0.65rem;
}

.btn-google {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  width: 100%;
  padding: 0.45rem;
  background: var(--c-surface, #ffffff);
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
  color: var(--c-text, #0f172a);
  font-weight: 600;
  font-size: 0.9rem;
  text-decoration: none;
  transition: all 0.15s;
  box-shadow: 0 1px 3px color-mix(in srgb, var(--c-shadow, #000000) 8%, transparent);
}

.btn-google:hover {
  background: var(--c-surface-subtle, #f8fafc);
  border-color: var(--c-border-strong, #cbd5e1);
  box-shadow: 0 2px 6px color-mix(in srgb, var(--c-shadow, #000000) 12%, transparent);
}

.social-divider {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin: 0.55rem 0;
  color: var(--c-text-dim, #94a3b8);
  font-size: 0.8rem;
}

.social-divider::before,
.social-divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--c-border, #e2e8f0);
}

/* The logo and name on the sign-in and sign-up cards are the way back to
   the marketing site. `display: block` so the two children keep the exact
   block layout they had unwrapped, and the colour/underline reset for the
   same reason .header-brand needs one -- there is no global `a {}` rule. */
.auth-brand-home {
  display: block;
  color: inherit;
  text-decoration: none;
}

.auth-logo {
  display: block;
  width: 80px;
  height: 80px;
  margin: 0 auto 0.4rem;
  object-fit: contain;
}

.login-card h1 {
  margin-top: 0;
  margin-bottom: 0.1rem;
  font-size: 1.2rem;
  text-align: center;
}

.login-tagline {
  margin-top: 0;
  margin-bottom: 0.5rem;
  font-size: 0.85rem;
  text-align: center;
}

#login-form label,
#add-user-form input,
#add-user-form select {
  display: block;
  width: 100%;
  margin-bottom: 0.75rem;
  font-size: 0.9rem;
}

#login-form input {
  display: block;
  width: 100%;
  margin-top: 0.3rem;
  padding: 0.45rem 0.6rem;
  font-size: 0.95rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
}

#login-form button {
  width: 100%;
  margin-top: 0.5rem;
}

#add-user-form input,
#add-user-form select,
#ws-add-member-form input,
#ws-add-member-form select,
#ws-add-group-form input {
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
  max-width: 280px;
}

#ws-add-group-form input[type="color"] {
  padding: 0.2rem;
  max-width: 60px;
}

#compose-section label {
  display: block;
  font-size: 0.9rem;
  font-weight: 600;
  margin-top: 0.75rem;
  margin-bottom: 0.3rem;
}

#compose-body {
  width: 100%;
  padding: 0.6rem 0.7rem;
  font-family: inherit;
  font-size: 0.95rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
  resize: vertical;
}

#recipient-type,
#recipient-group-select {
  width: 100%;
  max-width: 280px;
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
  margin-top: 0.4rem;
}

#send-btn {
  margin-top: 1rem;
}

.progress-bar-track {
  margin-top: 0.75rem;
  height: 10px;
  background: var(--c-border-soft, #e5e7eb);
  border-radius: 999px;
  overflow: hidden;
}

.progress-bar-fill {
  height: 100%;
  width: 0;
  background: var(--c-info, #2563eb);
  transition: width 0.2s ease;
}

.badge {
  display: inline-block;
  border-radius: 999px;
  padding: 0.15rem 0.6rem;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--c-on-accent, #ffffff);
  text-transform: capitalize;
}

.badge-main {
  background: var(--c-brand, #6366f1); color: var(--c-on-status, #0f172a);
}

.linxicard-approve-checkbox-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0.4rem 0 1rem;
}

.linxicard-approve-checkbox-list label {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-weight: normal;
  font-size: 0.85rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 999px;
  padding: 0.25rem 0.7rem;
  cursor: pointer;
}

.linxicard-approve-checkbox-list label input {
  margin: 0;
}

/* Same pill-checkbox look as .linxicard-approve-checkbox-list, under a
   generic name -- used by the New Contact modal's list/tag/group pickers
   (unrelated to Linxi Card) so that name doesn't leak into an unrelated
   feature's markup. */
.checkbox-pill-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0.4rem 0 1rem;
}

.checkbox-pill-list label {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-weight: normal;
  font-size: 0.85rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 999px;
  padding: 0.25rem 0.7rem;
  cursor: pointer;
}

.checkbox-pill-list label input {
  margin: 0;
}

.badge-direction-inbound {
  background: var(--c-brand, #6366f1); color: var(--c-on-status, #0f172a);
}

.badge-direction-outbound {
  background: var(--c-success, #16a34a); color: var(--c-on-status, #0f172a);
}

/* Twilio's outbound status sequence, colour-coded by how much is
   actually confirmed: grey = handed off but nothing confirmed yet,
   blue = it left Twilio, green = the carrier confirmed delivery,
   red = terminal failure. The matching ⏰/✅/❌ glyphs are prefixed in
   statusBadge() in js/log.js -- keep the two in step; a status with a
   rule here and no icon there (or the reverse) still renders, it just
   looks out of place next to its neighbours.

   .badge-status-accepted is NOT optional decoration: 'accepted' is the
   first status Twilio reports on a fresh send, and with no rule of its
   own it fell through to bare .badge -- which sets color: #fff and no
   background at all, i.e. white on white. That is why a just-sent
   message read as having no status pill rather than an early one. */
.badge-status-delivered {
  background: var(--c-success, #16a34a); color: var(--c-on-status, #0f172a);
}

/* Deliberately NOT the same green as delivered -- 'sent' only means the
   message left Twilio, the carrier has not confirmed anything yet, and
   showing both in one colour hid that distinction entirely. */
.badge-status-sent {
  background: var(--c-info, #2563eb); color: var(--c-on-fill, #ffffff);
}

.badge-status-failed,
.badge-status-undelivered {
  background: var(--c-danger-strong, #dc2626);
}

.badge-status-accepted,
.badge-status-queued {
  background: var(--c-text-muted, #64748b); color: var(--c-on-fill, #ffffff);
}

.badge-status-received {
  background: var(--c-brand, #6366f1); color: var(--c-on-status, #0f172a);
}

.badge-clickable {
  cursor: pointer;
  border: none;
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
}

/* Two classes always beats one in specificity regardless of source
   order, so .modal-overlay.hidden reliably wins over .modal-overlay's
   own `display: flex` -- see the CSS-specificity bug this fixed on the
   original single-ID version of this rule. */
.modal-overlay.hidden {
  display: none;
}

/* ⚠️⚠️ A MODAL SITS ABOVE EVERY VIEW IT CAN BE OPENED FROM, AND
   1000 WAS NOT HIGH ENOUGH. The contact sheet is 1200/1201 -- it had to
   beat the mobile settings rail at 1000 -- so every .modal-overlay
   reachable from inside it rendered BEHIND it: the Delete contact
   confirm, and the trait picker, which had been landing behind the sheet
   since it was moved out of it.

   1300 clears the whole view band: the rail drawer (999/1000), the
   webmail folder panel (1001), the contact sheet (1200/1201) and the
   email editor (1200). Everything ABOVE it is a toast, a floating panel
   or the image lightbox -- none of which opens a modal, so nothing there
   changes.

   ⚠️ THIS IS THE BAND, NOT ONE FIX. The note on .direct-view-open
   already states the rule from the other side: Direct is z-index 60
   precisely so `#error-modal-overlay` stays reachable above it. A view
   that out-stacks this number is the bug, and raising that view is what
   must not happen -- the modal band moves, or the view comes down. */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: color-mix(in srgb, var(--c-shadow, #000000) 50%, transparent);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
  z-index: 1300;
}

.modal {
  background: var(--c-surface, #fff);
  border-radius: 8px;
  padding: 1.5rem;
  width: 100%;
  max-width: 400px;
  box-shadow: 0 10px 40px color-mix(in srgb, var(--c-shadow, #000000) 25%, transparent);
}

.modal h3 {
  margin: 0 0 0.75rem;
  font-size: 1.05rem;
}

.modal p {
  font-size: 0.9rem;
  margin: 0.4rem 0;
}

.modal a {
  color: var(--c-info, #2563eb);
}

.modal #error-modal-close-btn {
  margin-top: 0.75rem;
}

.confirm-modal {
  max-width: 320px;
}

.campaign-rate-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin: 0.4rem 0 0.9rem;
}

.campaign-rate-number {
  width: 60px;
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
  font-size: 0.85rem;
}

#campaign-rate-unit {
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
  font-size: 0.85rem;
}

#campaign-schedule-at {
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
  font-size: 0.85rem;
}

/* ⚠️ WIDE, AND CAPPED IN HEIGHT. At 720px the performance grid collapsed
   to a single column and the popout became one very tall strip -- every
   section stacked, then seventeen recipients under it. The width is what
   lets the summary sit ACROSS rather than DOWN.

   Flex column with a height cap so the popout never runs past the
   viewport: the header, the tiles and the summary stay put, and the
   recipient table is the only part that scrolls. */
.campaign-detail-modal {
  max-width: min(1180px, 95vw);
  width: 100%;
  max-height: 90vh;
  display: flex;
  flex-direction: column;
}
/* Anything that is not the scroller keeps its natural height; the
   scroller takes what is left. Without min-height:0 a flex child refuses
   to shrink below its content and the cap above does nothing. */
.campaign-detail-modal > * { flex: 0 0 auto; }
.campaign-detail-modal > .campaign-recipients-scroll { flex: 1 1 auto; min-height: 0; }

.campaign-detail-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin: 0.6rem 0;
}

.campaign-stat-pill {
  background: var(--c-page, #f4f5f7);
  border-radius: 6px;
  padding: 0.35rem 0.7rem;
  font-size: 0.8rem;
}

.campaign-stat-pill strong {
  display: block;
  font-size: 1rem;
}

/* ⚠️ ABOUT FIVE ROWS, NOT 45vh. On a tall screen 45vh showed a dozen
   recipients at once, which is a list to scroll past rather than read --
   and the summary above it is the thing somebody opened this for. Five is
   enough to see the shape of the list and to know it scrolls.

   A row is ~44px with the actions inline; the header is sticky and adds
   its own. */
.campaign-recipients-scroll {
  max-height: calc(5 * 44px + 38px);
  overflow-y: auto;
  margin: 0.5rem 0;
  border: 1px solid var(--c-border);
  border-radius: 8px;
}
/* Scrolling past the header leaves the columns unlabelled, and this one
   has an Opened column that is meaningless without its name. */
.campaign-recipients-scroll thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--c-surface-subtle);
}
/* The actions sit INLINE. Stacked they doubled every row's height, which
   is most of why the list ran so long. */
.campaign-recipients-scroll .campaign-quick-reply {
  display: flex;
  gap: 0.35rem;
  align-items: center;
  flex-wrap: wrap;
}
.campaign-recipients-scroll td { vertical-align: middle; }
/* The subject repeats on every row of a campaign -- it is the same
   message -- so it must not be what decides the column widths. */
.campaign-recipients-scroll td:nth-child(3) {
  max-width: 26rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.campaign-quick-reply {
  display: flex;
  gap: 0.3rem;
}

.campaign-quick-reply input {
  width: 140px;
  padding: 0.25rem 0.4rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 5px;
  font-size: 0.78rem;
}

.confirm-modal-actions {
  justify-content: flex-end;
  margin-top: 1rem;
}

#new-workspace-form label,
#ws-settings-form label,
#account-info-form label,
#account-password-form label,
#email-no-account label,
#email-password-reveal label,
#admin-email-form label,
#admin-cpanel-form label {
  display: block;
  font-size: 0.9rem;
  font-weight: 600;
  margin-top: 0.75rem;
  margin-bottom: 0.3rem;
}

#account-info-form input,
#account-password-form input,
#email-no-account input,
#email-has-account input,
#email-password-reveal input,
#admin-email-form input,
#admin-cpanel-form input {
  display: block;
  width: 100%;
  padding: 0.45rem 0.6rem;
  font-size: 0.95rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
  margin-bottom: 0.5rem;
}

#new-workspace-form input,
#ws-settings-form input,
#new-apikey-value {
  display: block;
  width: 100%;
  padding: 0.45rem 0.6rem;
  font-size: 0.95rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
}

#new-apikey-value {
  margin: 0.75rem 0;
  font-family: monospace;
  background: var(--c-page, #f4f5f7);
}

/* #ws-settings-form input above is an ID selector (1,0,1) and it matches
   EVERY input in the settings form -- checkboxes included. So all 13 of
   them were being painted as display:block, width:100%, with padding and
   a 1px border: a full-width bordered box where a tick should be. And
   #ws-settings-form label forced display:block over .mnw-agree's
   display:flex, so align-items:flex-start was never doing anything and
   the box could not sit at the top left of its text.
   
   No class selector can beat an id, so the repair has to carry the id
   too. Scoped to the form rather than added to each component, because
   this hit the three Step 4 agreements, the two capability toggles and
   every other checkbox in here the same way. */
#ws-settings-form input[type="checkbox"],
#ws-settings-form input[type="radio"] {
  display: inline-block;
  width: auto;
  padding: 0;
  border: none;
  border-radius: 0;
  flex: 0 0 auto;
}

/* Restores the flex layouts that #ws-settings-form label flattened. */
/* The same #ws-settings-form input override was also beating .mnw-input,
   which is why the wizard's Step 2 boxes kept the old 0.45rem padding and
   6px corners while its textareas (not matched by `input`) took the new
   ones. Half a restyle rendering is worse than none: the two sat side by
   side looking like different components. */
#ws-settings-form input.mnw-input,
#ws-settings-form input.num-search-input {
  padding: 0.7rem 0.8rem;
  font-size: 0.9rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.6rem;
}
#ws-settings-form input.mnw-input:focus {
  outline: none;
  border-color: var(--c-brand, #6366f1);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-brand, #6366f1) 15%, transparent);
}

#ws-settings-form label.mnw-agree,
#ws-settings-form label.num-toggle-row {
  display: flex;
  align-items: flex-start;
}

#ws-settings-form label.num-toggle-row { align-items: center; }
#ws-settings-form .mnw-checks label.num-toggle-row { align-items: flex-start; }

#ws-settings-form input:read-only,
#account-info-form input:read-only {
  background: var(--c-page, #f4f5f7);
  color: var(--c-text-3, #475569);
  cursor: default;
}

/* --- Workspace switcher --- */

.workspace-switcher {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.workspace-switcher select {
  padding: 0.35rem 0.6rem;
  border-radius: 6px;
  border: 1px solid var(--c-head-select-border, #4c3b7a);
  background: var(--c-head-select, #2a2050);
  color: var(--c-head-ink, #ffffff);
  font-size: 0.9rem;
  max-width: 220px;
}

/* Same single-class specificity as .nav-tab-btn above, but declared
   later in the file so these win on source order -- overrides its
   transparent background/plain border for this one icon-only button. */
.header-cog-btn {
  padding: 0.35rem 0.5rem;
  border-radius: 6px;
  border: 1px solid var(--c-head-border, #4b5563);
  background: var(--c-head-control, #27272a);
  color: var(--c-head-ink, #ffffff);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
}

.header-cog-btn:hover {
  background: var(--c-head-control-hover, #3f3f46);
}

/* Admin dropdown (Users + Admin Settings), positioned just right of the
   cog above -- reuses .user-menu-dropdown/.user-menu-item verbatim for
   the panel itself (see the username dropdown further down this file);
   only the wrapper/trigger need their own rules, styled to match the
   boxed cog/select look of its neighbors in .workspace-switcher rather
   than the plain-background username trigger on the opposite side of
   the header. */
.admin-menu {
  position: relative;
}

.admin-menu-trigger {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.35rem 0.5rem;
  border-radius: 6px;
  border: 1px solid var(--c-head-border, #4b5563);
  background: var(--c-head-control, #27272a);
  color: var(--c-head-ink, #ffffff);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
}


/* --- Links tab --- */

/* Links tab gets the full viewport width on desktop instead of main's
   usual 1280px cap -- the 3rd column below (the Linxi Card panel) needs
   the extra room, and the 2-column list+thread layout benefits from it
   too. Scoped to ONLY the Links tab via :has() so every other tab keeps
   the normal reading-width cap; degrades gracefully (stays at 1280px)
   in a browser without :has() support. */
/* ⚠️ AND NO PADDING AROUND IT. main's own 1.5rem is a reading margin
   for a column of cards; this tab is a full-height three-panel layout
   whose panels do their own padding, so the outer ring was a frame
   around a frame. Same :has() scope as the widen above, so every other
   tab keeps it.

   ⚠️ THE 1800px CAP STAYS. On a screen wider than that the layout is
   still centred by main's `margin: 0 auto` -- that is the existing
   decision recorded above, not padding, and removing it is a different
   change. */
main:has(> #tab-links:not(.hidden)) {
  max-width: 1800px;
  padding: 0;
}

.links-layout {
  display: grid;
  grid-template-columns: 300px 1fr 300px;
  gap: 0;
  padding: 0;
  overflow: hidden;
}

.links-list-panel {
  border-right: 1px solid var(--c-border, #e2e8f0);
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  overflow-y: auto;
}

/* --- Links tab: pseudo/qualified Linxi Card panel (3rd column) --- */

.links-card-panel {
  border-left: 1px solid var(--c-border, #e2e8f0);
  padding: 1rem;
  overflow-y: auto;
}

/* Smaller sibling of .linxicard-preview (the editor's own live preview)
   -- same gradient-card look, scaled down for a sidebar instead of a
   square hero card. */
.linxicard-mini {
  position: relative;
  border-radius: 16px;
  padding: 1.5rem 1.25rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  color: var(--c-on-fill, #ffffff);
  background: linear-gradient(160deg, var(--c-brand, #6366f1), var(--c-brand-strong, #4f46e5));
  box-shadow: 0 12px 30px -10px color-mix(in srgb, var(--c-brand-deep, #4338ca) 50%, transparent);
  overflow: hidden;
}

.linxicard-mini-avatar {
  width: 64px;
  height: 64px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.4rem;
  font-weight: 700;
  margin-bottom: 0.75rem;
  background: var(--c-surface, #ffffff);
  overflow: hidden;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-highlight, #ffffff) 25%, transparent);
}

.linxicard-mini-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.linxicard-mini-name {
  font-size: 1.1rem;
  font-weight: 800;
  word-break: break-word;
}

.linxicard-mini-username {
  font-size: 0.8rem;
  opacity: 0.85;
  margin-top: 0.1rem;
}

.links-card-verified-badge {
  font-size: 0.7rem;
  font-weight: 700;
  background: color-mix(in srgb, var(--c-highlight, #ffffff) 20%, transparent);
  border-radius: 999px;
  padding: 0.2rem 0.6rem;
  margin-top: 0.35rem;
}

.linxicard-mini-bio {
  font-size: 0.85rem;
  opacity: 0.92;
  margin-top: 0.6rem;
  line-height: 1.4;
}

/* ⚠️ overflow-wrap, not word-break: an email address or a phone number
   has no space to break at, and on a 300px column -- or a phone -- one
   long address is enough to set the tile's min-content width. `anywhere`
   rather than `break-word` so it also counts toward the intrinsic
   minimum, which is the half that stops it pushing the layout. */
.linxicard-mini-contact {
  font-size: 0.8rem;
  opacity: 0.85;
  margin-top: 0.3rem;
  overflow-wrap: anywhere;
}

.linxicard-mini-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  justify-content: center;
  margin-top: 0.85rem;
}

.linxicard-mini-tags .tag-pill {
  background: color-mix(in srgb, var(--c-highlight, #ffffff) 18%, transparent);
  color: var(--c-on-accent, #ffffff);
}

.links-card-import-panel {
  margin-top: 1rem;
  padding: 1rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 10px;
  background: var(--c-surface-subtle, #f8fafc);
}

/* ID selectors always outrank a plain .hidden class, regardless of
   source order -- these need the matching #id.hidden override or they
   can never actually hide (same underlying issue as
   .manage-organize-panel.hidden above, worse here since it's not even
   a source-order tiebreak). */
#links-search-panel.hidden,
#contacts-search-panel.hidden,
#create-organize-panel.hidden,
#upload-panel.hidden,
#new-campaign-panel.hidden {
  display: none;
}

#links-search-panel,
#contacts-search-panel {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin-top: 0.5rem;
}

#create-organize-panel {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  margin-top: 0.75rem;
}

#upload-panel {
  margin-top: 0.75rem;
}

#links-search {
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
  font-size: 0.9rem;
  width: 100%;
}

.links-bulk-toolbar {
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0;
}

.links-bulk-toolbar select {
  max-width: 110px;
}

.select-all-links {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.85rem;
  color: var(--c-text-soft, #555555);
}

.links-list {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.link-card {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  padding: 0.6rem;
  border-radius: 8px;
  cursor: pointer;
  position: relative;
}

.link-card-checkbox {
  margin: -5px 0 0 -5px;
  flex-shrink: 0;
}

.link-card:hover {
  background: var(--c-page, #f4f5f7);
}

.link-card.active {
  background: var(--c-brand-tint-3, #e0e7ff);
}

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

.link-card-name-row {
  display: flex;
  justify-content: space-between;
  gap: 0.5rem;
  font-size: 0.9rem;
  font-weight: 600;
}

.link-card-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

/* Shares .link-card-name-row with the name now, to its right (see
   renderLinksList) -- flex-shrink: 0 keeps the badge at its full size
   regardless of how much .link-card-name (min-width: 0, its own
   ellipsis-truncation) has to shrink to make room for it. */
.link-card-linxi-badge {
  display: inline-block;
  flex-shrink: 0;
  font-size: 0.65rem;
  font-weight: 700;
  background: var(--c-violet-tint-2, #ede9fe);
  color: var(--c-brand, #6366f1);
  border-radius: 999px;
  padding: 0.1rem 0.45rem;
  white-space: nowrap;
}

.link-card-preview {
  font-size: 0.85rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.unread-badge {
  position: absolute;
  top: 0.5rem;
  right: 0.4rem;
  background: var(--c-info, #2563eb);
  color: var(--c-on-fill, #ffffff);
  border-radius: 999px;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  font-size: 0.7rem;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}

.avatar-circle {
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: var(--c-brand, #6366f1);
  color: var(--c-on-fill, #ffffff);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 0.85rem;
  overflow: hidden;
}

.avatar-circle img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.avatar-large {
  width: 56px;
  height: 56px;
  font-size: 1.2rem;
}

.account-avatar-row {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-bottom: 1.25rem;
}

.account-avatar-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.account-avatar-actions .result {
  flex-basis: 100%;
  margin: 0.2rem 0 0;
}

/* No height cap/scroll of its own -- this panel grows to fit its
   content (nav + profile + reply box) naturally. .chat-thread below is
   the ONE scrollable region in here; capping this panel too used to
   create a second, outer scrollbar for the whole module (profile card
   included) on top of the thread's own, which is exactly what looked
   "oversized" and confusing on desktop. */
.links-detail-panel {
  padding: 1rem 1.5rem;
}

.link-detail-nav {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 1rem;
}

/* ⚠️ THE BARE .link-detail-org RULE IS GONE. It dates from when the
   organization was a direct child of .link-detail-nav, and every one of
   its five declarations was already being overridden -- font-weight and
   colour by .link-detail-name (a single class too, winning on SOURCE
   ORDER alone), the rest by .link-detail-identity .link-detail-org.
   A rule that only loses because it is written first is one reorder away
   from deciding the colour, which is the drift the shared class exists
   to prevent. See the identity block near the end of this file. */

.contact-profile-card {
  border-bottom: 1px solid var(--c-border-soft, #e5e7eb);
  padding-bottom: 1rem;
  margin-bottom: 1rem;
}

/* ⚠️ NO BORDER OR GROUND OF ITS OWN. This briefly had both, from a
   reading where THIS was "the tile" -- it is not. The tile is
   #links-card-visual, the Linxi Label mini card, and this whole record
   now sits directly beneath it in the same column. A boxed block
   immediately inside another boxed block reads as a mistake. */
.profile-header {
  display: flex;
  align-items: flex-start;
  flex-wrap: wrap;
  gap: 0.75rem;
}

/* ⚠️ A SIBLING OF .profile-header, NOT A CHILD -- see the markup.
   flex-basis: 100% only ever moved these to their own row INSIDE the
   tile; taking them out of it is what puts them under it. */
.profile-header-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.75rem;
}

/* ⚠️ .contact-profile-fields AND .profile-field ARE GONE. They styled
   the phone/email grid under the tile, and the tile itself prints both
   -- #links-card-phone / #links-card-email -- so the pair below it was
   a second copy of one contact detail with an editable box round it.
   Nothing in any page matches either class now; .profile-header stays
   because public/profile.html has one of its own. */

/* ⚠️ .contact-profile-card.editing AND .group-pill-remove ARE GONE with
   the mode that put them on screen. Edit Profile opens the contact sheet
   now, so nothing ever adds `editing` to this card and no pill ever
   carries a x -- both rules were waiting for a class and an element that
   can no longer exist. */

/* ⚠️ .contact-groups, not .contact-groups-tags. The tag pills that
   shared this row are gone -- the tile above draws tags -- so a name
   promising both would describe a row that holds one. */
.contact-groups {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-top: 0.75rem;
}

.group-pills {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
}

/* ⚠️ A GROUP IS A BOX, A TAG IS A CAPSULE. These two rules were
   byte-identical apart from the background -- .tag-pill fills with
   --c-brand, .group-pill takes its colour inline from the group's own
   row -- so on a panel showing both, the only thing telling them apart
   was a colour the user picks and can set to anything. The radius is
   what carries the difference now: 4px reads as a rounded rectangle
   against .tag-pill's 999px stadium, at any colour and at this size.

   ⚠️ SQUARER IN BOTH SENSES. The corners, and the proportion: 0.15rem of
   vertical padding makes a flat capsule, which still reads as a tag
   however the corners are cut.

   ⚠️ DO NOT "UNIFY" THESE BY MOVING .tag-pill. It is rendered on the
   PUBLIC Linxi Label (public/label.html, publicCard.js) and in the
   Contacts tab's CSV import, none of which know about this panel --
   changing it to match would restyle three surfaces to fix one. */
.group-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  color: var(--c-on-accent, #ffffff);
  border-radius: 4px;
  padding: 0.25rem 0.5rem 0.25rem 0.55rem;
  font-size: 0.75rem;
  font-weight: 600;
}

.color-swatch {
  display: inline-block;
  width: 16px;
  height: 16px;
  border-radius: 4px;
  vertical-align: middle;
}

.dynamic-fields {
  margin-top: 0.75rem;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.4rem;
}

.dynamic-field-row label {
  display: block;
  font-size: 0.7rem;
  color: var(--c-text-muted, #64748b);
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

.dynamic-field-row input {
  width: 100%;
  padding: 0.3rem 0.5rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 6px;
  font-size: 0.85rem;
}

.dynamic-field-row input[readonly] {
  border-color: transparent;
  background: transparent;
  cursor: default;
}

.chat-thread {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  min-height: 200px;
  max-height: 45vh;
  overflow-y: auto;
  padding: 0.5rem 0;
}

.date-separator {
  text-align: center;
  font-size: 0.75rem;
  color: var(--c-text-faint, #999999);
  margin: 0.5rem 0;
}

.chat-row {
  display: flex;
  flex-direction: column;
  max-width: 70%;
}

.chat-outbound {
  align-self: flex-end;
  align-items: flex-end;
}

.chat-inbound {
  align-self: flex-start;
  align-items: flex-start;
}

.chat-bubble {
  padding: 0.5rem 0.75rem;
  border-radius: 12px;
  font-size: 0.9rem;
  white-space: pre-wrap;
  word-break: break-word;
}

.chat-bubble-outbound {
  background: var(--c-info, #2563eb);
  color: var(--c-on-fill, #ffffff);
  border-bottom-right-radius: 2px;
}

.chat-bubble-inbound {
  background: var(--c-border-soft, #e5e7eb);
  color: var(--c-ink, #1a1a1a);
  border-bottom-left-radius: 2px;
}

.chat-meta {
  font-size: 0.7rem;
  margin-top: 0.2rem;
  display: flex;
  align-items: center;
  gap: 0.3rem;
}

.status-icon {
  border: none;
  background: none;
  padding: 0;
  font-size: 0.8rem;
  cursor: default;
  color: inherit;
}

.status-icon.status-failed {
  cursor: pointer;
}

.reply-box {
  margin-top: 0.75rem;
  border-top: 1px solid var(--c-border-soft, #e5e7eb);
  padding-top: 0.75rem;
}

/* ── The message field ────────────────────────────────────────────────
 *
 * ⚠️ ITS COLOURS, ITS BORDER, ITS RADIUS AND ITS FOCUS RING ALL COME
 * FROM THE BASE RULE in the reset ("Text-entry controls, everywhere").
 * This box is where the missing-background bug was reported and it was
 * fixed here first, with its own background, its own ring and its own
 * `outline: none`. Then the same fix was needed everywhere, and a local
 * copy of a site-wide look is the fork where the two drift -- one gets a
 * new focus colour and the other does not, and nobody notices until the
 * two are on screen together.
 *
 * ⚠️ WHAT IS LEFT IS WHAT IS GENUINELY LOCAL: a composer is taller than
 * a one-line field and wants room to breathe. Nothing here restates a
 * colour.
 */
/* ⚠️ resize: none, AND THAT IS A REAL LOSS. The grip a browser draws
   for a resizable textarea sits in the bottom-RIGHT corner, which is
   exactly where the send button now is -- the button wins every drag,
   so the handle would be there and unusable. Stated rather than left to
   be discovered; auto-growing the field is the way to give it back, and
   is its own change. */
.reply-box textarea {
  width: 100%;
  min-height: 2.9rem;
  /* ⚠️ The right padding is the button's room. Without it the last line
     of a long message runs underneath Send. */
  padding: 0.6rem 5.25rem 0.6rem 0.75rem;
  font-size: 0.9rem;
  line-height: 1.5;
  resize: none;
  display: block;
}

/* ⚠️ A BUTTON CANNOT BE A CHILD OF A <textarea>, so the two are siblings
   in a relative box and the button is placed over the field's own
   padding. */
.reply-input-wrap {
  position: relative;
}

/* ⚠️ THE MARGIN FLIPS WITH THE ROW. .channel-toggle carries
   margin-bottom: 0.5rem, which was the gap to whatever it sat above;
   under the message box that gap is now on the wrong side and the
   picker hugs the field it belongs to while floating away from the
   counter below it. */
#reply-channel-toggle {
  margin-top: 0.5rem;
  margin-bottom: 0.25rem;
}

.reply-send-inline {
  position: absolute;
  right: 0.4rem;
  bottom: 0.4rem;
  padding: 0.35rem 0.85rem;
  font-size: 0.85rem;
}

/* ⚠️ .reply-box-footer IS GONE with the counter it held. It existed to
   put the counter and the Send button on one row; Send moved into the
   field, then the counter was removed, and what was left was a flex row
   laying out nothing. */
/* --- Compose A2P warning banner --- */

.warning-banner {
  background: var(--c-warning-tint-2, #fef3c7);
  border: 1px solid var(--c-warning, #f59e0b);
  color: var(--c-warning-text, #92400e);
  border-radius: 6px;
  padding: 0.6rem 0.8rem;
  font-size: 0.85rem;
  margin-top: 0.75rem;
}

/* --- Setup Wizard --- */

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
}

.wizard-modal {
  max-width: 560px;
  position: relative;
  padding-top: 2.25rem;
}/* Same look as .wizard-close-x, under a generic name for reuse outside
   the setup wizard (currently the webmail compose modal). */
.modal-close-x {
  position: absolute;
  top: 0.75rem;
  right: 0.75rem;
  background: transparent;
  color: var(--c-text-muted, #64748b);
  border: none;
  font-size: 1.3rem;
  line-height: 1;
  padding: 0.2rem 0.5rem;
  cursor: pointer;
}

.modal-close-x:hover {
  color: var(--c-ink, #1a1a1a);
}

/* Scoped to this one modal (not a blanket .modal rule) so no other
   modal's layout is affected -- position: relative is what lets
   .modal-close-x anchor to this box instead of the nearest other
   positioned ancestor, and the extra top padding keeps the X from
   overlapping the title. */
#webmail-compose-modal-overlay .modal,
#email-signature-modal-overlay .modal {
  position: relative;
  padding-top: 2.25rem;
}

.success-badge {
  background: var(--c-success-tint, #f0fdf4);
  border: 1px solid var(--c-success-border, #bbf7d0);
  color: var(--c-success-text, #166534);
  padding: 0.75rem 1rem;
  border-radius: 8px;
  font-size: 0.875rem;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.75rem;
}

/* ── My <brand> Email ──────────────────────────────────────────────
   One mailbox card per address, a gate that looks like a gate, and an
   address field that reads as one control rather than a box beside some
   text. Everything here stacks at 640px; see the block at the end. */

.email-account-card {
  border: 1px solid var(--c-border-soft, #e5e7eb);
  border-radius: 10px;
  padding: 0.85rem 1rem;
  margin-bottom: 0.75rem;
  background: var(--c-surface, #ffffff);
}

.email-account-card:last-child {
  margin-bottom: 0;
}

/* The stack of mailbox cards. `:empty` rather than a class toggled from
   JS: with no addresses the div is genuinely empty, and a margin under
   nothing pushes the form down for no reason. */
.email-account-list:not(:empty) { margin-bottom: 1.1rem; }

.email-account-head {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-bottom: 0.65rem;
}

/* ⚠️ TRUNCATES RATHER THAN WRAPPING. An address is one token with no
   break opportunities, so a long one on a narrow card either wraps
   mid-word or pushes the card wider than the panel. `min-width: 0` is
   what lets a flex child shrink below its content at all. */
.email-account-addr {
  font-weight: 700;
  font-size: 0.95rem;
  min-width: 0;
  flex: 1 1 12rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.email-account-badges {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  flex-wrap: wrap;
}

/* ⚠️ A GRID THAT WRAPS, not a single row. Six buttons in a .table-toolbar
   laid out as one unbroken line and squeezed to unreadable widths on a
   phone. `auto-fit` + `minmax` gives each one a floor and lets the row
   break when it is reached. */
.email-account-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem;
}

/* Reading the mail is the act; the rest is maintenance. */
.email-account-primary-action { margin-right: auto; }

/* ── the verify gate ─────────────────────────────────────────────── */

.email-verify { margin-top: 0; }

.email-verify-main {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
}

.email-verify-icon { font-size: 1.35rem; line-height: 1.2; }
.email-verify-copy { min-width: 0; }

/* The address the code is going to. Set apart so somebody can check it
   at a glance -- it is the whole question the banner is asking. */
.email-verify-addr {
  font-weight: 700;
  word-break: break-all;
}

.email-verify-aside {
  font-size: 0.76rem;
  opacity: 0.85;
}

.email-verify-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-top: 0.7rem;
}

.email-verify-actions .input-actions { max-width: 16rem; }

.email-verified-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin: 0 0 0.75rem;
  padding: 0.25rem 0.6rem;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--c-success, #16a34a);
  background: var(--c-success-tint, #f0fdf4);
  border: 1px solid var(--c-success-border, #bbf7d0);
  border-radius: 2rem;
}

/* ── no mailbox yet ──────────────────────────────────────────────── */

.email-empty {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 1rem 1.1rem;
  margin-bottom: 1rem;
  border: 1px dashed var(--c-border, #e2e8f0);
  border-radius: 10px;
  background: var(--c-surface-subtle, #f8fafc);
}

.email-empty-icon { font-size: 1.5rem; }
.email-empty p { margin: 0; font-size: 0.85rem; color: var(--c-text-muted, #64748b); }

.email-limit { margin-top: 0; margin-bottom: 1rem; }

/* ── the add form ────────────────────────────────────────────────── */

.email-add { margin-top: 1.25rem; }
.email-add h3 { margin: 0 0 0.75rem; font-size: 0.95rem; }

/* ⚠️ THE PREFIX AND THE DOMAIN ARE ONE ADDRESS. They were an input and a
   bare span side by side, so on a phone the domain wrapped onto its own
   line under a box that looked complete without it. The border lives on
   the wrapper and the input gives up its own. */
.email-addr-field {
  display: flex;
  align-items: stretch;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 8px;
  background: var(--c-surface, #ffffff);
  overflow: hidden;
  max-width: 28rem;
}

.email-addr-field:focus-within {
  border-color: var(--c-brand, #6366f1);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-brand, #6366f1) 18%, transparent);
}

.email-addr-field input {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
}

.email-addr-field input:focus { outline: none; box-shadow: none; }

.email-addr-domain {
  display: flex;
  align-items: center;
  padding: 0 0.7rem;
  font-size: 0.85rem;
  white-space: nowrap;
  color: var(--c-text-muted, #64748b);
  background: var(--c-surface-subtle, #f1f5f9);
  border-left: 1px solid var(--c-border, #e2e8f0);
}

.email-blocked {
  margin: 0.5rem 0 0;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--c-warning-text, #b45309);
}

/* A gated form reads as deliberately waiting, not as broken. */
.email-add.is-gated .email-addr-field { opacity: 0.6; }

.email-add #email-create-btn { margin-top: 0.85rem; }

.email-reveal { margin-top: 1rem; }
.email-reveal .input-actions { max-width: 24rem; margin-bottom: 0.6rem; }

/* ── the same panel on a phone ────────────────────────────────────────
   ⚠️ 640px, the breakpoint the rest of this stylesheet already uses.
   A second one three pixels away is how two rules end up disagreeing
   about what "mobile" means. */
@media (max-width: 640px) {
  /* Every action full width and stacked. Six buttons on one line at
     360px gave each of them about 50px -- a row of unreadable slivers,
     which is what "wraps" was hiding. */
  .email-account-actions {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.45rem;
  }

  .email-account-actions button,
  .email-account-actions .btn {
    width: 100%;
    margin: 0;
  }

  /* ⚠️ The primary action and the test result each take the FULL row.
     margin-right:auto does nothing in a grid, and a status line sharing
     a cell with a button is unreadable at this width. */
  .email-account-primary-action,
  .email-account-actions .email-test-result {
    grid-column: 1 / -1;
  }

  .email-account-actions .email-test-result { text-align: center; }

  .email-account-addr { flex-basis: 100%; }

  /* The address field takes the width it has; a max-width meant for a
     desktop column leaves a phone with a short box and a wide gap. */
  .email-addr-field { max-width: none; }

  /* ⚠️ The domain can be LONGER than the prefix box on a narrow screen
     (@sensorium.events is 18 characters). Let it shrink and ellipsis
     rather than squeezing the input it belongs to down to nothing --
     the part being typed is the part that needs the room. */
  .email-addr-domain {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    display: block;
    padding: 0.55rem 0.6rem;
  }

  .email-verify-main { gap: 0.6rem; }
  .email-verify-icon { font-size: 1.15rem; }

  .email-verify-actions,
  .email-verify-actions .input-actions {
    max-width: none;
    width: 100%;
  }

  .email-verify-actions > button { width: 100%; }

  .email-empty { flex-direction: column; text-align: center; gap: 0.5rem; }

  .email-reveal .input-actions { max-width: none; }
}

.email-test-result {
  font-size: 0.8rem;
  align-self: center;
}

.email-test-result.success {
  color: var(--c-success, #16a34a);
}

.email-test-result.error {
  color: var(--c-danger-strong, #dc2626);
}

/* Brief highlight on the Reset Password button when a connection test
   fails specifically because the stored password no longer works --
   points at the one actual fix instead of leaving the user to guess
   which of the row's several buttons addresses it. */
.email-action-nudge {
  box-shadow: 0 0 0 2px var(--c-warning, #f59e0b);
}

/* Wider than the default .modal (400px) -- a rich text compose box reads
   cramped at that width. Same specificity as .modal's own max-width, so
   source order (this rule comes after) is what makes it win.
   max-height/overflow-y is what was actually missing before: with no
   cap, the From select + To + Cc + Subject + Quill editor (its own
   min-height: 220px) + signature preview + buttons could add up to
   taller than a phone's viewport with no way to scroll down to Send --
   same fix already applied to .new-contact-modal below. */
.webmail-compose-modal {
  max-width: 960px;
  max-height: 90vh;
  overflow-y: auto;
}

/* Fields (From/To/Cc/Bcc/Subject) in a narrower left column, editor in
   a wider right column -- on the old single-column layout the editor
   was stuck at the same cramped width as every text input above it.
   Collapses back to a single stacked column under the breakpoint below. */
.webmail-compose-grid {
  display: grid;
  grid-template-columns: 260px 1fr;
  gap: 1.5rem;
  margin-top: 0.75rem;
}

@media (max-width: 700px) {
  .webmail-compose-grid {
    grid-template-columns: 1fr;
    gap: 0;
  }
}

/* "From" label + account <select> on one line instead of the label
   stacking above it like every other .modal label does by default. */
.webmail-compose-from-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0 0 0.3rem;
}

/* ID-scoped so this reliably beats .modal label's own margin regardless
   of source order -- same equal-specificity/source-order-wins issue
   documented elsewhere in this file, sidestepped the same way. */
#webmail-compose-modal-overlay .webmail-compose-from-row label {
  margin: 0;
  flex-shrink: 0;
}

.webmail-compose-from-row select {
  flex: 1;
  min-width: 0;
}

/* Condenses To/Cc/Bcc into a single row + toggle (Gmail-style) instead
   of Cc always taking its own permanently-visible label+input --
   mobile-first reasoning (see the ask), but kept the same on desktop
   too rather than showing it two different ways at two widths. */
.webmail-compose-to-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

.webmail-compose-ccbcc-toggle {
  background: transparent;
  border: none;
  color: var(--c-brand, #6366f1);
  font-size: 0.8rem;
  font-weight: 600;
  padding: 0.2rem 0.3rem;
  cursor: pointer;
}

.webmail-compose-ccbcc-toggle:hover {
  text-decoration: underline;
}

/* Wider than the default .modal (400px) -- with three checkbox-pill
   lists (lists/tags/groups) plus any custom fields, the narrow default
   made this modal stack into something taller than the viewport on
   desktop. Same source-order-wins-at-equal-specificity reasoning as
   .webmail-compose-modal above. */
.new-contact-modal {
  max-width: 600px;
  max-height: 90vh;
  overflow-y: auto;
}

/* Quill renders its own toolbar + .ql-container/.ql-editor inside
   whichever element it's initialized on (#webmail-compose-editor,
   #email-signature-editor) -- these size the editable area itself since
   Quill's own defaults are too short for either use case. */
.webmail-ql-editor .ql-editor {
  min-height: 220px;
  max-height: 420px;
  overflow-y: auto;
  font-size: 0.9rem;
}

.webmail-ql-editor-signature .ql-editor {
  min-height: 90px;
  max-height: 220px;
}

.webmail-signature-preview {
  margin-top: 0.75rem;
  padding: 0.6rem 0.75rem;
  border: 1px dashed var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
  background: var(--c-surface-subtle, #f8fafc);
}

.webmail-signature-preview p.muted {
  margin: 0 0 0.4rem;
  font-size: 0.75rem;
}.wizard-progress-step.active .wizard-progress-dot {
  background: var(--c-info, #2563eb);
  color: var(--c-on-accent, #ffffff);
}

.wizard-progress-step.completed .wizard-progress-dot {
  background: var(--c-success, #16a34a);
  color: var(--c-on-fill, #ffffff);
}.wizard-progress-step.active .wizard-progress-label {
  color: var(--c-ink, #1a1a1a);
  font-weight: 600;
}.wizard-step-panel.active {
  display: block;
}.password-field,
.input-actions {
  display: flex;
  gap: 0.4rem;
}

.password-field input,
.input-actions input {
  flex: 1;
}

.wizard-note {
  margin-top: 0.75rem;
  font-size: 0.8rem;
}.wizard-verify-buttons {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.75rem;
  margin-top: 1.25rem;
}

.wizard-verify-col button {
  width: 100%;
}

.wizard-verify-result {
  margin-top: 0.5rem;
  font-size: 0.8rem;
  min-height: 1.2em;
}

.wizard-result-ok {
  color: var(--c-success, #16a34a);
  font-weight: 600;
}

.wizard-result-fail {
  color: var(--c-danger-strong, #dc2626);
  font-weight: 600;
}

.wizard-checklist-item {
  padding: 0.15rem 0;
}

.wizard-checklist-item.ok {
  color: var(--c-success, #16a34a);
}

.wizard-checklist-item.warn {
  color: var(--c-warning-text, #92400e);
}

.wizard-checklist-item.fail {
  color: var(--c-danger-strong, #dc2626);
}

.wizard-checklist-item a {
  color: inherit;
  text-decoration: underline;
}

.spinner {
  display: inline-block;
  width: 12px;
  height: 12px;
  border: 2px solid var(--c-border-strong, #cbd5e1);
  border-top-color: var(--c-info, #2563eb);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
  vertical-align: middle;
  margin-right: 0.3rem;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}.a2p-badge {
  display: inline-block;
  border-radius: 999px;
  padding: 0.1rem 0.6rem;
  font-size: 0.75rem;
}

.a2p-badge.a2p-approved {
  background: var(--c-success-tint-2, #dcfce7);
  color: var(--c-success-text, #166534);
}

.a2p-badge.a2p-processing,
.a2p-badge.a2p-pending {
  background: var(--c-warning-tint-2, #fef3c7);
  color: var(--c-warning-text, #92400e);
}

.a2p-badge.a2p-unregistered {
  background: var(--c-danger-tint-2, #fee2e2);
  color: var(--c-danger-darkest, #991b1b);
}

.a2p-badge.a2p-unknown {
  background: var(--c-border-soft, #e5e7eb);
  color: var(--c-text-2, #334155);
}

.a2p-status-section {
  margin-top: 1.25rem;
}

.a2p-status-section h4 {
  font-size: 0.9rem;
  font-weight: 600;
  margin: 0 0 0.5rem;
}

.a2p-status-panel {
  margin-top: 0.75rem;
  padding: 0.75rem 1rem;
  border-radius: 8px;
  font-size: 0.85rem;
  border: 1px solid transparent;
}

.a2p-status-panel .a2p-status-headline {
  font-weight: 700;
  margin-bottom: 0.3rem;
}

.a2p-status-panel a {
  color: inherit;
  text-decoration: underline;
}

/* Green -- fully approved, Standard registration: no meaningful limits. */
.a2p-status-panel.a2p-status-approved {
  background: var(--c-success-tint-2, #dcfce7);
  border-color: var(--c-success, #16a34a);
  color: var(--c-success-text, #166534);
}

/* Amber -- approved but Sole Proprietor (personal-use limits apply), or
   partially configured (in a Messaging Service, brand not yet approved).
   Deliberately distinct from the plain green above, not a lighter shade
   of it -- these are both "not fully cleared" states even though one of
   them is technically "approved". */
.a2p-status-panel.a2p-status-sole-proprietor,
.a2p-status-panel.a2p-status-partial {
  background: var(--c-warning-tint-2, #fef3c7);
  border-color: var(--c-warning-strong, #d97706);
  color: var(--c-warning-text, #92400e);
}

/* Orange -- brand registration submitted, still under Twilio/carrier review. */
.a2p-status-panel.a2p-status-processing {
  background: var(--c-warning-border, #fde68a);
  border-color: var(--c-orange, #ea580c);
  color: var(--c-warning-text, #92400e);
}

/* Red -- number isn't in any Messaging Service at all. */
.a2p-status-panel.a2p-status-unregistered {
  background: var(--c-danger-tint-2, #fee2e2);
  border-color: var(--c-danger-strong, #dc2626);
  color: var(--c-danger-darkest, #991b1b);
}

/* Gray -- the check itself failed/couldn't determine status. */
.a2p-status-panel.a2p-status-unknown {
  background: var(--c-surface-sunken, #f1f5f9);
  border-color: var(--c-text-dim, #94a3b8);
  color: var(--c-text-2, #334155);
}

/* Brand approved, Campaign still IN_PROGRESS -- distinct yellow/amber
   from a2p-status-processing's orange (brand itself pending) above, so
   "brand pending" and "campaign pending" don't read as the same state. */
.a2p-status-panel.a2p-status-campaign-pending {
  background: var(--c-warning-tint-2, #fef3c7);
  border-color: var(--c-warning-strong, #d97706);
  color: var(--c-warning-text, #92400e);
}

/* Brand approved, Campaign FAILED or never created -- same red urgency
   as a2p-status-unregistered, since sending is equally blocked either
   way once you're at this point. */
.a2p-status-panel.a2p-status-campaign-failed {
  background: var(--c-danger-tint-2, #fee2e2);
  border-color: var(--c-danger-strong, #dc2626);
  color: var(--c-danger-darkest, #991b1b);
}

/* Brand approved, Campaign SUSPENDED by a carrier post-approval. */
.a2p-status-panel.a2p-status-campaign-suspended {
  background: var(--c-warning-tint-2, #fef3c7);
  border-color: var(--c-warning-strong, #d97706);
  color: var(--c-warning-text, #92400e);
}

/* Number is in a Messaging Service, but no Campaign exists under it at
   all (so its Brand can't even be identified) -- same red urgency as
   a2p-status-campaign-failed, since sending is equally blocked. */
.a2p-status-panel.a2p-status-no-campaign {
  background: var(--c-danger-tint-2, #fee2e2);
  border-color: var(--c-danger-strong, #dc2626);
  color: var(--c-danger-darkest, #991b1b);
}

.a2p-status-panel .a2p-status-overall {
  font-weight: 600;
  margin-top: 0.4rem;
}

/* --- Compliance profile (src/routes/compliance.js, public/js/complianceWizard.js) --- */
/* Same shape/palette convention as .a2p-status-panel above -- distinct
   status vocabulary (draft/pending/approved/rejected/failed), so its own
   class family rather than reusing a2p's. */

.compliance-status-panel {
  margin-top: 0.75rem;
  padding: 0.75rem 1rem;
  border-radius: 8px;
  font-size: 0.85rem;
  border: 1px solid transparent;
}

.compliance-status-panel .compliance-status-headline {
  font-weight: 700;
  margin-bottom: 0.3rem;
}

.compliance-status-panel.compliance-status-draft {
  background: var(--c-surface-sunken, #f1f5f9);
  border-color: var(--c-text-dim, #94a3b8);
  color: var(--c-text-2, #334155);
}

.compliance-status-panel.compliance-status-pending {
  background: var(--c-warning-border, #fde68a);
  border-color: var(--c-orange, #ea580c);
  color: var(--c-warning-text, #92400e);
}

.compliance-status-panel.compliance-status-approved {
  background: var(--c-success-tint-2, #dcfce7);
  border-color: var(--c-success, #16a34a);
  color: var(--c-success-text, #166534);
}

.compliance-status-panel.compliance-status-rejected,
.compliance-status-panel.compliance-status-failed {
  background: var(--c-danger-tint-2, #fee2e2);
  border-color: var(--c-danger-strong, #dc2626);
  color: var(--c-danger-darkest, #991b1b);
}

.compliance-wizard-modal {
  max-width: 560px;
}

.compliance-wizard-review-summary {
  margin: 0.75rem 0;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 8px;
  overflow: hidden;
}

.compliance-wizard-review-row {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.5rem 0.75rem;
  font-size: 0.85rem;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
}

.compliance-wizard-review-row:last-child {
  border-bottom: none;
}

.compliance-wizard-review-label {
  font-weight: 600;
  color: var(--c-text-muted, #64748b);
  flex-shrink: 0;
}

/* --- Number Setup progress tracker (public/js/numberSetup.js) --- */

.number-setup-complete-box {
  background: var(--c-success-tint, #f0fdf4);
  border: 1px solid var(--c-success-border, #bbf7d0);
  border-radius: 0.75rem;
  padding: 1.25rem;
  text-align: center;
}

.number-setup-complete-icon {
  font-size: 2rem;
  margin-bottom: 0.5rem;
}

.number-setup-complete-box h4 {
  color: var(--c-success-text, #166534);
  margin: 0 0 0.25rem;
}

.number-setup-complete-box p {
  color: var(--c-success-text, #166534);
  margin: 0;
  font-size: 0.875rem;
}

.number-setup-step {
  display: flex;
  gap: 0.75rem;
  padding: 0.75rem;
  border-radius: 0.5rem;
  margin-bottom: 0.5rem;
  background: var(--c-surface-subtle, #f8fafc);
  border: 1px solid var(--c-border, #e2e8f0);
}

.number-setup-step-available {
  background: var(--c-brand-tint, #f5f3ff);
  border-color: var(--c-brand-border, #c7d2fe);
}

.number-setup-step-locked {
  opacity: 0.55;
}

.number-setup-step-icon {
  font-size: 1.25rem;
  flex-shrink: 0;
}

.number-setup-step-title {
  font-weight: 600;
  font-size: 0.875rem;
  color: var(--c-text, #0f172a);
}

.number-setup-step-desc {
  font-size: 0.8rem;
  color: var(--c-text-muted, #64748b);
}

.number-setup-step-detail {
  font-size: 0.75rem;
  color: var(--c-brand, #6366f1);
  margin-top: 0.2rem;
  word-break: break-word;
}

.number-setup-step-action {
  margin-top: 0.5rem;
}

/* --- A2P Brand wizard (public/js/numberSetup.js) -- selected-card state
   for the Brand Type step, and review-screen section headers with Edit
   links, reusing .number-wizard-choice-card/.compliance-wizard-review-row
   for everything else. --- */

.number-wizard-choice-card-selected {
  border-color: var(--c-brand, #6366f1);
  background: var(--c-brand-tint, #f5f3ff);
}

.brand-wizard-review-section-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: var(--c-surface-subtle, #f8fafc);
  padding: 0.5rem 0.75rem;
  font-weight: 700;
  font-size: 0.8rem;
  color: var(--c-text, #0f172a);
  border-bottom: 1px solid var(--c-border, #e2e8f0);
}

.brand-wizard-review-section-title:not(:first-child) {
  border-top: 1px solid var(--c-border, #e2e8f0);
}

.brand-wizard-edit-link {
  background: none;
  border: none;
  color: var(--c-brand, #6366f1);
  font-size: 0.78rem;
  font-weight: 600;
  cursor: pointer;
  padding: 0;
}

.brand-wizard-edit-link:hover {
  text-decoration: underline;
}

/* --- A2P Campaign submission tracker (public/js/numberSetup.js) --- */

.campaign-submissions-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
}

.campaign-submissions-header h2 {
  margin: 0;
}

.campaign-submissions-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.85rem;
  margin-top: 0.75rem;
}

.campaign-submissions-table th {
  text-align: left;
  padding: 0.35rem 0.5rem;
  font-size: 0.72rem;
  color: var(--c-text-dim, #94a3b8);
  text-transform: uppercase;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
}

.campaign-submissions-table td {
  padding: 0.5rem;
  border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9);
  vertical-align: top;
}

.campaign-submission-status-cell {
  text-transform: capitalize;
}

.campaign-submission-rejection-reason {
  font-size: 0.75rem;
  color: var(--c-danger-strong, #dc2626);
  margin-top: 0.2rem;
  text-transform: none;
}

.campaign-submission-action-btn {
  padding: 0.25rem 0.65rem;
  font-size: 0.75rem;
  margin-right: 0.35rem;
}

.campaign-submission-view-fields {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  margin: 1rem 0;
}

.campaign-submission-view-field-label {
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--c-text-dim, #94a3b8);
  text-transform: uppercase;
  margin-bottom: 0.2rem;
}

.campaign-submission-view-field-value {
  font-size: 0.875rem;
  color: var(--c-text, #0f172a);
  white-space: pre-wrap;
}

.campaign-draft-indicator {
  align-self: center;
  color: var(--c-success-alt, #10b981);
}

/* --- A2P Campaign wizard (public/js/numberSetup.js) --- */

.campaign-wizard-tip {
  background: var(--c-info-tint, #eff6ff);
  border: 1px solid var(--c-info-border, #bfdbfe);
  border-radius: 8px;
  padding: 0.6rem 0.85rem;
  font-size: 0.8rem;
  color: var(--c-info-text, #1d4ed8);
  margin: 0.6rem 0;
}

.campaign-optin-methods {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin: 0.75rem 0;
}

.campaign-optin-method > label {
  font-weight: 600;
  font-size: 0.9rem;
}

.campaign-optin-requirements {
  margin: 0.4rem 0 0.6rem 1.5rem;
  padding: 0.6rem 0.85rem;
  background: var(--c-surface-subtle, #f8fafc);
  border: 1px solid var(--c-border, #e2e8f0);
  border-left: 3px solid var(--c-brand, #6366f1);
  border-radius: 6px;
  font-size: 0.8rem;
}

.campaign-optin-requirements-title {
  font-weight: 600;
  margin-bottom: 0.3rem;
  color: var(--c-text, #0f172a);
}

.campaign-optin-requirements ul {
  margin: 0 0 0.4rem;
  padding-left: 1.1rem;
}

.campaign-optin-requirements li {
  margin-bottom: 0.15rem;
  color: var(--c-text-3, #475569);
}

.campaign-check-result {
  margin: 0.75rem 0;
  padding: 0.75rem 1rem;
  border-radius: 8px;
  font-size: 0.85rem;
  border: 1px solid transparent;
}

.campaign-check-pass {
  background: var(--c-success-tint-2, #dcfce7);
  border-color: var(--c-success, #16a34a);
  color: var(--c-success-text, #166534);
}

.campaign-check-fail {
  background: var(--c-danger-tint-2, #fee2e2);
  border-color: var(--c-danger-strong, #dc2626);
  color: var(--c-danger-darkest, #991b1b);
}

.campaign-check-error-line {
  margin-top: 0.25rem;
}

.campaign-check-warning-line {
  margin-top: 0.25rem;
  opacity: 0.85;
}

/* --- Twilio field help popovers (public/js/workspaceSettings.js) --- */

.field-help-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.1rem;
  height: 1.1rem;
  border-radius: 50%;
  border: 1px solid var(--c-text-dim, #94a3b8);
  background: var(--c-surface-sunken, #f1f5f9);
  color: var(--c-text-3, #475569);
  font-size: 0.68rem;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  padding: 0;
  vertical-align: middle;
  margin-left: 0.3rem;
}

.field-help-btn:hover {
  background: var(--c-brand, #6366f1);
  border-color: var(--c-brand, #6366f1);
  color: var(--c-on-fill, #ffffff);
}

.field-help-popover {
  position: fixed;
  z-index: 10001;
  width: 320px;
  max-width: calc(100vw - 24px);
  max-height: 80vh;
  overflow-y: auto;
  background: var(--c-inverse-surface-2, #1e293b);
  color: var(--c-chrome-text, #cbd5e1);
  border-radius: 10px;
  box-shadow: 0 12px 32px color-mix(in srgb, var(--c-shadow, #000000) 35%, transparent);
  padding: 1rem;
  font-size: 0.85rem;
}

.field-help-popover-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0.5rem;
}

.field-help-popover-header h4 {
  margin: 0;
  font-size: 0.95rem;
  color: var(--c-on-accent, #ffffff);
}

.field-help-popover-close {
  background: none;
  border: none;
  color: var(--c-text-dim, #94a3b8);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
}

.field-help-popover-close:hover {
  color: var(--c-on-accent, #ffffff);
}

.field-help-popover-desc {
  margin: 0 0 0.75rem;
  color: var(--c-chrome-text, #cbd5e1);
  line-height: 1.4;
}

.field-help-popover-steps {
  margin: 0 0 0.75rem;
  padding-left: 1.2rem;
  color: var(--c-chrome-text, #cbd5e1);
  line-height: 1.6;
}

.field-help-popover-steps strong {
  color: var(--c-on-dark-brand, #a5b4fc);
}

.field-help-popover-example {
  background: var(--c-inverse-surface, #0f172a);
  border-radius: 6px;
  padding: 0.5rem 0.7rem;
  margin-bottom: 0.75rem;
  font-size: 0.8rem;
}

.field-help-popover-example-label {
  color: var(--c-text-dim, #94a3b8);
}

.field-help-popover-example code {
  color: var(--c-on-dark-success, #6ee7b7);
}

.field-help-popover-note {
  border-radius: 0 8px 8px 0;
  padding: 0.75rem;
  margin-bottom: 0.75rem;
  font-size: 0.78rem;
  color: var(--c-text-dim, #94a3b8);
  line-height: 1.5;
}

.field-help-popover-note-info {
  background: var(--c-inverse-surface-2, #1e293b);
  border-left: 3px solid var(--c-brand, #6366f1);
}

.field-help-popover-note-warning {
  background: color-mix(in srgb, var(--c-warning, #f59e0b) 22%, var(--c-inverse-surface, #0f172a));
  border-left: 3px solid var(--c-warning, #f59e0b);
}

.field-help-popover-link {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  color: var(--c-on-dark-brand, #a5b4fc);
  font-size: 0.8rem;
  text-decoration: none;
  margin-top: 0.25rem;
}

.field-help-popover-link:hover {
  text-decoration: underline;
}

/* --- Admin Settings: Tracking & Analytics (public/js/adminTracking.js) --- */
/* Not wrapped in one of the existing #xxx-form-scoped label/input rules
   above (admin-cpanel-form etc.) -- this section isn't a <form>, so it
   gets its own scoped rules instead. */

#admin-tracking-section label {
  display: block;
  font-size: 0.9rem;
  font-weight: 600;
  margin-top: 0.75rem;
  margin-bottom: 0.3rem;
}

#admin-tracking-section input[type="text"] {
  display: block;
  width: 100%;
  padding: 0.45rem 0.6rem;
  font-size: 0.95rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
}

.admin-tracking-textarea {
  display: block;
  width: 100%;
  padding: 0.6rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 6px;
  font-size: 0.78rem;
  font-family: monospace;
  resize: vertical;
}

.admin-tracking-quick-setup {
  background: var(--c-success-tint, #f0fdf4);
  border: 1px solid var(--c-success-border, #bbf7d0);
  border-radius: 8px;
  padding: 1rem;
  margin: 0.75rem 0 1.25rem;
}

.admin-tracking-quick-setup h4 {
  margin: 0 0 0.5rem;
  color: var(--c-success-text, #166534);
  font-size: 0.9rem;
}

.admin-tracking-quick-setup p {
  margin: 0 0 0.75rem;
  font-size: 0.8rem;
  color: var(--c-success-text, #166534);
}

.admin-tracking-quick-setup .input-actions input {
  border-color: var(--c-success-border, #bbf7d0);
}

/* --- Register page --- */

#register-form label {
  display: block;
  width: 100%;
  margin-top: 0.15rem;
  margin-bottom: 0.15rem;
  font-size: 0.9rem;
  font-weight: 600;
}

#register-form input {
  display: block;
  width: 100%;
  padding: 0.35rem 0.6rem;
  font-size: 0.95rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
}

#register-form button[type="submit"] {
  width: 100%;
  margin-top: 0.3rem;
}

.reg-name-row {
  display: flex;
  gap: 0.5rem;
}

/* #register-form-prefixed to win over #register-form input's own
   width:100%/display:block -- same reasoning as the checkbox override
   just below this.

   ⚠️ flex: 1 1 0, NOT width: 50%. Two items at 50% plus the row's 0.5rem
   gap is 100% + 8px, so they overflow and flex-shrink pulls them back by
   whatever their content allows -- which is not the same amount for a long
   placeholder and a short one, so "half each" came out visibly uneven.
   Sizing from 0 makes the gap come out of the free space instead, and the
   two are then genuinely equal. min-width: 0 because a flex item refuses to
   shrink below its content otherwise, and these carry long placeholders. */
#register-form .reg-name-row input {
  flex: 1 1 0;
  min-width: 0;
  width: auto;
}

.advanced-toggle-btn {
  background: none;
  border: none;
  color: var(--c-brand, #6366f1);
  font-size: 0.85rem;
  cursor: pointer;
  font-weight: 600;
}

#account-type-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem;
  margin-bottom: 1rem;
}

.type-option {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
  cursor: pointer;
  transition: all 0.15s;
  font-size: 0.8rem;
}

.type-option:hover {
  border-color: var(--c-brand, #6366f1);
  background: var(--c-brand-tint, #f5f3ff);
}

.type-option.active {
  border-color: var(--c-brand, #6366f1);
  background: var(--c-brand-tint, #f5f3ff);
  color: var(--c-brand-strong, #4f46e5);
}

.type-option strong {
  display: block;
  font-size: 0.8rem;
}

.type-option small {
  color: var(--c-text-dim, #94a3b8);
  font-size: 0.72rem;
}

.type-option span {
  font-size: 1.25rem;
  flex-shrink: 0;
}

/* #type-extra-fields' inputs are plain <input>s with no extra classes,
   deliberately -- they still land inside #register-form, so they already
   inherit that shared width:100%/padding/border rule (line ~2664 above)
   the rest of this form's inputs use, no separate styling needed.

   ⚠️ They are injected by register.js as bare siblings, so with that
   inherited `display: block; width: 100%` they stacked FLUSH -- two
   full-width boxes touching, with nothing between them. A grid gives them
   a column each and a real gap without register.js having to emit any
   wrapper markup, which keeps ACCOUNT_TYPE_EXTRA_FIELDS as the plain
   two-input strings it is.

   auto-fit, so the pair is side by side where there is room and stacks --
   still with the gap -- where there is not. */
#type-extra-fields.has-fields {
  margin-top: 0.75rem;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 0.6rem;
}
#type-extra-fields.has-fields > input { min-width: 0; }

.register-terms-row {
  margin: 0.6rem 0 0;
}

.register-terms-row label {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  font-weight: normal;
  font-size: 0.8rem;
  color: var(--c-text-soft, #555555);
  cursor: pointer;
}

/* Overrides #register-form input's own width:100%/display:block above --
   needs the same #register-form-prefixed specificity to actually win. */
#register-form input[type="checkbox"] {
  width: auto;
  display: inline-block;
  margin-top: 0.15rem;
  flex-shrink: 0;
}

.register-terms-row a {
  color: var(--c-brand-strong, #4f46e5);
  text-decoration: underline;
}

/* The SMS row is longer than the Terms row and says more, so it gets a
   little room of its own rather than crowding against it. */
/* --- SMS consent: account card + gate modal ------------------------- */

.sms-consent-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin: 0.75rem 0 0.5rem;
}

.sms-consent-state { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.sms-consent-status { font-size: 0.85rem; font-weight: 600; color: var(--c-text-muted, #64748b); }
.sms-consent-status-on { color: var(--c-success, #16a34a); }
.sms-consent-since { font-size: 0.78rem; }
.sms-consent-fineprint { font-size: 0.75rem; line-height: 1.55; margin: 0; }

.sms-consent-modal {
  position: fixed;
  inset: 0;
  z-index: 25000;
  background: color-mix(in srgb, var(--c-scrim, #0f172a) 60%, transparent);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
}

/* .hidden is display:none app-wide, and it has to beat the display:flex
   above -- which it does only because this rule is more specific than
   .hidden's single class. Stated explicitly rather than relying on it. */
.sms-consent-modal.hidden { display: none; }

.sms-consent-modal-box {
  background: var(--c-surface, #fff);
  border-radius: 1rem;
  padding: 1.75rem;
  max-width: 480px;
  width: 100%;
  max-height: 90vh;
  overflow-y: auto;
  box-shadow: 0 20px 60px color-mix(in srgb, var(--c-scrim, #0f172a) 30%, transparent);
}

.sms-consent-modal-head { text-align: center; margin-bottom: 1.25rem; }
.sms-consent-modal-icon { font-size: 2.5rem; margin-bottom: 0.5rem; }
.sms-consent-modal-head h3 { font-size: 1.1rem; font-weight: 700; margin: 0 0 0.5rem; color: var(--c-text, #0f172a); }
.sms-consent-modal-head p { font-size: 0.85rem; color: var(--c-text-muted, #64748b); margin: 0; line-height: 1.55; }

.sms-consent-modal-list {
  background: var(--c-surface-subtle, #f8fafc);
  border-radius: 0.5rem;
  padding: 1rem;
  margin-bottom: 1.1rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  font-size: 0.78rem;
  color: var(--c-text-muted, #64748b);
}
.sms-consent-modal-list-title { font-size: 0.8rem; font-weight: 700; color: var(--c-text, #0f172a); margin-bottom: 0.15rem; }

.sms-consent-modal-check {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  cursor: pointer;
  font-size: 0.8rem;
  color: var(--c-text-3, #475569);
  line-height: 1.55;
  margin-bottom: 1.1rem;
  font-weight: normal;
}
.sms-consent-modal-check input { margin-top: 0.2rem; flex-shrink: 0; cursor: pointer; width: auto; }

.sms-consent-modal-submit { width: 100%; margin-bottom: 0.4rem; }

/* Opts out of the app-wide gradient button: "maybe later" must not
   compete with the action above it. */
.sms-consent-modal-dismiss {
  width: 100%;
  background: none;
  border: none;
  box-shadow: none;
  color: var(--c-text-dim, #94a3b8);
  font-size: 0.8rem;
  font-weight: 500;
  cursor: pointer;
  padding: 0.5rem;
}
.sms-consent-modal-dismiss:hover:not(:disabled) {
  background: none;
  color: var(--c-text-muted, #64748b);
  box-shadow: none;
  transform: none;
}

.sms-consent-modal-foot { text-align: center; font-size: 0.72rem; color: var(--c-text-dim, #94a3b8); margin: 0.6rem 0 0; }

.register-sms-consent-row {
  margin-top: 0.9rem;
  padding: 0.65rem 0.75rem;
  background: var(--c-surface-subtle, #f8fafc);
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
}

.register-sms-consent-row label { line-height: 1.55; }

/* Says out loud that this one is not required. Consent to receive
   messages cannot be a condition of signing up -- both legal pages
   already state that, and the form should not imply otherwise by looking
   identical to the checkbox that IS mandatory. */
.register-optional-chip {
  display: inline-block;
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--c-text-muted, #64748b);
  background: var(--c-border, #e2e8f0);
  border-radius: 2rem;
  padding: 0.1rem 0.45rem;
  margin-right: 0.3rem;
  vertical-align: 1px;
}

.field-error {
  color: var(--c-danger-strong, #dc2626);
  font-size: 0.78rem;
  min-height: 0.7em;
  margin: 0.05rem 0 0;
}

.reg-username-hint {
  font-size: 0.78rem;
  margin: 0.05rem 0 0;
}

.auth-footer-link {
  text-align: center;
  margin: 0.4rem 0 0;
}

.password-strength {
  margin-top: 0.15rem;
}

.password-strength-track {
  height: 5px;
  background: var(--c-border-soft, #e5e7eb);
  border-radius: 999px;
  overflow: hidden;
}

.password-strength-fill {
  height: 100%;
  transition: width 0.2s ease, background-color 0.2s ease;
}

.strength-fill-weak {
  width: 33%;
  background: var(--c-danger-strong, #dc2626);
}

.strength-fill-fair {
  width: 66%;
  background: var(--c-warning, #f59e0b);
}

.strength-fill-strong {
  width: 100%;
  background: var(--c-success, #16a34a);
}

.strength-label {
  display: inline-block;
  margin-top: 0.2rem;
  font-size: 0.75rem;
  font-weight: 600;
}

.strength-label-weak {
  color: var(--c-danger-strong, #dc2626);
}

.strength-label-fair {
  color: var(--c-warning-text, #92400e);
}

.strength-label-strong {
  color: var(--c-success, #16a34a);
}

/* --- Privacy / Terms pages ------------------------------------------
 *
 * These are read by two audiences with opposite needs: a customer
 * skimming for one answer, and a carrier compliance reviewer checking
 * that specific language exists. Both are served by the same thing --
 * structure. Hence the contents rail, the section anchors, and the
 * coloured blocks around the SMS disclosures.
 *
 * Everything here is scoped to .legal-page / .legal-* so the app's own
 * header, main and card rules are untouched.
 */

.legal-page {
  background: var(--c-surface-sunken, #f1f5f9);
}

.legal-header {
  background: linear-gradient(135deg, var(--c-chrome, #17121f) 0%, var(--c-chrome-2, #241a3d) 55%, var(--c-chrome-3, #1a1329) 100%);
  padding: 0;
}

.legal-header-inner {
  max-width: 1100px;
  margin: 0 auto;
  padding: 0.9rem 1.5rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  flex-wrap: wrap;
}

.legal-brand {
  font-size: 1.3rem;
  font-weight: 900;
  text-decoration: none;
  background: linear-gradient(90deg, var(--c-brand, #6366f1), var(--c-purple, #a855f7), var(--c-pink, #ec4899));
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}

.legal-nav {
  display: flex;
  gap: 0.4rem;
  margin: 0;
}

.legal-nav a {
  color: var(--c-chrome-text, #cbd5e1);
  text-decoration: none;
  font-size: 0.82rem;
  font-weight: 600;
  padding: 0.35rem 0.8rem;
  border-radius: 2rem;
  transition: background 0.15s ease, color 0.15s ease;
}

.legal-nav a:hover {
  color: var(--c-on-accent, #ffffff);
  background: color-mix(in srgb, var(--c-highlight, #ffffff) 8%, transparent);
  text-decoration: none;
}

.legal-nav a.active {
  color: var(--c-on-accent, #ffffff);
  background: color-mix(in srgb, var(--c-highlight, #ffffff) 14%, transparent);
  text-decoration: none;
}

/* The cross-links between About, Security and the policies, as a sub-bar
   under the site's own header.
   ⚠️ `.legal-header` above is KEPT. It is what these pages showed before
   the swap and what any page still carrying that markup would show; a
   rule deleted because nothing in this checkout uses it is the rule a
   page added next month finds missing. */
/* A divider inside the Site Branding block: the identity fields above
   describe the site, these describe the company behind it. */
.sb-subhead {
  margin: 1.6rem 0 0.2rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--c-border, #e2e8f0);
  font-size: 0.95rem;
}

.legal-subnav {
  background: var(--c-surface, #ffffff);
  border-bottom: 1px solid var(--c-border, #e2e8f0);
}

.legal-subnav-inner {
  max-width: 1100px;
  margin: 0 auto;
  padding: 0.5rem 1.5rem;
  display: flex;
  align-items: center;
  gap: 0.3rem;
  flex-wrap: wrap;
}

.legal-subnav a {
  color: var(--c-text-muted, #64748b);
  text-decoration: none;
  font-size: 0.82rem;
  font-weight: 600;
  padding: 0.3rem 0.75rem;
  border-radius: 2rem;
  transition: background 0.15s ease, color 0.15s ease;
}

.legal-subnav a:hover {
  color: var(--c-text, #0f172a);
  background: var(--c-surface-subtle, #f1f5f9);
  text-decoration: none;
}

.legal-subnav a.active {
  color: var(--c-brand-deep, #4338ca);
  background: var(--c-surface-subtle, #f1f5f9);
  text-decoration: none;
}

/* Title band. The document title lives here rather than as the first
   line inside the card, so the card can start with actual clause one. */
.legal-hero {
  background: linear-gradient(135deg, var(--c-chrome-3, #1a1329) 0%, var(--c-chrome-2, #241a3d) 100%);
  color: var(--c-on-accent, #ffffff);
  border-bottom: 1px solid color-mix(in srgb, var(--c-highlight, #ffffff) 6%, transparent);
}

.legal-hero-inner {
  max-width: 1100px;
  margin: 0 auto;
  padding: 2.25rem 1.5rem 2.5rem;
}

.legal-eyebrow {
  margin: 0 0 0.4rem;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--c-on-dark-brand, #a5b4fc);
}

.legal-hero-title {
  margin: 0;
  font-size: 2.1rem;
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -0.02em;
}

.legal-hero-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 1rem 0 0;
}

.legal-chip {
  display: inline-block;
  font-size: 0.72rem;
  font-weight: 600;
  padding: 0.25rem 0.7rem;
  border-radius: 2rem;
  background: color-mix(in srgb, var(--c-highlight, #ffffff) 10%, transparent);
  color: var(--c-chrome-text, #cbd5e1);
  border: 1px solid color-mix(in srgb, var(--c-highlight, #ffffff) 12%, transparent);
}

.legal-chip-muted {
  background: transparent;
  color: var(--c-on-dark-brand, #a5b4fc);
}

/* Contents rail + document. Overrides the app-wide `main` rule, which is
   a 1280px flex column. */
.legal-content {
  display: grid;
  grid-template-columns: 220px minmax(0, 1fr);
  align-items: start;
  gap: 2rem;
  max-width: 1100px;
  margin: 0 auto;
  padding: 2rem 1.5rem 3rem;
}

@media (max-width: 900px) {
  .legal-content { grid-template-columns: minmax(0, 1fr); gap: 1.25rem; }
  .legal-toc { display: none; }
}

.legal-toc {
  position: sticky;
  top: 1.5rem;
  max-height: calc(100vh - 3rem);
  overflow-y: auto;
  font-size: 0.78rem;
}

.legal-toc-title {
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--c-text-dim, #94a3b8);
  margin: 0 0 0.6rem 0.7rem;
}

.legal-toc a {
  display: block;
  padding: 0.35rem 0.7rem;
  border-left: 2px solid var(--c-border, #e2e8f0);
  color: var(--c-text-muted, #64748b);
  text-decoration: none;
  line-height: 1.4;
  transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}

.legal-toc a:hover {
  color: var(--c-brand-deep, #4338ca);
  background: var(--c-brand-tint-2, #eef2ff);
  border-left-color: var(--c-brand-border, #c7d2fe);
}

.legal-toc a.active {
  color: var(--c-brand-deep, #4338ca);
  font-weight: 600;
  border-left-color: var(--c-brand, #6366f1);
  background: var(--c-brand-tint-2, #eef2ff);
}

.legal-doc {
  background: var(--c-surface, #fff);
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.9rem;
  padding: 2.25rem 2.5rem 2.5rem;
  box-shadow: 0 1px 3px color-mix(in srgb, var(--c-scrim, #0f172a) 4%, transparent);
}

@media (max-width: 640px) {
  .legal-doc { padding: 1.5rem 1.25rem 1.75rem; }
  .legal-hero-title { font-size: 1.6rem; }
  .legal-hero-inner { padding: 1.75rem 1.25rem 2rem; }
}

/* scroll-margin-top so an anchor jump does not land the heading flush
   against the top of the viewport. */
.legal-doc h3 {
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--c-text, #0f172a);
  letter-spacing: -0.01em;
  margin: 2.25rem 0 0.6rem;
  padding-top: 0.25rem;
  scroll-margin-top: 1.5rem;
}

.legal-doc h3:first-of-type,
.legal-doc > section:first-child h3 {
  margin-top: 0;
}

.legal-doc h4 {
  font-size: 0.9rem;
  font-weight: 700;
  color: var(--c-text-2, #334155);
  margin: 1.4rem 0 0.4rem;
}

/* 0.95/1.75 rather than 0.9/1.6. Legal text is the least skimmable
   content in the app and was set tighter than anything else in it. */
.legal-doc p,
.legal-doc ul,
.legal-doc ol {
  font-size: 0.95rem;
  line-height: 1.75;
  color: var(--c-text-2, #334155);
  margin: 0 0 0.9rem;
}

.legal-doc ul,
.legal-doc ol {
  padding-left: 1.35rem;
}

.legal-doc li {
  margin-bottom: 0.45rem;
}

.legal-doc a {
  color: var(--c-brand-strong, #4f46e5);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-color: var(--c-brand-border, #c7d2fe);
  transition: text-decoration-color 0.15s ease;
}

.legal-doc a:hover { text-decoration-color: var(--c-brand-strong, #4f46e5); }

.legal-doc strong { color: var(--c-text, #0f172a); }

/* Same subtle indigo used throughout the app for accent/emphasis --
   wraps the SMS-specific disclosure sections in both legal docs so
   they're immediately visible to a TCR reviewer scanning the page,
   rather than reading identically to every other paragraph. */
.sms-highlight {
  background: var(--c-brand-tint-2, #eef2ff);
  border: 1px solid var(--c-brand-border, #c7d2fe);
  border-radius: 0.75rem;
  padding: 1.5rem 1.75rem;
  margin: 2rem 0;
}

.sms-highlight h3:first-child { margin-top: 0; }
.sms-highlight h3 { color: var(--c-brand-deep, #4338ca); }

/* Green (distinct from .sms-highlight's indigo) specifically for the
   "How We Obtain SMS Consent" section -- TCR reviewers look for this
   exact content, so it gets its own unmistakable treatment rather than
   blending into the general SMS-highlight styling above. */
.consent-section {
  background: var(--c-success-tint, #f0fdf4);
  border: 1px solid var(--c-success-border, #bbf7d0);
  border-radius: 0.75rem;
  padding: 1.5rem 1.75rem;
  margin: 2rem 0;
}

.consent-section h3:first-child {
  color: var(--c-success-text, #166534);
  margin-top: 0;
}

.consent-section h4 { color: var(--c-success-text, #166534); }

.consent-section blockquote {
  background: var(--c-surface, #fff);
  border-left: 4px solid var(--c-success, #16a34a);
  padding: 0.9rem 1.2rem;
  margin: 0.75rem 0;
  border-radius: 0 0.5rem 0.5rem 0;
  font-style: italic;
  color: var(--c-text-2, #334155);
  font-size: 0.9rem;
  line-height: 1.65;
}

/* Two cards rather than a line of links -- this is the only navigation
   at the bottom of a very long page. */
.legal-footer-nav {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 0.75rem;
  margin: 2.5rem 0 0;
  padding-top: 1.75rem;
  border-top: 1px solid var(--c-border, #e2e8f0);
}

.legal-footer-link {
  display: block;
  padding: 0.9rem 1.1rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.6rem;
  text-decoration: none;
  transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.12s ease;
}

.legal-footer-link:hover {
  border-color: var(--c-brand, #6366f1);
  box-shadow: 0 3px 10px color-mix(in srgb, var(--c-scrim, #0f172a) 8%, transparent);
  transform: translateY(-1px);
}

.legal-footer-link span {
  display: block;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--c-text-dim, #94a3b8);
  margin-bottom: 0.15rem;
}

.legal-footer-link strong {
  font-size: 0.92rem;
  font-weight: 700;
  color: var(--c-text, #0f172a);
}

/* A legal document is something people print and file. Worth the eight
   lines it costs to make that come out as a document rather than as a
   screenshot of a web page. */
@media print {
  .legal-page { background: var(--c-surface, #fff); }
  .legal-header,
  .legal-toc,
  .legal-footer-nav { display: none; }
  .legal-hero { background: none; color: var(--c-text, #0f172a); border-bottom: 1px solid var(--c-chrome, #17121f); }
  .legal-hero-title { color: var(--c-text, #0f172a); }
  .legal-eyebrow, .legal-chip, .legal-chip-muted { color: var(--c-text-2, #334155); border-color: var(--c-text-faint, #999999); }
  .legal-content { display: block; max-width: none; padding: 0; }
  .legal-doc { border: none; box-shadow: none; padding: 0; }
  .legal-doc p, .legal-doc ul, .legal-doc li { color: var(--c-text, #0f172a); }
  .sms-highlight, .consent-section { border: 1px solid var(--c-text-faint, #999999); background: none; break-inside: avoid; }
  .legal-doc h3 { break-after: avoid; }
}

/* --- Contact lists & tags --- */

.bulk-actions-toolbar,
.create-organize-toolbar,
.contacts-pagination {
  flex-wrap: wrap;
}

.contacts-pagination select {
  padding: 0.3rem 0.5rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
  font-size: 0.85rem;
}

.create-organize-toolbar input[type="text"] {
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
  font-size: 0.85rem;
  width: 140px;
}

.create-organize-toolbar input[type="color"] {
  padding: 0.1rem;
  width: 32px;
  height: 28px;
}

/* Two-class specificity so this reliably beats .manage-organize-panel's
   own `display: flex` below regardless of source order -- same fix as
   .modal-overlay.hidden (see that rule for the full explanation). */
.manage-organize-panel.hidden {
  display: none;
}

.manage-organize-panel {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem;
  margin: 0.75rem 0;
  padding: 1rem;
  background: var(--c-surface-subtle, #f8fafc);
  border: 1px solid var(--c-border-soft, #e5e7eb);
  border-radius: 8px;
}

.manage-organize-col {
  flex: 1;
  min-width: 220px;
}

.manage-organize-col h3 {
  margin: 0 0 0.5rem;
  font-size: 0.9rem;
}

.manage-organize-table {
  width: 100%;
  border-collapse: collapse;
}

.manage-organize-table td,
.manage-organize-table th {
  padding: 0.25rem 0.3rem;
  vertical-align: middle;
}

.manage-organize-table th {
  text-align: left;
  font-size: 0.75rem;
  color: var(--c-text-muted, #64748b);
}

.manage-organize-table input[type="text"] {
  width: 100%;
  padding: 0.3rem 0.4rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
  font-size: 0.85rem;
}

.manage-organize-table input[type="color"] {
  padding: 0.1rem;
  width: 28px;
  height: 26px;
}

.manage-organize-table .muted {
  font-size: 0.75rem;
  white-space: nowrap;
}

/* Same fix as .manage-organize-panel.hidden above -- new-group-quick-form
   combines this class with .hidden (for non-admins) and would otherwise
   stay visible since this rule's display: flex is declared after .hidden. */
.bulk-action-group.hidden {
  display: none;
}

.bulk-action-group {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.contact-name-link {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  color: var(--c-brand, #6366f1);
  font: inherit;
  text-decoration: underline;
  cursor: pointer;
}

.contact-name-cell {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.contact-row-avatar {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
}

.contact-row-avatar-placeholder {
  background: var(--c-border, #e2e8f0);
  color: var(--c-text-muted, #64748b);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 0.8rem;
}

.sortable-col {
  cursor: pointer;
  user-select: none;
}

.sortable-col:hover {
  color: var(--c-brand, #6366f1);
}

.sortable-col.sort-asc::after {
  content: ' \25B2';
  font-size: 0.7em;
}

.sortable-col.sort-desc::after {
  content: ' \25BC';
  font-size: 0.7em;
}

.contact-linxi-badge {
  font-size: 0.65rem;
  background: var(--c-brand-tint, #f5f3ff);
  color: var(--c-brand, #6366f1);
  padding: 0.1rem 0.4rem;
  border-radius: 2rem;
  font-weight: 600;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
}

.contact-linxi-badge:hover {
  text-decoration: underline;
}

/* .contact-linxi-badge's base 0.65rem/0.1rem-0.4rem is sized for the
   cramped Contacts table row it's also used in -- here in the Links
   tab's profile header it sits beside the Opted In pill and Export/
   Edit Profile buttons (0.75rem text, and the Opted In pill's own
   0.15rem/0.6rem padding), so both get bumped up to match instead of
   reading as a visibly smaller pill next to them. */
#profile-linxi-link.contact-linxi-badge {
  font-size: 0.75rem;
  padding: 0.15rem 0.6rem;
}

/* Far-right Edit column on the Linx (Contacts) tab table -- right-
   aligned so the button sits flush against the table's own right edge
   rather than left-aligned like every other cell. */
.contacts-edit-cell {
  text-align: right;
  white-space: nowrap;
}

.filter-chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.75rem;
}

.filter-chip {
  background: var(--c-surface, #fff);
  color: var(--c-text-2, #334155);
  border: 1.5px solid var(--c-border-strong, #cbd5e1);
  border-radius: 999px;
  padding: 0.25rem 0.75rem;
  font-size: 0.8rem;
  font-weight: 600;
  cursor: pointer;
}

.filter-chip.active {
  color: var(--c-on-accent, #ffffff);
}

.row-pill {
  display: inline-block;
  color: var(--c-on-accent, #ffffff);
  border-radius: 999px;
  padding: 0.1rem 0.5rem;
  font-size: 0.7rem;
  font-weight: 600;
  margin: 0.1rem 0.2rem 0.1rem 0;
}

.list-assignment-options {
  margin-top: 0.5rem;
}

.radio-option {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.9rem;
  font-weight: 600;
  margin-top: 0.6rem;
}

.radio-option input {
  width: auto;
}

.list-sub-fields {
  margin: 0.4rem 0 0 1.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  max-width: 360px;
}

.list-sub-fields input,
.list-sub-fields select {
  padding: 0.4rem 0.55rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
  font-size: 0.9rem;
}

.color-preset-row {
  display: flex;
  gap: 0.5rem;
}

.color-preset {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: 2px solid transparent;
  padding: 0;
  cursor: pointer;
}

.color-preset.active {
  border-color: var(--c-ink, #1a1a1a);
}

#tag-input {
  width: 100%;
  max-width: 360px;
  margin-top: 0.5rem;
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
  font-size: 0.9rem;
}

.tag-suggestions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-top: 0.5rem;
}

.tag-suggestion-chip {
  background: var(--c-surface, #fff);
  border: 1.5px solid var(--c-border-strong, #cbd5e1);
  border-radius: 999px;
  padding: 0.15rem 0.6rem;
  font-size: 0.78rem;
  cursor: pointer;
}

.tag-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.6rem;
}

.tag-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  background: var(--c-brand, #6366f1);
  color: var(--c-on-fill, #ffffff);
  border-radius: 999px;
  padding: 0.15rem 0.5rem 0.15rem 0.7rem;
  font-size: 0.75rem;
  font-weight: 600;
}

.tag-pill-remove {
  background: transparent;
  border: none;
  color: inherit;
  padding: 0;
  font-size: 0.9rem;
  line-height: 1;
  cursor: pointer;
}

.tag-pills-select {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.4rem;
}/* --- Landing page (shown instead of the app shell for anonymous visitors) --- */

/* ⚠️ FLEX, so the page links can sit on the right with margin-left:auto.
   Its two original children are an inline-flex brand and an inline
   tagline, so as flex items they lay out in the same row they already
   did -- and `align-items:center` centres the tagline against the brand
   rather than sitting it on a shared baseline, which is the only visible
   difference. `flex-wrap` so a long menu on a narrow screen drops below
   the brand instead of squeezing it. */
.landing-header {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  background: var(--c-inverse-surface, #0f172a);
  color: var(--c-on-accent, #ffffff);
  padding: 1rem 1.5rem;
}

.landing-brand {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  vertical-align: middle;
  font-weight: 700;
  font-size: 1.15rem;
  color: inherit;
  text-decoration: none;
}

.landing-header-tagline {
  margin-left: 0.6rem;
  font-size: 0.85rem;
  color: var(--c-text-dim, #94a3b8);
}

/* --- Hero -- dark navy, brand gradient on the headline, a few softly
   floating channel emoji in the background (decorative, aria-hidden).
   --- */

.landing-hero {
  position: relative;
  background: var(--c-inverse-surface, #0f172a);
  color: var(--c-on-accent, #ffffff);
  overflow: hidden;
}

.landing-hero-floaters {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}

.landing-hero-floater {
  position: absolute;
  font-size: 2.5rem;
  opacity: 0.12;
  animation: landing-float 7s ease-in-out infinite;
}

.landing-hero-floater-1 {
  top: 12%;
  left: 8%;
  animation-delay: 0s;
}

.landing-hero-floater-2 {
  top: 62%;
  left: 88%;
  animation-delay: 1.5s;
}

.landing-hero-floater-3 {
  top: 78%;
  left: 15%;
  animation-delay: 3s;
}

@keyframes landing-float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-18px); }
}

.landing-hero-inner {
  position: relative;
  max-width: 780px;
  margin: 0 auto;
  padding: 4.5rem 1.5rem 4rem;
}

.landing-hero-inner h1 {
  font-size: 2.75rem;
  margin: 0 0 0.75rem;
  line-height: 1.1;
}

.landing-gradient-text {
  background: linear-gradient(90deg, var(--c-brand, #6366f1), var(--c-purple, #a855f7), var(--c-pink, #ec4899));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.landing-lede {
  font-size: 1.1rem;
  color: var(--c-text-2, #334155);
  margin: 0 0 2rem;
  max-width: 60ch;
}

.landing-lede-dark {
  color: var(--c-chrome-text, #cbd5e1);
}

.landing-cta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-bottom: 1.25rem;
}

.landing-hero-subtext {
  margin: 0;
  color: var(--c-text-dim, #94a3b8);
  font-size: 0.85rem;
}

.btn {
  display: inline-block;
  padding: 0.7rem 1.4rem;
  border-radius: 6px;
  text-decoration: none;
  font-weight: 600;
  font-size: 0.95rem;
}

/* Aliases used by the newer modals/sections -- pointed at the same
   tokens as .secondary and the base rule so there is exactly one
   primary look and one secondary look in the app, not two of each. */
.btn-primary {
  background: var(--btn-grad);
  color: var(--c-on-accent, #ffffff);
}

.btn-primary:hover:not(:disabled) {
  background: var(--btn-grad-hover);
}

.btn-secondary {
  background: var(--btn-surface);
  color: var(--c-text-2, #334155);
  border: 1px solid var(--c-border, #e2e8f0);
}

.btn-secondary:hover:not(:disabled) {
  background: var(--btn-surface-hover);
  border-color: var(--c-border-strong, #cbd5e1);
  color: var(--c-text, #0f172a);
}

/* Scoped to the landing page's own CTAs -- not a change to the global
   .btn-primary used elsewhere in the app (checkout, upgrade modal, etc). */
.landing-btn-gradient {
  background: linear-gradient(90deg, var(--c-brand, #6366f1), var(--c-purple, #a855f7), var(--c-pink, #ec4899));
  color: var(--c-on-fill, #ffffff);
  border: none;
}

.landing-btn-outline {
  background: transparent;
  color: var(--c-on-accent, #ffffff);
  border: 1px solid color-mix(in srgb, var(--c-highlight, #ffffff) 35%, transparent);
}

/* --- Channel icons bar -- WhatsApp/Instagram/Facebook are clearly
   marked "Coming Soon", not claimed as live: this app only actually
   sends SMS and email today. --- */

.landing-channel-bar {
  background: var(--c-inverse-surface-2, #1e293b);
  color: var(--c-chrome-text, #cbd5e1);
  padding: 0.9rem 1.5rem;
}

.landing-channel-bar-inner {
  max-width: 900px;
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  font-size: 0.85rem;
}

.landing-channel-sep {
  color: var(--c-text-3, #475569);
}

.landing-channel-soon em {
  font-style: normal;
  font-size: 0.65rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--c-purple, #a855f7);
  margin-left: 0.3rem;
}

.landing-channel-muted {
  color: var(--c-text-muted, #64748b);
}

.landing-features {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 1.5rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

.landing-features li {
  background: var(--c-surface, #fff);
  border: 1px solid var(--c-border-soft, #e5e7eb);
  border-radius: 8px;
  padding: 1.1rem 1.2rem;
}

.landing-features strong {
  display: block;
  margin-bottom: 0.3rem;
}

.landing-features span {
  color: var(--c-text-soft, #555555);
  font-size: 0.92rem;
}

.landing-footer {
  color: var(--c-text-muted, #64748b);
  font-size: 0.85rem;
}

.landing-footer a {
  color: var(--c-brand, #6366f1);
}

.landing-footer p {
  margin: 0.75rem 0 0;
}

.landing-footer-columns {
  background: var(--c-surface-subtle, #f8fafc);
  padding: 3rem 1.5rem 2rem;
}

.landing-footer-inner {
  max-width: 1000px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: 1.2fr 1fr 1fr 1fr 1fr;
  gap: 2rem;
  padding-bottom: 2rem;
  border-bottom: 1px solid var(--c-border-soft, #e5e7eb);
}

.landing-footer-col {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}

.landing-footer-col h4 {
  margin: 0 0 0.3rem;
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--c-text-dim, #94a3b8);
}

.landing-footer-brand-col .landing-brand {
  color: var(--c-ink, #1a1a1a);
}

.landing-footer-tagline {
  margin: 0.5rem 0 0;
  color: var(--c-text-dim, #94a3b8);
  font-size: 0.85rem;
}
.landing-footer-copyright {
  max-width: 1000px;
  margin: 0 auto;
  padding: 1.25rem 0 0;
}

/* --- Landing page popup (see #landing-popup in index.html, js/
   landingPopup.js) -- deliberately NOT a .modal-overlay: no dimming
   backdrop, no inset:0 wrapper. Just the panel itself, fixed and
   centered, with its own shadow doing the visual separation a backdrop
   would normally do, so the landing page underneath stays fully usable
   while this floats above it. --- */

.landing-popup {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 2000;
  width: min(600px, 92vw);
  max-height: 90vh;
  overflow-y: auto;
  background: var(--c-surface, #fff);
  border-radius: 16px;
  box-shadow: 0 24px 70px color-mix(in srgb, var(--c-scrim, #0f172a) 35%, transparent);
  padding: 2rem;
}

.landing-popup-close {
  position: absolute;
  top: 0.75rem;
  right: 0.85rem;
  background: none;
  border: none;
  font-size: 1.5rem;
  line-height: 1;
  color: var(--c-text-dim, #94a3b8);
  cursor: pointer;
  padding: 0.25rem;
  border-radius: 6px;
  transition: color 0.15s, background 0.15s;
}

.landing-popup-close:hover {
  color: var(--c-ink, #1a1a1a);
  background: var(--c-surface-sunken, #f1f5f9);
}

.landing-popup-header {
  text-align: center;
  margin-bottom: 1.5rem;
}

.landing-popup-badge {
  display: inline-block;
  font-size: 0.8rem;
  font-weight: 700;
  color: var(--c-brand, #6366f1);
  background: var(--c-brand-tint, #f5f3ff);
  border: 1px solid var(--c-brand-tint-3, #e0e7ff);
  border-radius: 999px;
  padding: 0.3rem 0.85rem;
  margin-bottom: 0.6rem;
}

.landing-popup-subtitle {
  margin: 0;
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--c-text, #0f172a);
}

.landing-popup-demo-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.75rem;
}

.landing-popup-demo-card {
  display: flex;
  align-items: flex-start;
  gap: 0.65rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 10px;
  padding: 0.85rem;
  text-decoration: none;
  color: inherit;
  transition: all 0.15s;
}

.landing-popup-demo-card:hover {
  border-color: var(--c-brand, #6366f1);
  background: var(--c-brand-tint, #f5f3ff);
  transform: translateY(-1px);
  box-shadow: 0 4px 14px color-mix(in srgb, var(--c-brand, #6366f1) 15%, transparent);
}

.landing-popup-demo-emoji {
  font-size: 1.4rem;
  line-height: 1;
  flex-shrink: 0;
}

.landing-popup-demo-text {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

.landing-popup-demo-title {
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--c-ink, #1a1a1a);
}

.landing-popup-demo-desc {
  font-size: 0.75rem;
  line-height: 1.35;
  color: var(--c-text-muted, #64748b);
}

.landing-popup-footer {
  margin-top: 1.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--c-border, #e2e8f0);
}

.landing-popup-footer-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0.75rem;
}

.landing-popup-footer-label {
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--c-text, #0f172a);
}

.landing-popup-login-hint {
  margin: 0;
  font-size: 0.8rem;
  color: var(--c-text-muted, #64748b);
}

.landing-popup-cta-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.6rem;
}

.landing-popup-cta-row .btn-google {
  padding: 0.6rem;
}

.landing-popup-signup-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
}

@media (max-width: 560px) {
  .landing-popup {
    padding: 1.5rem 1.25rem;
  }

  .landing-popup-demo-grid {
    grid-template-columns: 1fr;
  }

  .landing-popup-footer-top {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.3rem;
  }

  .landing-popup-cta-row {
    grid-template-columns: 1fr;
  }
}

/* --- Landing page: Problem/Solution, Features, Pricing sections --- */

html {
  scroll-behavior: smooth;
}

.landing-section {
  padding: 4rem 1.5rem;
}

.landing-section-alt {
  background: var(--c-surface-subtle, #f8fafc);
}

.landing-section-inner {
  max-width: 1000px;
  margin: 0 auto;
}

.landing-section-inner > h2 {
  font-size: 1.6rem;
  text-align: center;
  margin: 0 0 2.5rem;
}

.landing-section-lede {
  text-align: center;
  color: var(--c-text-muted, #64748b);
  margin: -1.75rem 0 2.5rem;
}

/* --- "See Linxi in Action" demo section -- reuses .demo-cards/.demo-card
   verbatim (same component demo.html itself uses), just widened to 4
   columns for this section's roomier max-width. --- */

.landing-demo-grid.demo-cards {
  grid-template-columns: repeat(4, 1fr);
}

.landing-demo-card-cta {
  display: block;
  margin-top: 0.6rem;
  color: var(--c-brand, #6366f1);
  font-weight: 700;
  font-size: 0.8rem;
}

/* --- Social proof --- */

.landing-social-proof {
  padding: 4rem 1.5rem;
  text-align: center;
  background: var(--c-inverse-surface, #0f172a);
  color: var(--c-on-accent, #ffffff);
}

.landing-social-proof-lede {
  font-size: 1.6rem;
  font-weight: 700;
  margin: 0 0 0.75rem;
}

.landing-social-proof-sub {
  color: var(--c-text-dim, #94a3b8);
  margin: 0 0 1.5rem;
  line-height: 1.6;
}

.landing-social-proof-tag {
  display: inline-block;
  font-size: 1.1rem;
  font-style: italic;
  padding: 0.5rem 1.25rem;
  border: 1px solid color-mix(in srgb, var(--c-highlight, #ffffff) 20%, transparent);
  border-radius: 999px;
  margin: 0;
}

.landing-compare-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.5rem;
}

.landing-compare-col {
  background: var(--c-surface, #fff);
  border-radius: 10px;
  padding: 1.5rem 1.75rem;
}

.landing-compare-col h3 {
  margin: 0 0 1rem;
  font-size: 1.1rem;
}

.landing-compare-col ul {
  margin: 0;
  padding-left: 1.2rem;
}

.landing-compare-col li {
  margin-bottom: 0.6rem;
  color: var(--c-text-2, #334155);
  font-size: 0.92rem;
}

.landing-compare-old {
  border: 1px solid var(--c-danger-border, #fecaca);
}

.landing-compare-old h3 {
  color: var(--c-danger-strong, #dc2626);
}

.landing-compare-new {
  border: 1px solid var(--c-brand-border, #c7d2fe);
}

.landing-compare-new h3 {
  color: var(--c-brand, #6366f1);
}

.landing-feature-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 1.5rem;
}

.landing-feature-card {
  background: var(--c-surface, #fff);
  border: 1px solid var(--c-border-soft, #e5e7eb);
  border-radius: 10px;
  padding: 1.5rem;
}

.landing-feature-card h3 {
  margin: 0 0 0.5rem;
  font-size: 1.05rem;
  color: var(--c-brand, #6366f1);
}

.landing-feature-card p {
  margin: 0;
  color: var(--c-text-soft, #555555);
  font-size: 0.92rem;
}

.landing-pricing-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1.25rem;
  align-items: stretch;
}

.landing-pricing-card {
  position: relative;
  background: var(--c-surface, #fff);
  border: 1px solid var(--c-border-soft, #e5e7eb);
  border-radius: 10px;
  padding: 1.75rem 1.5rem;
  display: flex;
  flex-direction: column;
}

.landing-pricing-featured {
  border: 2px solid var(--c-brand, #6366f1);
  box-shadow: 0 8px 24px color-mix(in srgb, var(--c-brand, #6366f1) 15%, transparent);
}

.landing-pricing-badge {
  position: absolute;
  top: -0.7rem;
  left: 50%;
  transform: translateX(-50%);
  background: var(--c-brand, #6366f1);
  color: var(--c-on-fill, #ffffff);
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: 0.25rem 0.7rem;
  border-radius: 999px;
}

.landing-pricing-card h3 {
  margin: 0 0 0.4rem;
  font-size: 1.1rem;
}

.landing-pricing-tagline {
  margin: 0 0 0.6rem;
  font-size: 0.8rem;
  font-style: italic;
  color: var(--c-text-dim, #94a3b8);
}

.landing-price {
  font-size: 1.8rem;
  font-weight: 700;
  margin: 0 0 1.25rem;
}

.landing-price span {
  font-size: 0.85rem;
  font-weight: 400;
  color: var(--c-text-muted, #64748b);
}

.landing-pricing-card ul {
  list-style: none;
  margin: 0 0 1.5rem;
  padding: 0;
  flex: 1;
}

.landing-pricing-card li {
  font-size: 0.85rem;
  color: var(--c-text-2, #334155);
  padding: 0.35rem 0;
  border-top: 1px solid var(--c-surface-sunken, #f1f5f9);
}

.landing-pricing-card li:first-child {
  border-top: none;
}

.landing-pricing-card .btn {
  text-align: center;
}

.landing-pricing-note {
  text-align: center;
  margin: 1rem 0 0;
  color: var(--c-text-soft, #555555);
  font-size: 0.9rem;
}

.landing-pricing-note a {
  color: var(--c-brand, #6366f1);
}

@media (max-width: 800px) {
  .landing-compare-grid {
    grid-template-columns: 1fr;
  }

  .landing-pricing-grid {
    grid-template-columns: 1fr 1fr;
  }

  .landing-demo-grid.demo-cards {
    grid-template-columns: 1fr 1fr;
  }

  .landing-hero-inner h1 {
    font-size: 2.1rem;
  }

  .landing-footer-inner {
    grid-template-columns: 1fr 1fr;
  }

  .landing-footer-brand-col {
    grid-column: 1 / -1;
  }
}

@media (max-width: 520px) {
  .landing-pricing-grid {
    grid-template-columns: 1fr;
  }

  .landing-demo-grid.demo-cards {
    grid-template-columns: 1fr;
  }

  .landing-footer-inner {
    grid-template-columns: 1fr;
  }

  .landing-hero-floaters {
    display: none;
  }
}

/* --- Linxi Card editor (tab-linxicard) --- */

.linxicard-editor-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: start;
  gap: 2.5rem;
  margin-top: 1.25rem;
}

@media (max-width: 800px) {
  .linxicard-editor-grid {
    grid-template-columns: 1fr;
  }
}

.linxicard-section-heading {
  margin: 0 0 0.9rem;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--c-brand, #6366f1);
}

.linxicard-editor-form .linxicard-section-heading:not(:first-child) {
  margin-top: 2rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--c-border-soft, #e5e7eb);
}

.linxicard-editor-form label {
  display: block;
  font-weight: 600;
  font-size: 0.85rem;
  color: var(--c-text-2, #334155);
  margin: 0 0 0.5rem;
}

.linxicard-editor-form input[type="text"],
.linxicard-editor-form textarea {
  display: block;
  width: 100%;
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 8px;
  font-size: 0.95rem;
  font-family: inherit;
  margin-bottom: 1.75rem;
  transition: border-color 0.15s, box-shadow 0.15s;
}

.linxicard-editor-form input[type="text"]:focus,
.linxicard-editor-form textarea:focus {
  outline: none;
  border-color: var(--c-brand, #6366f1);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-brand, #6366f1) 15%, transparent);
}

.linxicard-editor-form textarea {
  resize: vertical;
  min-height: 70px;
}

.linxicard-actions-row {
  margin-top: 0.5rem;
}

.linxicard-color-picker {
  display: flex;
  gap: 0.6rem;
  margin: 0.5rem 0 1.5rem;
}

.linxicard-photo-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin: 0.5rem 0 1.5rem;
  flex-wrap: wrap;
}

.linxicard-photo-row .linxicard-toggle-row {
  flex: 1;
  min-width: 200px;
}

.linxicard-no-photo-hint {
  margin: 0;
  font-size: 0.82rem;
}

.linxicard-color-swatch {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 2px solid transparent;
  padding: 0;
  cursor: pointer;
  transition: transform 0.15s;
}

.linxicard-color-swatch:hover {
  transform: scale(1.08);
}

.linxicard-color-swatch.selected {
  border-color: var(--c-text, #0f172a);
  box-shadow: 0 0 0 2px var(--c-surface, #ffffff) inset;
}

.linxicard-slug-row {
  display: flex;
  align-items: center;
  gap: 0.3rem;
}

.linxicard-slug-display {
  font-size: 0.9rem;
  font-weight: 600;
  flex: 1;
  overflow-wrap: anywhere;
}

.linxicard-slug-prefix {
  font-size: 0.85rem;
  white-space: nowrap;
}

.linxicard-open-btn {
  text-decoration: none;
  white-space: nowrap;
}

.linxicard-open-btn.hidden {
  display: none;
}

.linxicard-toggle-list {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
  margin: 0.6rem 0 1.5rem;
}

@media (max-width: 520px) {
  .linxicard-toggle-list {
    grid-template-columns: 1fr;
  }
}

.linxicard-toggle-row {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  font-weight: normal;
  margin: 0;
  cursor: pointer;
}

/* Two-class specificity so this reliably beats .linxicard-toggle-row's
   own display: flex above regardless of source order -- same fix as
   .modal-overlay.hidden elsewhere in this file. Needed here specifically
   for #linxicard-use-photo-row (see refreshLinxiCardProfilePhotoOption in
   linxicard.js), the first .linxicard-toggle-row ever toggled hidden. */
.linxicard-toggle-row.hidden {
  display: none;
}

.linxicard-toggle-text {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  font-size: 0.85rem;
  color: var(--c-ink, #1a1a1a);
}

.linxicard-toggle-text .muted {
  font-size: 0.75rem;
}

/* Standard accessible checkbox-driven toggle switch -- the real
   checkbox stays in the DOM (and keeps working exactly as before, same
   ids, same .checked reads/writes in linxicard.js) but is visually
   hidden in favor of the adjacent slider it drives via :checked. */
.toggle-switch {
  position: relative;
  display: inline-block;
  width: 40px;
  height: 22px;
  flex-shrink: 0;
  margin-top: 0.1rem;
}

.toggle-switch input {
  opacity: 0;
  width: 0;
  height: 0;
  position: absolute;
}

.toggle-slider {
  position: absolute;
  inset: 0;
  background: var(--c-border-strong, #cbd5e1);
  border-radius: 999px;
  transition: background 0.2s;
  cursor: pointer;
}

.toggle-slider::before {
  content: '';
  position: absolute;
  width: 18px;
  height: 18px;
  left: 2px;
  top: 2px;
  background: var(--c-surface, #ffffff);
  border-radius: 50%;
  transition: transform 0.2s;
  box-shadow: 0 1px 3px color-mix(in srgb, var(--c-shadow, #000000) 30%, transparent);
}

.toggle-switch input:checked + .toggle-slider {
  background: var(--c-brand, #6366f1);
}

.toggle-switch input:checked + .toggle-slider::before {
  transform: translateX(18px);
}

.toggle-switch input:focus-visible + .toggle-slider {
  outline: 2px solid var(--c-brand, #6366f1);
  outline-offset: 2px;
}

.linxicard-preview-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.25rem;
}

.linxicard-preview-wrap {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  width: 100%;
}

.linxicard-preview {
  position: relative;
  width: 100%;
  max-width: 320px;
  aspect-ratio: 1 / 1;
  border-radius: 24px;
  padding: 2.25rem 1.75rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  color: var(--c-on-fill, #ffffff);
  background: linear-gradient(160deg, var(--c-brand, #6366f1), var(--c-brand-strong, #4f46e5));
  box-shadow: 0 20px 45px -12px color-mix(in srgb, var(--c-brand-deep, #4338ca) 55%, transparent), 0 2px 8px color-mix(in srgb, var(--c-shadow, #000000) 8%, transparent);
  overflow: hidden;
}

.linxicard-preview::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(115deg, color-mix(in srgb, var(--c-highlight, #ffffff) 18%, transparent) 0%, color-mix(in srgb, var(--c-highlight, #ffffff) 0%, transparent) 40%);
  pointer-events: none;
}

.linxicard-preview-avatar {
  position: relative;
  width: 96px;
  height: 96px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 2rem;
  font-weight: 700;
  margin-bottom: 1.1rem;
  flex-shrink: 0;
  overflow: hidden;
  box-shadow: 0 8px 20px color-mix(in srgb, var(--c-shadow, #000000) 18%, transparent), 0 0 0 4px color-mix(in srgb, var(--c-highlight, #ffffff) 25%, transparent);
}

.linxicard-preview-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.linxicard-preview-name {
  position: relative;
  font-size: 1.5rem;
  font-weight: 800;
  letter-spacing: -0.01em;
  margin-bottom: 0.5rem;
  word-break: break-word;
}

.linxicard-preview-bio {
  position: relative;
  font-size: 0.95rem;
  line-height: 1.4;
  opacity: 0.92;
  flex-grow: 1;
}

.linxicard-preview-contact {
  position: relative;
  font-size: 0.85rem;
  opacity: 0.85;
  margin: 0.15rem 0;
}

.linxicard-preview-tags {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  justify-content: center;
  margin-top: 0.6rem;
}

.linxicard-preview-tags .tag-pill {
  background: color-mix(in srgb, var(--c-highlight, #ffffff) 18%, transparent);
  color: var(--c-on-accent, #ffffff);
}

.linxicard-preview-request-btn {
  position: relative;
  background: var(--c-surface, #ffffff);
  color: var(--c-brand-deep, #4338ca);
  border: none;
  border-radius: 999px;
  padding: 0.65rem 1.75rem;
  font-weight: 700;
  font-size: 0.9rem;
  cursor: pointer;
  margin: 1.1rem 0;
  box-shadow: 0 8px 20px color-mix(in srgb, var(--c-shadow, #000000) 18%, transparent);
  transition: transform 0.15s, box-shadow 0.15s;
}

.linxicard-preview-request-btn:hover {
  transform: translateY(-1px);
  box-shadow: 0 10px 24px color-mix(in srgb, var(--c-shadow, #000000) 22%, transparent);
}

.linxicard-preview-qr {
  position: relative;
  width: 60px;
  height: 60px;
  padding: 5px;
  background: var(--c-surface, #ffffff);
  border-radius: 8px;
  box-shadow: 0 4px 12px color-mix(in srgb, var(--c-shadow, #000000) 18%, transparent);
  margin-bottom: 0.9rem;
}

.linxicard-preview-brand {
  position: relative;
  font-size: 0.7rem;
  font-weight: 700;
  opacity: 0.85;
  letter-spacing: 0.16em;
}

.linxicard-export-panel {
  width: 100%;
  max-width: 320px;
  background: var(--c-surface-subtle, #f8fafc);
  border: 1px solid var(--c-border-soft, #e5e7eb);
  border-radius: 14px;
  padding: 1.1rem;
  display: flex;
  align-items: center;
  gap: 1rem;
}

.linxicard-qr-block {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  flex-shrink: 0;
}

.linxicard-qr-preview {
  width: 84px;
  height: 84px;
  border-radius: 8px;
  border: 1px solid var(--c-border-soft, #e5e7eb);
  background: var(--c-surface, #ffffff);
  padding: 4px;
}

.linxicard-qr-caption {
  font-size: 0.68rem;
  text-align: center;
  max-width: 90px;
  margin: 0;
}

.linxicard-export-buttons {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  flex: 1;
}

.linxicard-export-buttons button {
  width: 100%;
  text-align: center;
}

/* --- Public Linxi Card page (public/card.html) --- */

.public-card-body {
  background: var(--c-surface-sunken, #f1f5f9);
}

.public-card-main {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
}

.public-card {
  --public-card-color: var(--c-brand, #6366f1);
  width: 100%;
  max-width: 400px;
  background: linear-gradient(160deg, var(--public-card-color), var(--c-inverse-surface, #0f172a));
  border-radius: 24px;
  padding: 3rem 2rem 2rem;
  text-align: center;
  color: var(--c-on-accent, #ffffff);
  box-shadow: 0 20px 40px color-mix(in srgb, var(--c-shadow, #000000) 15%, transparent);
}

.public-card-avatar {
  width: 120px;
  height: 120px;
  border-radius: 50%;
  background: var(--c-surface, #ffffff);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 2.5rem;
  font-weight: 700;
  margin: 0 auto 1.5rem;
  overflow: hidden;
}

.public-card-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.public-card-name {
  font-size: 1.8rem;
  margin: 0 0 0.75rem;
}

.public-card-bio {
  font-size: 1rem;
  opacity: 0.92;
  margin: 0 0 1rem;
}

.public-card-contact {
  font-size: 0.9rem;
  opacity: 0.85;
  margin: 0.2rem 0;
}

.public-card-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  justify-content: center;
  margin: 0.75rem 0;
}

.public-card-tag-pill {
  background: color-mix(in srgb, var(--c-highlight, #ffffff) 18%, transparent);
  border: 1px solid color-mix(in srgb, var(--c-highlight, #ffffff) 30%, transparent);
  color: var(--c-on-accent, #ffffff);
  border-radius: 999px;
  padding: 0.25rem 0.75rem;
  font-size: 0.8rem;
  font-weight: 600;
}

/* Shown inside the Request modal only once a logged-in viewer's own
   info is confirmed available (see publicCard.js) -- a one-click
   typing-saver, not the auto-filled-on-open behavior it replaced. */
.public-card-use-my-info-btn {
  display: block;
  width: 100%;
  margin: 0.5rem 0 1rem;
}

.public-card-use-my-info-btn.hidden {
  display: none;
}

.public-card-request-btn {
  background: var(--c-surface, #ffffff);
  color: var(--c-text, #0f172a);
  border: none;
  border-radius: 999px;
  padding: 0.75rem 2rem;
  font-weight: 600;
  font-size: 1rem;
  cursor: pointer;
  margin: 1.5rem 0 0.5rem;
}

.public-card-qr {
  width: 88px;
  height: 88px;
  padding: 6px;
  background: var(--c-surface, #ffffff);
  border-radius: 10px;
  box-shadow: 0 4px 14px color-mix(in srgb, var(--c-shadow, #000000) 20%, transparent);
  margin: 1rem auto 0;
  display: block;
}

.public-card-brand {
  margin-top: 2.5rem;
  padding-top: 1.5rem;
  border-top: 1px solid color-mix(in srgb, var(--c-highlight, #ffffff) 20%, transparent);
}

.public-card-wordmark {
  font-weight: 700;
  letter-spacing: 0.05em;
  margin-bottom: 0.4rem;
}

.public-card-cta {
  color: var(--c-on-accent, #ffffff);
  font-size: 0.85rem;
  text-decoration: underline;
}

/* Fixed (not absolute) -- unlike .public-card-edit-btn below, this needs
   to stay put in the same viewport corner across every state this page
   can be in (loading, not-found, the read-only card, or the owner's
   editor), not just within .public-card itself. */
.public-card-visit-link {
  position: fixed;
  top: 1rem;
  left: 1rem;
  z-index: 10;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.5rem 1rem;
  background: var(--c-surface, #ffffff);
  color: var(--c-text, #0f172a);
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 600;
  text-decoration: none;
  box-shadow: 0 4px 14px color-mix(in srgb, var(--c-shadow, #000000) 8%, transparent);
}

.public-card {
  position: relative;
}

.public-card-edit-btn {
  position: absolute;
  top: 1rem;
  right: 1rem;
  padding: 0.4rem 0.9rem;
  font-size: 0.8rem;
}

/* Owner-edit view (public/card.html) -- ported from the old SPA
   tab-linxicard tab (still using its .linxicard-editor-grid etc. classes
   above unchanged), just given its own width here since it's no longer
   inside the main app's tab layout. */
.linxicard-editor-page {
  width: 100%;
  max-width: 1000px;
  margin: 0 auto;
  text-align: left;
  color: var(--c-text, #0f172a);
}

.linxicard-editor-page-toolbar {
  margin-bottom: 1rem;
}

/* The editor is routinely taller than the viewport (unlike the plain
   read-only card, which .public-card-main's default centering suits
   fine) -- top-align instead while it's open so it doesn't render
   starting halfway off the top of the screen. Toggled by publicCard.js's
   openLinxiCardEditor()/closeLinxiCardEditor(). */
body.linxicard-editing .public-card-main {
  align-items: flex-start;
  padding-top: 3rem;
}

/* ==========================================================================
   Mobile -- applies app-wide, on top of every rule above. Tables are the
   one generic (non-scoped) rule here: rather than wrap every table in the
   app in a scroll container, forcing table -> display:block on small
   screens turns the table element itself into the horizontal-scroll
   container (the well-known CSS-only responsive-table trick), so every
   existing table/thead/tbody markup keeps working unmodified.
   ========================================================================== */

@media (max-width: 768px) {
  header {
    padding: 0.75rem 1rem;
  }

  /* Desktop positions this relative to the bell (right: 0 off
     .bell-container, which has its own position: relative) -- fine when
     the bell sits near the middle of a wide header, but on a narrow
     mobile screen the bell is close to the right edge, so that same
     math pushes the panel's left side off-screen instead. Switches to
     viewport-fixed and horizontally centered instead, with a fixed top
     offset sized for this breakpoint's own 2-row header (padding above +
     the title row + the workspace-switcher row below it, see
     .header-top's own comment) rather than trying to follow the bell at
     all on mobile. */
  .notification-panel {
    position: fixed;
    top: 110px;
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    width: calc(100vw - 2rem);
    max-width: 340px;
    /* Safety net for a short viewport (e.g. landscape) -- .notification-list
       already caps itself at 400px/scrolls internally, this just makes
       sure the panel as a whole (header + list + footer) can never push
       past the bottom of the screen either. */
    max-height: calc(100vh - 130px);
    overflow-y: auto;
  }

  /* Deliberate two-row layout, not organic wrapping: row 1 is the
     hamburger + title + user menu (workspace-switcher's own `order`
     below pushes it out of this row); row 2 is the workspace switcher at
     full width, which reads far better than a native <select> squeezed
     to whatever width happens to be left over. flex-start (not the base
     rule's space-between) plus .user-info's own margin-left: auto below
     keeps the hamburger flush against the logo on the far left, with
     only the user menu pushed to the far right. */
  .header-top {
    flex-wrap: wrap;
    justify-content: flex-start;
    gap: 0.6rem;
  }

  .header-top .user-info {
    margin-left: auto;
  }

  /* Account type badge (Social/Business/etc, see auth.js's #nav-role) --
     redundant on the cramped mobile header, desktop keeps it. */
  #nav-role {
    display: none;
  }

  header h1 {
    font-size: 1.05rem;
  }

  .header-logo {
    width: 28px;
    height: 28px;
  }

  .workspace-switcher {
    order: 3;
    flex-basis: 100%;
  }

  .workspace-switcher select {
    max-width: none;
    width: 100%;
  }

  .mobile-nav-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* Collapsed by default -- .mobile-nav-toggle (see auth.js) toggles
     `.open`, turning this from the old cramped multi-row wrap of tiny
     chips into a single-column dropdown drawer, full-width, one tab per
     row. Positioned to float below the header rather than pushing page
     content down while its animation runs. */
  #main-nav {
    display: none;
    flex-direction: column;
    gap: 0.15rem;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    margin-top: 0;
    background: var(--c-head-drawer, #1e293b);
    border-top: 1px solid var(--c-head-chip, #374151);
    padding: 0.5rem;
    box-shadow: 0 8px 24px color-mix(in srgb, var(--c-shadow, #000000) 35%, transparent);
    z-index: 400;
    max-height: 70vh;
    overflow-y: auto;
  }

  #main-nav.open {
    display: flex;
  }

  header {
    position: relative;
  }

  #main-nav .nav-tab-btn {
    width: 100%;
    text-align: left;
    padding: 0.65rem 0.8rem;
    font-size: 0.9rem;
  }

  main {
    padding: 0.85rem;
    gap: 1rem;
  }

  .card {
    padding: 1rem;
  }

  /* iOS Safari zooms the entire page in the moment a tapped input's
     *computed* font-size is under 16px -- true of most inputs/selects/
     textareas in this app, which sit at 0.78rem-0.95rem app-wide for a
     tighter desktop look (and plain <input>s with no scoped rule at all,
     e.g. forgot.html/reset.html, fall back to the browser default, which
     is also under 16px). Fixed 16px (not a rem value) guarantees this
     regardless of anyone's browser font-size setting. !important is
     deliberate, not a shortcut: dozens of selectors elsewhere in this
     file set a smaller font-size with higher specificity than a plain
     type selector could ever beat on its own, and duplicating each one
     here would silently rot the moment a new input rule is added
     anywhere else in this file without also being added here. This is
     the one property, in the one place, this stylesheet treats that way.
     Desktop is completely unaffected -- scoped to this breakpoint only. */
  input,
  select,
  textarea {
    font-size: 16px !important;
  }

  table {
    display: block;
    overflow-x: auto;
    white-space: nowrap;
    -webkit-overflow-scrolling: touch;
  }

  .links-layout {
    /* ⚠️ minmax(0, 1fr), NOT 1fr. A bare 1fr is minmax(AUTO, 1fr): the
       track refuses to shrink below its widest item's min-content size,
       so ONE unwrappable child drags the whole three-panel layout past
       the viewport rather than being clipped or scrolled itself. That is
       what a non-wrapping channel row did here. The wrap fixes the
       cause; this makes the next wide thing a local problem instead of a
       page-wide one. */
    grid-template-columns: minmax(0, 1fr);
    /* .card's own mobile padding (above) wins the cascade over
       .links-layout's desktop padding: 0 (same specificity, later in
       source) -- reasserted here so the tab goes back to edge-to-edge
       on mobile, where every rem of width matters far more than on a
       1280px desktop viewport. */
    padding: 0;
  }

  /* Reclaims the horizontal space main's own base padding would
     otherwise spend on every other (non-list-layout) tab -- scoped to
     just the Links tab via :has(), same technique the desktop-only
     1800px-widen rule near .links-layout already uses. */
  /* ⚠️ THE VERTICAL PADDING IS RESTATED HERE, and it has to be. The
     desktop rule above now says `padding: 0` at (1,1,1) -- higher than
     the plain `main` rule in this block -- so without these two lines
     mobile would silently lose the 0.85rem above and below the tab as a
     side effect of a desktop change. */
  main:has(> #tab-links:not(.hidden)) {
    padding-top: 0.85rem;
    padding-bottom: 0.85rem;
    padding-left: 0.4rem;
    padding-right: 0.4rem;
  }

  /* ⚠️ SAME RULE ONE LEVEL DOWN. A grid ITEM also defaults to
     min-width: auto and refuses to shrink below its own content, so the
     track can be honest about its width and the panel inside it still
     push past the edge. This file has recorded the flex twin of this
     twice, on .contact-profile-fields and .profile-field. */
  .links-list-panel,
  .links-detail-panel,
  .links-card-panel {
    min-width: 0;
  }

  /* ⚠️ ALL THREE ORDERS ARE STATED, ASCENDING. Only one of them needs
     to move to get this stack, and writing only that one leaves the
     other two relying on an implicit 0 and an implicit -1 -- so the next
     edit anywhere in this block can reorder the page without touching
     an `order` at all. Read together, these three ARE the mobile
     layout:

         1  the conversation   -- what you came here for
         2  the Linxi Label + the contact record
         3  the contact list   -- last, because it is the longest and
                                  the one you scroll rather than read

     Grid `order` restacks visually and does NOT touch the DOM, so
     keyboard and screen-reader order is unaffected by any of it. */
  .links-list-panel {
    order: 3;
    border-right: none;
    /* ⚠️ NO border-bottom. It had a hairline separating it from the card
       panel beneath; it is the LAST thing on the page now, and a rule
       under the final row marks the end of nothing. */
    border-bottom: none;
    /* Kept: this list is hundreds of contacts on a real account, and an
       uncapped one at the foot of the page makes the page itself
       enormous. Capped, it is an ordinary scrolling list. */
    max-height: 50vh;
    padding: 0.6rem;
  }

  /* ⚠️ THE CARD PANEL STILL CARRIED ITS DESKTOP RULES HERE. It is the
     third COLUMN on a wide screen, so it has a border-LEFT, an 80vh cap
     and its own overflow-y -- all three wrong for a stacked block on a
     phone, and the cap in particular put a nested scroll region in the
     middle of the page, which traps a thumb dragging through it. The
     page scrolls; this does not need to. */
  .links-card-panel {
    order: 2;
    border-left: none;
    border-bottom: 3px solid var(--c-border, #e2e8f0);
    max-height: none;
    overflow-y: visible;
    padding: 0.65rem 0.6rem 0.85rem;
    margin-bottom: 0.6rem;
  }

  /* On mobile the conversation itself is what you actually came here
     for, so it is shown first -- reading or replying does not mean
     scrolling past the whole contact list every time. See the ordering
     note above .links-list-panel for the full stack.

     ⚠️ The border-bottom is a deliberately VISIBLE seam, not the
     hairline the list used to carry: a panel that is first needs
     something marking where it ends, and what follows it now is the
     Linxi Label card rather than the search panel it was written
     against. The card carries the matching seam below itself. */
  .links-detail-panel {
    max-height: none;
    order: 1;
    padding: 0.65rem 0.6rem 0.85rem;
    border-bottom: 3px solid var(--c-border, #e2e8f0);
    margin-bottom: 0.6rem;
  }

  /* Tighten the contact profile header (avatar + name/phone/email +
     badges) so it spends less vertical space, leaving more of the
     panel for the actual conversation below it. */
  .contact-profile-card {
    padding-bottom: 0.6rem;
    margin-bottom: 0.6rem;
  }

  .profile-header {
    gap: 0.5rem;
  }

  /* ⚠️ The mobile override that used to live here is GONE, because the
     base rule now does exactly what it did. Leaving a byte-identical
     copy inside a media query is a second place for the same decision
     to be edited, and the one nobody updates is the one that renders. */

  #profile-avatar.avatar-large {
    width: 42px;
    height: 42px;
    font-size: 0.95rem;
  }

  .contact-groups {
    margin-top: 0.5rem;
  }

  /* Chat bubbles were capped at 70% of the panel -- fine on a wide
     desktop column, but on a narrow mobile screen (now widened by the
     rules above) that cap wastes exactly the width this change is
     trying to reclaim. */
  .chat-row {
    max-width: 88%;
  }

  /* Retract/collapse is a desktop-only affordance -- mobile shows one
     full panel at a time via the drill-down system below instead, so a
     "collapse to a thin rail" state doesn't apply here. Forces
     grid-template-columns back to a single column and each panel's own
     .webmail-panel-content back to its real display value, regardless
     of any collapsed class left over from a desktop session (classes
     persist in sessionStorage, see webmail.js).

     .card.webmail-layout (not bare .webmail-layout) is deliberate, not
     stylistic -- the actual element carries both classes (see
     index.html: <section class="card webmail-layout">), and the
     desktop .webmail-layout { grid-template-columns: 180px 340px 1fr }
     rule sits LATER in this file, at equal specificity to a bare
     .webmail-layout selector here. That meant grid-template-columns was
     NEVER actually collapsing to a single column on mobile at all --
     the three-column desktop grid stayed in effect underneath whichever
     panel the drill-down/drawer logic was showing, squeezing it into a
     180px or 340px track. Almost certainly the single biggest
     contributor to this tab looking broken on mobile. Referencing both
     classes the element actually has raises specificity enough to win
     regardless of source order, same fix already applied to several
     other rules in this block. */
  .card.webmail-layout,
  .card.webmail-layout.folders-collapsed,
  .card.webmail-layout.list-collapsed,
  .card.webmail-layout.folders-collapsed.list-collapsed {
    grid-template-columns: 1fr;
  }

  /* .webmail-layout prefix -- same source-order reasoning as
     .webmail-folder-panel's own comment below: the desktop
     .webmail-panel-toggle rule (later in this file) was winning the
     cascade over a bare-class override here, leaving the desktop-only
     "collapse to a thin rail" chevron button visibly cluttering each
     panel on mobile even though it does nothing useful there (mobile
     uses the drill-down/drawer system instead). */
  .webmail-layout .webmail-panel-toggle {
    display: none;
  }

  .webmail-folder-panel .webmail-panel-content,
  .webmail-layout.folders-collapsed .webmail-folder-panel .webmail-panel-content {
    display: block;
  }

  .webmail-list-panel .webmail-panel-content,
  .webmail-layout.list-collapsed .webmail-list-panel .webmail-panel-content {
    display: flex;
  }

  /* Drill-down navigation: list <-> reading, controlled by
     .webmail-layout's own data-mobile-view attribute (see webmail.js's
     setWebmailMobileView) -- message list -> tap a message -> full
     reading pane, replacing the list instead of stacking both. Folders
     is no longer a third drill-down step here (see the drawer override
     below) -- it overlays on top of whichever of these two is showing
     instead of replacing it, Gmail-style, so switching folders/accounts
     never has to leave the list's scroll position or context at all.
     Defaults to "list" (set whenever loadWebmailFolders runs).
     max-height: none on both -- with only one ever shown, the page
     itself scrolls rather than a nested scroll container, which feels
     more natural on a touch device than the desktop 3-column layout's
     fixed-height columns. */
  .webmail-list-panel,
  .webmail-reading-panel {
    display: none;
    border: none;
    max-height: none;
  }

  .webmail-layout[data-mobile-view="list"] .webmail-list-panel {
    display: flex;
  }

  .webmail-layout[data-mobile-view="reading"] .webmail-reading-panel {
    display: block;
  }

  /* Folder panel becomes a Gmail-style slide-in drawer instead of its
     own drill-down "view" -- always present just off-screen (not
     display: none, which can't be transitioned) and slid into view via
     transform, so opening it feels like a panel sliding over the list
     rather than a full screen navigation away from it. Independent of
     data-mobile-view above -- can open from the list. */
  /* .webmail-layout prefix (not a bare .webmail-folder-panel selector)
     is deliberate, not stylistic -- this media block sits BEFORE the
     desktop .webmail-folder-panel rule (border-right/display/etc.) in
     this file's source order, so a same-specificity bare-class rule
     here would lose the cascade to that later, unconditional rule for
     any property both set (border-right most notably), even though this
     one's condition is the one actually true on a narrow screen. The
     extra ancestor class bumps specificity enough to win regardless of
     source order -- same fix already applied to .links-layout earlier
     in this file for the identical reason. */
  .webmail-layout .webmail-folder-panel {
    display: flex;
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    width: 260px;
    max-width: 78vw;
    z-index: 1001;
    background: var(--c-surface, #fff);
    border-right: none;
    box-shadow: 2px 0 24px color-mix(in srgb, var(--c-shadow, #000000) 18%, transparent);
    transform: translateX(-100%);
    transition: transform 0.22s ease;
  }

  .webmail-layout[data-folder-drawer-open="true"] .webmail-folder-panel {
    transform: translateX(0);
  }

  /* Same source-order reasoning as .webmail-folder-panel above --
     .webmail-folder-drawer-backdrop's base display: none (added
     alongside the desktop rules, later in this file) would otherwise
     win over a bare-class override here. */
  .webmail-layout .webmail-folder-drawer-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 1000;
    background: color-mix(in srgb, var(--c-scrim, #0f172a) 45%, transparent);
    border: none;
    padding: 0;
    cursor: default;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.22s ease;
  }

  .webmail-layout[data-folder-drawer-open="true"] .webmail-folder-drawer-backdrop {
    opacity: 1;
    pointer-events: auto;
  }

  /* .webmail-layout prefix, same source-order reasoning as
     .webmail-folder-panel's own comment above -- .webmail-mobile-nav-btn's
     base display: none (desktop section, later in this file) was
     winning the cascade over a bare-class override here despite this
     being the rule whose condition was actually true on a narrow
     screen. In practice this meant the "Back" button in the reading
     pane was invisible on mobile -- once you opened a message there was
     no way back to the list without leaving the tab and re-entering,
     which is likely a large part of why this tab kept getting reported
     as broken even after the earlier overflow fixes. */
  .webmail-layout .webmail-mobile-nav-btn {
    display: inline-flex;
  }

  #webmail-mobile-back-btn {
    margin-bottom: 0.75rem;
  }

  /* Hamburger icon (opens the folder drawer) -- larger tap target and
     a plain icon glyph instead of the "← Folders" text button this
     replaced, matching the drawer-trigger convention every Gmail-style
     mail app uses instead of a labeled nav button. */
  .webmail-hamburger-btn {
    order: -1;
    background: transparent;
    border: 1px solid var(--c-border-strong, #cbd5e1);
    border-radius: 8px;
    font-size: 1.1rem;
    line-height: 1;
    padding: 0.5rem 0.65rem;
    color: var(--c-text-2, #334155);
    cursor: pointer;
  }

  .webmail-hamburger-btn:hover {
    background: var(--c-surface-sunken, #f1f5f9);
  }

  /* Compose moves off the inline toolbar (where it competed for space
     with Search on a narrow screen) onto a floating action button
     instead -- same handler, just a second, thumb-reachable trigger for
     it. Desktop keeps the inline button; this stays display: none there
     (see the base rule above). */
  #webmail-compose-btn {
    display: none;
  }

  /* Same source-order reasoning as .webmail-folder-panel above --
     .webmail-compose-fab's base display: none (added alongside the
     desktop rules, later in this file) would otherwise win over a
     bare-class override here. */
  .webmail-layout .webmail-compose-fab {
    display: flex;
    align-items: center;
    justify-content: center;
    position: fixed;
    right: 1.1rem;
    bottom: 1.1rem;
    width: 56px;
    height: 56px;
    border-radius: 50%;
    background: var(--c-brand, #6366f1);
    color: var(--c-on-accent, #ffffff);
    font-size: 1.3rem;
    border: none;
    box-shadow: 0 6px 20px color-mix(in srgb, var(--c-brand, #6366f1) 45%, transparent);
    z-index: 20;
    cursor: pointer;
  }

  .webmail-compose-fab:hover {
    background: var(--c-brand-strong, #4f46e5);
  }

  /* Only meaningful in the list view -- reading a message covers the
     same corner with the message body, and the FAB would otherwise sit
     on top of it looking like a stray floating button unrelated to what
     the panel scrolled to. */
  .webmail-layout[data-mobile-view="reading"] .webmail-compose-fab {
    display: none;
  }

  .dynamic-fields,
  .wizard-verify-buttons {
    grid-template-columns: 1fr;
  }

  .upgrade-modal-comparison {
    flex-direction: column;
  }

  .account-usage-row {
    flex-wrap: wrap;
  }

  .account-usage-track {
    width: auto;
    flex: 1 1 120px;
  }

  .modal-overlay {
    padding: 0.75rem;
  }

  .linxicard-preview-wrap {
    margin-top: 1rem;
  }
}

@media (max-width: 480px) {
  .user-info {
    font-size: 0.8rem;
    gap: 0.4rem;
  }

  .ws-linxi-number-display {
    font-size: 1.3rem;
  }

  .public-card {
    padding: 2.25rem 1.25rem 1.5rem;
  }

  .public-card-avatar {
    width: 96px;
    height: 96px;
    font-size: 2rem;
  }

  .linxicard-preview {
    padding: 1.5rem 1rem;
  }
}

/* --- PWA install banner --- */

.install-banner {
  position: fixed;
  bottom: 1rem;
  left: 50%;
  transform: translateX(-50%);
  background: var(--c-brand-strong, #4f46e5);
  color: var(--c-on-accent, #ffffff);
  padding: 0.75rem 1.25rem;
  border-radius: 2rem;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  box-shadow: 0 4px 20px color-mix(in srgb, var(--c-brand-strong, #4f46e5) 40%, transparent);
  z-index: 9999;
  font-size: 0.9rem;
  white-space: nowrap;
}

.install-banner button {
  background: var(--c-on-accent, #ffffff);
  color: var(--c-brand-strong, #4f46e5);
  border: none;
  padding: 0.35rem 0.85rem;
  border-radius: 1rem;
  font-weight: 600;
  cursor: pointer;
  font-size: 0.85rem;
}

.install-banner #install-dismiss {
  background: transparent;
  color: var(--c-on-accent, #ffffff);
  padding: 0.25rem;
  font-size: 1rem;
}

/* --- Real-time (Socket.io) --- */

.nav-unread-badge {
  display: none;
  background: var(--c-danger, #ef4444);
  color: var(--c-on-fill, #ffffff);
  border-radius: 999px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  font-size: 0.68rem;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
  margin-left: 0.35rem;
}

/* --- Reply box send spinner --- */

.btn-spinner {
  display: inline-block;
  width: 12px;
  height: 12px;
  border: 2px solid color-mix(in srgb, var(--c-highlight, #ffffff) 50%, transparent);
  border-top-color: var(--c-on-accent, #ffffff);
  border-radius: 50%;
  margin-right: 0.4rem;
  vertical-align: -1px;
  animation: btn-spinner-spin 0.6s linear infinite;
}

.btn-spinner.hidden {
  display: none;
}

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

.links-active-number {
  margin: 0 0 0.6rem;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--c-brand, #6366f1);
}

.links-active-number:empty {
  display: none;
}

/* --- My Linxi Email (Account tab) --- */

.email-prefix-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 1rem;
}

.email-prefix-row input {
  flex: 1;
  margin-bottom: 0;
}

/* --- Admin Settings (SMTP + cPanel) --- */


@media (max-width: 800px) {
  }

.admin-settings-toast {
  position: fixed;
  bottom: 1rem;
  right: 1rem;
  background: var(--c-inverse-surface, #0f172a);
  color: var(--c-on-accent, #ffffff);
  padding: 0.75rem 1.25rem;
  border-radius: 8px;
  font-size: 0.9rem;
  box-shadow: 0 8px 24px color-mix(in srgb, var(--c-shadow, #000000) 25%, transparent);
  z-index: 9999;
  max-width: 360px;
  opacity: 0;
  transform: translateY(8px);
  pointer-events: none;
  transition: opacity 0.2s, transform 0.2s;
}

.admin-settings-toast.visible {
  opacity: 1;
  transform: translateY(0);
}

/* --- Unified email + SMS: chat thread channel awareness --- */

.chat-email-subject {
  font-weight: 600;
  font-size: 0.8rem;
  opacity: 0.8;
  margin-bottom: 0.15rem;
}

.chat-bubble-email.chat-bubble-outbound {
  background: var(--c-teal, #0d9488);
  color: var(--c-on-fill, #ffffff);
}

.chat-bubble-email.chat-bubble-inbound {
  background: var(--c-success-tint-2, #dcfce7);
  color: var(--c-success-text, #166534);
}

.chat-opened-badge {
  font-size: 0.7rem;
}

/* --- Reply box: channel toggle + email fields --- */

/* ⚠️⚠️ flex-wrap IS LOAD-BEARING, AND ITS ABSENCE OVERFLOWED THE WHOLE
   PAGE. This row was four buttons when it was written and is now EIGHT:
   All / Linxi / SMS / Email / WhatsApp / Messenger / Instagram, plus the
   E2E Secure launcher past a divider. At ~0.8rem with 0.75rem of
   horizontal padding each and a 0.5rem gap, its min-content width is
   comfortably past 700px -- wider than a phone, and wider than the
   middle column of a 1280px desktop.

   ⚠️ AND A NON-WRAPPING FLEX ROW DOES NOT JUST OVERFLOW ITSELF. The
   mobile grid track is 1fr, which is minmax(AUTO, 1fr), so the track
   grows to its widest item's min-content size and takes the whole
   .links-layout past the viewport with it -- reported, accurately, as
   "everything on the page is overflowing to the right".

   ⚠️ NOT SCOPED TO MOBILE, deliberately: with room to spare, wrapping
   changes nothing, and the row is already too wide for the desktop
   conversation column once every channel is on. A mobile-only wrap
   would leave the identical bug at 1000px. Every other flex row in this
   tab (.filter-chip-row, .profile-header-actions, .carrier-badge-row,
   .group-pills) already wraps; this was the only one that did not. */
.channel-toggle {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 0.5rem;
}

/* Same later-rule-wins-the-cascade-tie issue documented elsewhere in
   this file -- .channel-toggle's own display: flex above is defined
   after .hidden in source order, so without this ID-scoped override the
   reply-channel toggle (see links.js) could never actually be hidden. */
#reply-channel-toggle.hidden {
  display: none;
}

.channel-btn {
  padding: 0.35rem 0.75rem;
  border-radius: 1rem;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface, #fff);
  color: var(--c-brand-strong, #4f46e5);
  cursor: pointer;
  font-size: 0.8rem;
}

.channel-btn.active {
  background: var(--c-brand-strong, #4f46e5);
  color: var(--c-on-accent, #ffffff);
  border-color: var(--c-brand-strong, #4f46e5);
}

.thread-channel-toggle {
  margin-top: 0.75rem;
}

/* ⚠️ .reply-subject-input IS GONE with the field it styled -- the email
   reply derives "Re: <the subject above it>" rather than asking. */
.from-row {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 0.4rem;
}

.from-row input {
  flex: 1;
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
  font-size: 0.9rem;
}

.reply-sending-as {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  margin-bottom: 0.4rem;
  font-size: 0.85rem;
  flex-wrap: wrap;
}

.reply-sending-as select {
  padding: 0.3rem 0.5rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
  font-size: 0.85rem;
}

/* --- Compose tab: campaign channel selector + email fields --- */

.campaign-channel-selector {
  margin: 0.75rem 0;
}

.channel-options {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  margin: 0.5rem 0;
}

.channel-option {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  cursor: pointer;
  font-weight: normal;
}

#email-campaign-fields .form-input {
  width: 100%;
  margin-bottom: 0.75rem;
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
  font-size: 0.95rem;
}

#email-campaign-fields textarea {
  width: 100%;
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
  font-size: 0.95rem;
  margin-bottom: 0.5rem;
}

.track-opens-row label {
  font-weight: normal;
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

/* --- Opt-in confirmation modal --- */

#optin-confirm-modal-overlay .modal {
  max-width: 480px;
}

#optin-channel-list {
  color: var(--c-text-3, #475569);
}

/* --- Flow (Send Log): channel filters + status badges --- */

.log-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 1rem;
}

.log-filter-pill {
  padding: 0.25rem 0.75rem;
  border-radius: 1rem;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface, #fff);
  /* REQUIRED. The global `button` rule sets color:#fff, so a pill that
     only sets a white background inherits white-on-white and is
     invisible until hover -- where `button:hover` swaps the background
     for the gradient and the white text finally has something to sit
     on. That was REQ-0023. `.flow-view-btn` and `.flow-mode-btn` beside
     it both set an explicit colour; this one was the odd rule out. */
  color: var(--c-brand, #6366f1);
  cursor: pointer;
  font-size: 0.85rem;
  font-weight: 600;
}

/* Also overrides `button:hover`, which would otherwise paint the
   gradient behind text that is now purple. */
.log-filter-pill:hover:not(.active) {
  background: var(--c-brand-tint, #f5f3ff);
  border-color: var(--c-brand-border, #c7d2fe);
  color: var(--c-brand-strong, #4f46e5);
  box-shadow: none;
  transform: none;
}

.log-filter-pill.active {
  background: var(--c-brand-strong, #4f46e5);
  color: var(--c-on-accent, #ffffff);
  border-color: var(--c-brand-strong, #4f46e5);
}

.status-opened {
  background: var(--c-success-tint-2, #dcfce7);
  color: var(--c-success-text, #166534);
}

.status-bounced {
  background: var(--c-warning-border, #fde68a);
  color: var(--c-warning-text, #92400e);
}

/* --- Admin Settings: merged SMTP+IMAP "Email Configuration" panel --- */

.admin-email-port-row {
  display: flex;
  gap: 1rem;
}

.admin-email-port-row > div {
  flex: 1;
}

/* Generic two-field-per-row layout for a form -- same pattern as
   .admin-email-port-row above, under a name that isn't scoped to one
   feature. Used by the New Contact modal (name/phone/email pairs, and
   its dynamic custom-field list) so a form of individually-short fields
   doesn't stack into one very tall single column. */
.form-row {
  display: flex;
  gap: 1rem;
}

.form-row > div {
  flex: 1;
  min-width: 0;
}

#new-contact-extra-fields {
  display: flex;
  flex-wrap: wrap;
  gap: 0 1rem;
}

#new-contact-extra-fields > div {
  flex: 1 1 45%;
  min-width: 160px;
}

.admin-email-section-heading {
  margin: 0 0 0.75rem;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--c-brand, #6366f1);
}

.admin-email-section-heading:not(:first-child) {
  margin-top: 1.75rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--c-border-soft, #e5e7eb);
}

/* --- Brand Logo & Favicon (Admin Settings) --- */

.logo-preview-row {
  display: flex;
  gap: 1.25rem;
  align-items: flex-end;
  flex-wrap: wrap;
  margin: 0.5rem 0 1.25rem;
}

.logo-preview-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
}

.logo-preview-img {
  width: 32px;
  height: 32px;
  object-fit: contain;
}

/* ⚠️ THE SIZE ON THE LABEL, ACTUALLY. The 16px tile rendered in the 32px
   box above and was upscaled 2x -- so the one row whose whole purpose is
   "what does this mark become in a tab" was showing something a browser
   never draws. The file was always right; the box was not. */
.logo-preview-img-sm {
  width: 16px;
  height: 16px;
}

/* ⚠️ NOT true size, deliberately, and the label names the FILE rather
   than the box. Five 192px tiles in a row is 960px of strip, and these
   two exist to check detail -- whether the mark holds together at all --
   not scale. 16 and 32 are the ones where scale is the question. */
.logo-preview-img-lg {
  width: 60px;
  height: 60px;
}

.logo-preview-og {
  /* ⚠️ The OG card is the one preview worth showing at a readable size:
     it is a picture with text in it, and 120px wide is too small to see
     whether that text survives the crop. Aspect-locked to 1.91:1, which
     is the frame every platform renders it in. */
  .logo-preview-og-lg { width: 100%; max-width: 382px; height: auto; aspect-ratio: 1200 / 630; }
  width: 120px;
  height: 63px;
  object-fit: cover;
  border-radius: 4px;
  border: 1px solid var(--c-border, #e2e8f0);
}

.logo-preview-label {
  font-size: 0.7rem;
  color: var(--c-text-dim, #94a3b8);
}

.logo-upload-area {
  border: 2px dashed var(--c-border, #e2e8f0);
  border-radius: 0.75rem;
  padding: 2rem;
  text-align: center;
  transition: all 0.2s;
}

.logo-upload-area.dragover {
  border-color: var(--c-brand, #6366f1);
  background: var(--c-brand-tint, #f5f3ff);
}

.logo-upload-icon {
  font-size: 2.5rem;
}

.logo-upload-title {
  margin: 0.5rem 0;
  font-weight: 600;
}

.logo-upload-hint {
  margin: 0;
  font-size: 0.85rem;
}

.logo-upload-choose-btn {
  margin-top: 0.75rem;
  display: inline-block;
  cursor: pointer;
}

.logo-uploading {
  text-align: center;
  padding: 2rem;
}

#admin-email-form .table-toolbar {
  margin-top: 1.25rem;
  flex-wrap: wrap;
}

/* Generic fallback so any modal's form fields (this one included) get
   real styling by default -- same bug class as the form-specific rules
   above (a plain <label>/<input> with no matching selector renders as
   bare, cramped browser defaults) fixed once here instead of needing a
   new #some-modal-form entry added every time a modal gets a form. */
.modal label {
  display: block;
  font-weight: 600;
  font-size: 0.9rem;
  margin: 0.75rem 0 0.3rem;
}

.modal input[type="text"],
.modal input[type="email"],
.modal input[type="password"],
.modal textarea {
  display: block;
  width: 100%;
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
  font-size: 0.95rem;
  font-family: inherit;
  margin-bottom: 0.5rem;
}

/* --- Webmail suite (tab-webmail) -- same 2/3-panel grid convention as
   .links-layout above, extended to a 3rd folder-sidebar column. ---

   Full-bleed: this is the one tab that wants the whole viewport width
   (a reading pane benefits from it far more than any other tab's
   content does) -- :has() is well-supported in every evergreen browser
   at this point, so this expands `main` itself only while tab-webmail
   is the visible tab, rather than a viewport-relative negative-margin
   breakout hack that risks a stray horizontal scrollbar. Every other
   tab keeps main's normal 1280px max-width untouched. */
body:has(#tab-webmail:not(.hidden)) main {
  max-width: none;
}

.webmail-layout {
  display: grid;
  grid-template-columns: 180px 340px 1fr;
  gap: 0;
  padding: 0;
  overflow: hidden;
  transition: grid-template-columns 0.18s ease;
}

/* Folders/list panels retract toward the left via
   .webmail-panel-toggle (public/js/webmail.js) collapsing their grid
   column down to a thin rail -- .webmail-panel-content is what actually
   disappears (display: none), the rail stays just wide enough to hold
   the toggle button so it's always reachable to expand again. */
.webmail-layout.folders-collapsed {
  grid-template-columns: 28px 340px 1fr;
}

.webmail-layout.list-collapsed {
  grid-template-columns: 180px 28px 1fr;
}

.webmail-layout.folders-collapsed.list-collapsed {
  grid-template-columns: 28px 28px 1fr;
}

.webmail-panel-toggle {
  display: block;
  width: 100%;
  flex-shrink: 0;
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
  color: var(--c-text-dim, #94a3b8);
  font-size: 1rem;
  line-height: 1;
  padding: 0.5rem;
  cursor: pointer;
  transition: transform 0.18s ease;
}

.webmail-panel-toggle:hover {
  color: var(--c-text-2, #334155);
  background: var(--c-surface-subtle, #f8fafc);
}

.webmail-layout.folders-collapsed .webmail-folder-panel .webmail-panel-toggle,
.webmail-layout.list-collapsed .webmail-list-panel .webmail-panel-toggle {
  transform: rotate(180deg);
}

.webmail-layout.folders-collapsed .webmail-folder-panel .webmail-panel-content,
.webmail-layout.list-collapsed .webmail-list-panel .webmail-panel-content {
  display: none;
}

.webmail-folder-panel {
  border-right: 1px solid var(--c-border, #e2e8f0);
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.webmail-folder-panel .webmail-panel-content {
  padding: 1rem 0.75rem;
  min-width: 0;
  overflow: hidden;
}

/* Mobile-only nav buttons -- the hamburger (opens the folder drawer)
   and "Back" (leaves the reading panel) -- see the mobile override in
   this file that shows them, and webmail.js's setWebmailMobileView/
   setWebmailFolderDrawerOpen. */
.webmail-mobile-nav-btn {
  display: none;
}

/* Backdrop + FAB are mobile-only affordances (Gmail-style folder
   drawer + floating compose button) -- hidden outright on desktop,
   where the collapsible-column folder panel and inline Compose button
   already cover the same jobs. See the mobile override below for their
   real shown styling. */
.webmail-folder-drawer-backdrop,
.webmail-compose-fab {
  display: none;
}

.webmail-account-select {
  width: 100%;
  font-size: 0.8rem;
  padding: 0.3rem 0.4rem;
}

.webmail-folder-list {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  margin-top: 0.75rem;
}

.webmail-folder-btn {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.45rem 0.6rem;
  border: none;
  border-radius: 6px;
  background: none;
  text-align: left;
  cursor: pointer;
  font-size: 0.9rem;
  color: var(--c-text-2, #334155);
}

.webmail-folder-btn:hover {
  background: var(--c-surface-sunken, #f1f5f9);
}

.webmail-folder-btn.active {
  background: var(--c-brand-strong, #4f46e5);
  color: var(--c-on-accent, #ffffff);
}

/* Same "later same-specificity rule beats the base .hidden" issue already
   worked around elsewhere in this file (#links-search-panel.hidden etc.)
   -- .webmail-folder-btn's own display: flex above is defined after
   .hidden in source order, so it would otherwise win the cascade tie. */
.webmail-folder-btn.hidden {
  display: none;
}

/* .badge's own color: #fff (its default for colored status pills
   elsewhere) would otherwise leave this invisible -- white text on the
   folder button's own transparent/white background -- whenever the
   button isn't the active one. */
.webmail-folder-badge {
  font-size: 0.7rem;
  color: var(--c-brand-strong, #4f46e5);
}

.webmail-folder-btn.active .webmail-folder-badge {
  color: var(--c-on-accent, #ffffff);
}

/* .badge's own display: inline-block is defined after .hidden in this
   file, so it would otherwise win the cascade tie (same issue as
   .webmail-folder-btn.hidden above) -- explicit override keeps the
   unread-count badges actually hideable. */
.webmail-folder-badge.hidden {
  display: none;
}

.webmail-list-panel {
  border-right: 1px solid var(--c-border, #e2e8f0);
  display: flex;
  flex-direction: column;
  min-width: 0;
  /* Fills the viewport below the header/nav (roughly 200px of chrome)
     instead of the old flat 80vh, which left a visible ~20vh gap of
     empty page under the whole 3-column layout on desktop. */
  max-height: calc(100vh - 200px);
}

/* .webmail-message-list's own flex: 1 (below) needs to be a direct flex
   child of a flex column to size correctly -- this wrapper (added so
   the toggle button above it can sit outside what collapses) has to
   carry that same flex context down to it, or the message list loses
   its scrollable fill-the-remaining-height behavior. */
.webmail-list-panel .webmail-panel-content {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  overflow: hidden;
}

.webmail-toolbar {
  padding: 0.75rem;
  border-bottom: 1px solid var(--c-border-soft, #e5e7eb);
}

.webmail-toolbar input[type="search"] {
  flex: 1;
  padding: 0.4rem 0.6rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
  font-size: 0.85rem;
}

.webmail-message-list {
  flex: 1;
  overflow-y: auto;
}

.webmail-list-empty {
  padding: 1rem;
}

.webmail-message-row {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.65rem 0.75rem;
  border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9);
  cursor: pointer;
}

.webmail-message-row:hover {
  background: var(--c-surface-subtle, #f8fafc);
}

.webmail-message-row.active {
  background: var(--c-brand-tint-2, #eef2ff);
}

.webmail-message-row.unread .webmail-message-from,
.webmail-message-row.unread .webmail-message-subject {
  font-weight: 700;
}

.webmail-message-row-top {
  display: flex;
  justify-content: space-between;
  gap: 0.5rem;
  font-size: 0.85rem;
}

/* Row main-axis flex items default to min-width: auto (a horizontal
   flex row, unlike the column-direction .webmail-message-row this sits
   in), so without this the sender name -- or, in Sent/Drafts, a
   "To: name1, name2, name3" list that can run much longer -- refused to
   shrink below its own content width. On a narrow mobile screen that
   pushed the whole row (and with it .webmail-layout, .card, and main)
   wider than the viewport instead of truncating like
   .webmail-message-subject below it already does. */
.webmail-message-from {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.webmail-message-row-top .muted {
  flex-shrink: 0;
}

.webmail-message-subject {
  font-size: 0.85rem;
  color: var(--c-text-3, #475569);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.webmail-pagination {
  padding: 0.5rem 0.75rem;
  border-top: 1px solid var(--c-border-soft, #e5e7eb);
  justify-content: center;
  align-items: center;
  gap: 1rem;
}

.webmail-reading-panel {
  padding: 1rem 1.5rem;
  /* Matches .webmail-list-panel's height exactly -- grid sibling
     columns stretch to the row's tallest member by default, so keeping
     both at the same calc() means the row height is driven by this
     value consistently rather than whichever panel happens to have
     more content on a given message. */
  max-height: calc(100vh - 200px);
  overflow-y: auto;
}

.webmail-action-toolbar {
  flex-wrap: wrap;
  margin-bottom: 1rem;
}

/* Subject and From/To are real email data, not app copy -- a long
   subject or address with no spaces (unlike ordinary wrapping text)
   won't break onto a new line by default and can push the panel (and
   the page) wider than the viewport on mobile instead. */
.webmail-message-header h3,
.webmail-message-header p {
  overflow-wrap: anywhere;
}

.webmail-message-header h3 {
  margin: 0 0 0.4rem;
}

.webmail-message-header p {
  margin: 0.15rem 0;
}

.webmail-detail-body {
  white-space: pre-wrap;
  word-break: break-word;
  margin-top: 1rem;
  line-height: 1.5;
}

.webmail-detail-body iframe {
  display: block;
  width: 100%;
  border: none;
  /* Real height is set in JS (webmailRenderDetailBody's load handler)
     once the srcdoc content is measured -- this is only the fallback
     while that's pending, or if measuring fails. .webmail-reading-panel
     (the ancestor scroll container) is what actually scrolls once the
     email is taller than the viewport, not this iframe itself. */
  min-height: 200px;
}

.webmail-attachments {
  margin-top: 1.25rem;
  padding-top: 1rem;
  border-top: 1px solid var(--c-border-soft, #e5e7eb);
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.webmail-attachments.hidden {
  display: none;
}

.webmail-attachment-link {
  color: var(--c-brand-strong, #4f46e5);
  text-decoration: none;
  font-size: 0.85rem;
  overflow-wrap: anywhere;
}

.webmail-attachment-link:hover {
  text-decoration: underline;
}

/* --- Linxi Sender Score (Account tab, Admin Users, public card) --- */

.score-header {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  flex-wrap: wrap;
  margin-bottom: 1.25rem;
}

.score-circle {
  width: 100px;
  height: 100px;
  border-radius: 50%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-weight: bold;
  border: 4px solid currentColor;
  flex-shrink: 0;
  color: var(--c-brand, #6366f1);
}

.score-circle.tier-standard { color: var(--c-brand, #6366f1); }
.score-circle.tier-trusted { color: var(--c-success-alt, #10b981); }
.score-circle.tier-verified_pro { color: var(--c-brand-alt, #8b5cf6); }
.score-circle.tier-linxi_elite { color: var(--c-warning, #f59e0b); }
.score-circle.tier-warning { color: var(--c-warning, #f59e0b); }
.score-circle.tier-restricted { color: var(--c-danger, #ef4444); }
.score-circle.tier-suspended { color: var(--c-danger-darkest, #991b1b); }

#score-value {
  font-size: 1.5rem;
  line-height: 1;
}

.score-label {
  font-size: 0.7rem;
  opacity: 0.7;
}

.score-info {
  flex: 1;
  min-width: 200px;
}

.tier-badge {
  display: inline-block;
  padding: 0.25rem 0.75rem;
  border-radius: 1rem;
  font-size: 0.8rem;
  font-weight: 600;
  margin-bottom: 0.5rem;
  color: var(--c-on-accent, #ffffff);
}

.score-stats {
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
  font-size: 0.85rem;
  color: var(--c-text-3, #475569);
  margin-top: 0.4rem;
}

.score-breakdown,
.domain-verification {
  margin-top: 1.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--c-border-soft, #e5e7eb);
}

.score-breakdown h4,
.domain-verification h4,
.score-penalties-heading {
  margin: 0 0 0.6rem;
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--c-text-muted, #64748b);
}

.score-penalties-heading {
  color: var(--c-danger, #ef4444);
}

.score-history-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9);
  font-size: 0.85rem;
  gap: 1rem;
}

.score-history-item:last-child {
  border-bottom: none;
}

.score-history-desc {
  color: var(--c-text-2, #334155);
}

.score-history-date {
  color: var(--c-text-dim, #94a3b8);
  font-size: 0.75rem;
  white-space: nowrap;
}

.points-positive { color: var(--c-success-alt, #10b981); font-weight: 600; }
.points-negative { color: var(--c-danger, #ef4444); font-weight: 600; }

.domain-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.4rem 0;
  font-size: 0.85rem;
  border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9);
}

.domain-row:last-child {
  border-bottom: none;
}

.verify-domain-form {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.75rem;
}

.verify-domain-form input {
  flex: 1;
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
  font-size: 0.9rem;
}

#verify-instructions {
  margin-top: 0.75rem;
  padding: 0.75rem;
  background: var(--c-surface-subtle, #f8fafc);
  border-radius: 6px;
  font-size: 0.8rem;
  white-space: pre-wrap;
  font-family: monospace;
}

#check-verify-btn {
  margin-top: 0.6rem;
}

.penalty-row {
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--c-danger-border, #fecaca);
  font-size: 0.85rem;
}

.penalty-row:last-child {
  border-bottom: none;
}

/* Public Linxi Card score badge */
.card-score-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.35rem 0.85rem;
  border-radius: 1rem;
  background: var(--c-surface-sunken, #f1f5f9);
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--c-text-2, #334155);
  margin: 0.75rem 0;
}

.card-score-badge.hidden {
  display: none;
}

.tier-emoji {
  font-size: 1rem;
}

/* Compose tab score indicator */
.sender-score-indicator {
  font-size: 0.8rem;
  color: var(--c-text-muted, #64748b);
  margin-top: 0.4rem;
}

/* Admin Users table score column */
.admin-score-cell {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  white-space: nowrap;
}

/* --- Demo selector (public/demo.html) -- reuses .login-main/.card, same
   light theme as login.html/register.html, not a separate dark page. --- */

.demo-select-card {
  width: 100%;
  max-width: 520px;
  text-align: center;
}

.demo-select-card h1 {
  margin-top: 0;
  margin-bottom: 0.15rem;
  font-size: 1.5rem;
}

.demo-cards {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1rem;
  margin: 1.25rem 0 1.5rem;
}

@media (max-width: 520px) {
  .demo-cards {
    grid-template-columns: 1fr;
  }
}

.demo-card {
  background: var(--c-surface-subtle, #f8fafc);
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.75rem;
  padding: 1.5rem 1rem;
  cursor: pointer;
  transition: all 0.15s;
  text-align: center;
  color: var(--c-ink, #1a1a1a);
  text-decoration: none;
  display: block;
}

.demo-card:hover {
  background: var(--c-brand-tint-2, #eef2ff);
  border-color: var(--c-brand, #6366f1);
  transform: translateY(-2px);
  box-shadow: 0 6px 20px color-mix(in srgb, var(--c-brand, #6366f1) 15%, transparent);
}

.demo-card-emoji {
  font-size: 2.25rem;
  margin-bottom: 0.5rem;
  display: block;
}

.demo-card h3 {
  font-size: 0.95rem;
  font-weight: 700;
  margin: 0 0 0.35rem;
  color: var(--c-ink, #1a1a1a);
}

.demo-card p {
  color: var(--c-text-muted, #64748b);
  font-size: 0.8rem;
  line-height: 1.4;
  margin: 0;
}

/* --- "Explore Interactive Demo" link (register.html) --- */

.demo-cta {
  margin-top: 0.2rem;
}

.demo-divider {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin: 0.5rem 0;
  color: var(--c-text-dim, #94a3b8);
  font-size: 0.875rem;
}

.demo-divider::before,
.demo-divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--c-border, #e2e8f0);
}

.demo-link-btn {
  display: block;
  text-align: center;
  padding: 0.4rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
  color: var(--c-brand-strong, #4f46e5);
  text-decoration: none;
  font-weight: 500;
  font-size: 0.9rem;
  transition: all 0.15s;
}

.demo-link-btn:hover {
  background: var(--c-surface-subtle, #f8fafc);
  border-color: var(--c-brand-strong, #4f46e5);
}

.demo-hint {
  text-align: center;
  color: var(--c-text-dim, #94a3b8);
  font-size: 0.8rem;
  margin: 0.2rem 0 0;
}

/* --- Username dropdown (header, replaces the old always-visible
   Account nav tab + separate Logout button) --- */

.user-menu {
  position: relative;
}

.user-menu-trigger {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  background: none;
  border: none;
  /* --c-head-ink, not --c-on-accent: the header follows the theme, and the
     fixed-white token was white on white in light. Same fix as .bell-btn. */
  color: var(--c-head-ink, #ffffff);
  cursor: pointer;
  font-size: 0.9rem;
  padding: 0.3rem 0.4rem;
  border-radius: 6px;
}


.user-menu-caret {
  font-size: 0.7rem;
  opacity: 0.7;
}

.user-menu-dropdown {
  position: absolute;
  top: calc(100% + 0.4rem);
  right: 0;
  z-index: 500;
  background: var(--c-surface, #fff);
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 8px;
  box-shadow: 0 8px 24px color-mix(in srgb, var(--c-shadow, #000000) 15%, transparent);
  min-width: 160px;
  /* Every row is `white-space: nowrap`, so the box shrink-to-fits the
     longest label instead of breaking it over two lines. The cap stops
     a long user-typed account name (the switcher rows below) from
     dragging the whole menu across the header -- those lines ellipsis
     rather than widening it. */
  max-width: 20rem;
  padding: 0.4rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.user-menu-dropdown.hidden {
  display: none;
}

/* One weight for every row in these menus. Half of them are <button>s,
   which pick up `font-weight: 600` from the base button rule, and half
   are <a>s, which inherit 400 from the body -- so "My Profile" and
   "My Linxi Label" sat visibly lighter than "My AI" and "Account" in
   the same list. Stated here once, on the class both kinds share.

   `nowrap` is the other half: this menu is sized by its content, and
   without it the longer entries ("Manage linked accounts") broke onto
   a second line inside the 160px floor rather than widening the box. */
.user-menu-item {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  padding: 0.5rem 0.65rem;
  border-radius: 6px;
  cursor: pointer;
  font-size: 0.9rem;
  font-weight: 500;
  white-space: nowrap;
  color: var(--c-ink, #1a1a1a);
}

/* "My Profile" (#nav-my-profile-link) is an <a>, not a <button> like every
   other .user-menu-item -- resets the anchor-only default this class
   doesn't otherwise need to touch. */
a.user-menu-item {
  text-decoration: none;
  box-sizing: border-box;
}

.user-menu-item:hover {
  background: var(--c-page, #f4f5f7);
}

.user-menu-item-danger {
  color: var(--c-danger-strong, #dc2626);
}

/* --- Account tab: email verification reminder --- */

.account-verify-banner {
  margin-top: 0.75rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--c-border-soft, #e5e7eb);
}

.account-verify-banner .input-actions {
  margin-top: 0.6rem;
  max-width: 260px;
}

.account-danger-zone {
  border-color: var(--c-danger-border, #fecaca);
}

.account-danger-zone h2 {
  color: var(--c-danger-strong, #dc2626);
}

/* --- Workspace switch confirmation toast (header) --- */

#workspace-switch-toast {
  position: fixed;
  top: 4.5rem;
  left: 50%;
  transform: translateX(-50%) translateY(-8px);
  z-index: 10002;
  background: var(--c-success, #16a34a);
  color: var(--c-on-fill, #ffffff);
  padding: 0.5rem 1rem;
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 600;
  box-shadow: 0 6px 20px color-mix(in srgb, var(--c-shadow, #000000) 20%, transparent);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s, transform 0.2s;
}

#workspace-switch-toast.visible {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* --- Generic app-wide toast (showToast() in api.js) -- same visual
   pattern as #workspace-switch-toast above, under a reusable name
   instead of a feature-specific one. Currently used for the webmail
   "email sent" confirmation; safe to reuse anywhere else that wants a
   brief, non-blocking confirmation instead of a modal. --- */

#app-toast {
  position: fixed;
  top: 4.5rem;
  left: 50%;
  transform: translateX(-50%) translateY(-8px);
  z-index: 10002;
  color: var(--c-on-accent, #ffffff);
  padding: 0.5rem 1rem;
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 600;
  box-shadow: 0 6px 20px color-mix(in srgb, var(--c-shadow, #000000) 20%, transparent);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s, transform 0.2s;
}

#app-toast.visible {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

#app-toast.success {
  background: var(--c-success, #16a34a); color: var(--c-on-status, #0f172a);
}

#app-toast.error {
  background: var(--c-danger-strong, #dc2626);
}

/* Super_admin-only workspace switcher color coding -- <option> styling
   support is inconsistent across browsers (Chrome/Firefox/Edge honor
   background-color/color on the dropdown list; some platforms only
   partly reflect it on the closed select), but degrades harmlessly to
   plain text where it isn't honored, so it's still worth setting. */
.ws-option-own {
  background-color: var(--c-success, #16a34a);
  color: var(--c-on-fill, #ffffff);
}

.ws-option-member {
  background-color: var(--c-brand-alt, #8b5cf6);
  color: var(--c-on-fill, #ffffff);
}

/* --- Analytics tab --- */

.analytics-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1rem;
}

.stat-card {
  background: var(--c-surface, #fff);
  border-radius: 8px;
  padding: 1.1rem 1.25rem;
  border: 1px solid var(--c-border, #e2e8f0);
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 0.25rem;
}

.stat-card-icon {
  font-size: 1.4rem;
}

.stat-card-label {
  font-size: 0.72rem;
  color: var(--c-text-dim, #94a3b8);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.stat-card-value {
  font-size: 1.6rem;
  font-weight: 800;
  color: var(--c-text, #0f172a);
}

.stat-card-sub {
  font-size: 0.78rem;
  color: var(--c-text-muted, #64748b);
}

.analytics-chart-card h3 {
  margin: 0 0 0.9rem;
  font-size: 1rem;
}

/* Grid items default to min-width: auto, which refuses to shrink below
   their content's own intrinsic width -- without this, a wide table
   (Most Active Contacts) or a Chart.js canvas sized to match (Channel
   Breakdown) grows the whole .analytics-two-col grid track wider than
   the viewport on mobile instead of scrolling within itself, even
   though the table already has its own overflow-x: auto (see the mobile
   `table { display: block; overflow-x: auto; }` rule) and the canvas is
   `responsive: true`. This is what actually lets either of those take
   effect. */
.analytics-chart-card {
  min-width: 0;
}

.analytics-chart-card canvas {
  max-width: 100%;
}

.analytics-two-col {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.5rem;
}

.top-contacts-bar {
  display: inline-block;
  height: 8px;
  background: var(--c-brand, #6366f1);
  border-radius: 4px;
  min-width: 4px;
  vertical-align: middle;
}

.analytics-clickable-row {
  cursor: pointer;
}

.analytics-clickable-row:hover {
  background: var(--c-surface-subtle, #f8fafc);
}

.analytics-contact-cell {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

.no-response-row {
  opacity: 0.75;
}

.no-response-row:hover {
  opacity: 1;
}

/* --- Venn diagram explorer --- */

.venn-selection-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
  margin: 0.75rem 0 1rem;
}

.venn-selection-col {
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 8px;
  overflow: hidden;
}

.venn-selection-col-header {
  background: var(--c-surface-subtle, #f8fafc);
  padding: 0.55rem 0.8rem;
  font-weight: 600;
  font-size: 0.8rem;
  color: var(--c-text-3, #475569);
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
}

.venn-selection-items {
  padding: 0.5rem;
  max-height: 200px;
  overflow-y: auto;
}

.venn-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 0.5rem;
  border-radius: 6px;
  cursor: pointer;
  font-size: 0.85rem;
}

.venn-item:hover {
  background: var(--c-surface-subtle, #f8fafc);
}

.venn-item-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex-shrink: 0;
}

.venn-item-count {
  margin-left: auto;
  color: var(--c-text-dim, #94a3b8);
  font-size: 0.75rem;
}

.venn-controls {
  display: flex;
  gap: 0.75rem;
  margin-bottom: 1rem;
  flex-wrap: wrap;
}

.venn-diagram-area {
  display: flex;
  gap: 1rem;
}

.venn-svg-container {
  flex: 1;
  min-height: 400px;
  min-width: 0;
}

.venn-svg-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 400px;
  text-align: center;
}

.venn-svg-container svg {
  max-width: 100%;
}

.venn-detail-panel {
  width: 260px;
  flex-shrink: 0;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 8px;
  padding: 1rem;
  font-size: 0.85rem;
  display: none;
}

.venn-detail-panel.visible {
  display: block;
}

.venn-detail-title {
  font-weight: 700;
  margin-bottom: 0.4rem;
}

.venn-detail-count {
  color: var(--c-brand, #6366f1);
  font-size: 1.1rem;
  font-weight: 700;
  margin-bottom: 0.75rem;
}

.venn-detail-contacts {
  list-style: none;
  padding: 0;
  margin: 0 0 1rem;
}

.venn-detail-contacts li {
  padding: 0.25rem 0;
  border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9);
  color: var(--c-text-3, #475569);
}

.venn-detail-actions {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

@media (max-width: 800px) {
  .analytics-grid {
    grid-template-columns: repeat(2, 1fr);
  }

  .analytics-two-col {
    grid-template-columns: 1fr;
  }

  .venn-selection-grid {
    grid-template-columns: 1fr;
  }

  .venn-diagram-area {
    flex-direction: column;
  }

  .venn-detail-panel {
    width: auto;
  }
}

/* --- Notification bell --- */

.bell-container {
  position: relative;
}

.bell-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  cursor: pointer;
  padding: 0.35rem;
  border-radius: 0.5rem;
  position: relative;
  /* ⚠️ --c-head-ink, NOT --c-on-accent. The header follows the theme
     (REQ-0046), so "text on a fill that stays dark" is the wrong token
     here: it is fixed white in both modes and was white on white in
     light. --c-head-ink is dark on the light header and white on the
     dark one, which is the whole point of that family. */
  color: var(--c-head-ink, #ffffff);
  transition: background 0.15s;
}


.bell-icon {
  width: 20px;
  height: 20px;
}

.bell-icon-filled {
  display: none;
}

.bell-btn.has-unread .bell-icon-outline {
  display: none;
}

.bell-btn.has-unread .bell-icon-filled {
  display: block;
}

.bell-badge {
  position: absolute;
  top: -2px;
  right: -2px;
  background: var(--c-success, #16a34a);
  color: var(--c-on-fill, #ffffff);
  font-size: 0.6rem;
  font-weight: 700;
  min-width: 16px;
  height: 16px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 3px;
  line-height: 1;
}

/* Two-class specificity so this reliably beats .bell-badge's own
   `display: flex` above regardless of source order -- same fix
   .manage-organize-panel.hidden documents elsewhere in this file. */
.bell-badge.hidden {
  display: none;
}

.notification-panel {
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  width: 340px;
  max-width: calc(100vw - 2rem);
  background: var(--c-surface, #fff);
  border-radius: 0.75rem;
  box-shadow: 0 8px 30px color-mix(in srgb, var(--c-shadow, #000000) 15%, transparent);
  border: 1px solid var(--c-border, #e2e8f0);
  z-index: 5000;
  overflow: hidden;
}

.notification-panel-header {
  padding: 0.85rem 1rem;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-weight: 700;
  font-size: 0.9rem;
  background: var(--c-surface-subtle, #f8fafc);
}

.btn-link {
  background: none;
  border: none;
  color: var(--c-brand, #6366f1);
  font-size: 0.8rem;
  cursor: pointer;
  padding: 0;
}

.btn-link:hover {
  text-decoration: underline;
}

.notification-list {
  max-height: 400px;
  overflow-y: auto;
}

.notification-panel-footer {
  padding: 0.65rem 1rem;
  border-top: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc);
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.notif-pref-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.75rem 0;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
}

.notif-pref-row:last-of-type {
  border-bottom: none;
}

.notif-pref-row p.muted {
  margin: 0.15rem 0 0;
}

.broadcast-type-btn.active {
  background: var(--c-brand, #6366f1);
  color: var(--c-on-fill, #ffffff);
  border-color: var(--c-brand, #6366f1);
}

#broadcast-preview {
  cursor: default;
  align-items: flex-start;
  gap: 0.75rem;
  display: flex;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
}

.notification-item {
  padding: 0.85rem 1rem;
  border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9);
  cursor: pointer;
  transition: background 0.15s;
}

.notification-item:hover {
  background: var(--c-surface-subtle, #f8fafc);
}

.notification-item.unread {
  background: var(--c-info-tint, #eff6ff);
  border-left: 3px solid var(--c-brand, #6366f1);
}

.notification-item.unread:hover {
  background: var(--c-info-tint-2, #dbeafe);
}

.notification-item-title {
  font-weight: 600;
  font-size: 0.85rem;
  color: var(--c-text, #0f172a);
  margin-bottom: 0.2rem;
}

.notification-item-body {
  font-size: 0.8rem;
  color: var(--c-text-muted, #64748b);
  margin-bottom: 0.4rem;
  line-height: 1.4;
}

.notification-item-time {
  font-size: 0.7rem;
  color: var(--c-text-dim, #94a3b8);
}

.notification-actions {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.5rem;
}

.notification-action-btn {
  padding: 0.3rem 0.65rem;
  font-size: 0.75rem;
  border-radius: 0.35rem;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface, #fff);
  cursor: pointer;
  transition: opacity 0.15s;
}

.notification-action-btn.primary {
  background: var(--c-brand, #6366f1);
  color: var(--c-on-fill, #ffffff);
  border-color: var(--c-brand, #6366f1);
}

/* The Linxi Network follow-back "View Profile" action is an <a>, not a
   <button> like every other .notification-action-btn -- resets the
   anchor-specific defaults this class doesn't otherwise need to touch. */
a.notification-action-btn {
  display: inline-block;
  color: var(--c-text-3, #475569);
  text-decoration: none;
}

.notification-action-btn:hover {
  opacity: 0.85;
}

.notification-empty {
  padding: 2rem;
  text-align: center;
  color: var(--c-text-dim, #94a3b8);
  font-size: 0.875rem;
}

/* --- Real-time notification toast --- */

/* ⚠️ BOTTOM-LEFT, because the assistant took bottom-right. At
   bottom: 5rem; right: 1.5rem this landed exactly on the chat panel's
   header -- a toast covering the thing it interrupts. The left corner is
   free now that the assistant has left it. */
.notification-toast {
  position: fixed;
  bottom: 1.5rem;
  left: 1.5rem;
  background: var(--c-inverse-surface, #0f172a);
  color: var(--c-on-accent, #ffffff);
  padding: 0.85rem 1.25rem;
  border-radius: 0.75rem;
  box-shadow: 0 4px 20px color-mix(in srgb, var(--c-shadow, #000000) 20%, transparent);
  z-index: 9999;
  max-width: 320px;
  animation: slideInLeft 0.3s ease;
  font-size: 0.875rem;
}

.notification-toast-inner {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
}

.notification-toast-icon {
  font-size: 1.25rem;
  flex-shrink: 0;
}

.notification-toast-title {
  font-weight: 700;
  margin-bottom: 0.2rem;
}

.notification-toast-detail {
  color: var(--c-text-dim, #94a3b8);
  font-size: 0.8rem;
}

.notification-toast-action {
  display: block;
  background: none;
  border: none;
  color: var(--c-on-dark-brand, #a5b4fc);
  font-size: 0.8rem;
  cursor: pointer;
  padding: 0.25rem 0;
  margin-top: 0.35rem;
}

.notification-toast-close {
  background: none;
  border: none;
  color: var(--c-text-dim, #94a3b8);
  cursor: pointer;
  font-size: 1rem;
  flex-shrink: 0;
}

/* ⚠️ Renamed with its one user rather than left beside it: a keyframe
   called slideInRight that slides in from the LEFT is worse than none.
   The toast is the only thing that ever used it. */
@keyframes slideInLeft {
  from {
    transform: translateX(-100%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}

/* --- Billing: checkout modal (src/routes/billing.js, public/js/billing.js) --- */

/* Wider than the base .modal's 440px (matches .campaign-detail-modal's
   own 720px) -- needs room for a row of full landing-page-style pricing
   cards, not just a couple of buttons. */
.checkout-modal {
  max-width: 720px;
}

/* auto-fit rather than a fixed column count: the grid shows every tier
   except free and the one already subscribed to, so the count changes
   with who is looking -- six tiles on free, five on anything else -- and
   a fixed column count would leave a phantom empty column. At 720px this
   lands on three across, which is what fits all six on screen without
   scrolling. It also reflows on its own, without needing the landing
   grid's viewport-width media queries. */
.checkout-plan-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 0.6rem;
  margin: 0.75rem 0 0.5rem;
  align-items: stretch;
}

/* A COMPACT tile: name, price, one line of figures, button. It no
   longer reuses .landing-pricing-card -- that shell is sized for a full
   pricing page, and six of them in a modal made a picker you had to
   scroll through to see the options you were picking between.

   Two-class selectors throughout because `.modal p` (0,1,1) and friends
   are more specific than a lone class and would otherwise reset the
   font-size and margins set here. */
.checkout-plan-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: 0.9rem 0.7rem 0.7rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 10px;
  background: var(--c-surface, #fff);
  text-align: center;
}

.checkout-plan-card.checkout-plan-featured {
  border-color: var(--c-violet, #7c3aed);
  box-shadow: inset 0 0 0 1px var(--c-violet, #7c3aed);
}

.checkout-plan-card .checkout-plan-badge {
  position: absolute;
  top: -8px;
  left: 50%;
  transform: translateX(-50%);
  background: var(--c-violet, #7c3aed);
  color: var(--c-on-fill, #ffffff);
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
  white-space: nowrap;
}

.checkout-plan-card .checkout-plan-name {
  margin: 0;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--c-text, #0f172a);
}

.checkout-plan-card .checkout-plan-price {
  font-size: 1.35rem;
  font-weight: 700;
  color: var(--c-text, #0f172a);
  line-height: 1.15;
}

.checkout-plan-card .checkout-plan-price span {
  font-size: 0.7rem;
  font-weight: 500;
  color: var(--c-text-muted, #64748b);
}

.checkout-plan-card .checkout-plan-meta {
  margin: 0 0 0.6rem;
  font-size: 0.71rem;
  line-height: 1.35;
  color: var(--c-text-muted, #64748b);
}

/* margin-top:auto keeps the buttons on one line across cards whose meta
   text wraps to a different number of lines. */
.checkout-plan-card .checkout-plan-btn {
  margin-top: auto;
  width: 100%;
  font-size: 0.78rem;
  padding: 0.4rem 0.5rem;
}

.checkout-enterprise-note {
  margin-top: 0.9rem;
}

/* --- Number wizard (src/routes/numbers.js, public/js/numberWizard.js) --- */

.number-wizard-modal {
  max-width: 520px;
}

.number-wizard-choice-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
  margin: 0.5rem 0;
}

@media (max-width: 520px) {
  .number-wizard-choice-grid {
    grid-template-columns: 1fr;
  }
}

.number-wizard-choice-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  text-align: center;
  background: var(--c-surface, #fff);
  border: 2px solid var(--c-border, #e2e8f0);
  border-radius: 10px;
  padding: 1.25rem 1rem;
  cursor: pointer;
}

.number-wizard-choice-card:hover {
  border-color: var(--c-brand, #6366f1);
  background: var(--c-brand-tint, #f5f3ff);
}

.number-wizard-choice-icon {
  font-size: 2.25rem;
}

.number-wizard-choice-title {
  font-weight: 700;
  font-size: 0.95rem;
  color: var(--c-text, #0f172a);
}

.number-wizard-choice-desc {
  font-size: 0.78rem;
  color: var(--c-text-muted, #64748b);
  line-height: 1.4;
}

.number-wizard-choice-tag {
  margin-top: 0.4rem;
  background: var(--c-surface-sunken, #f1f5f9);
  color: var(--c-text-muted, #64748b);
  font-size: 0.72rem;
  font-weight: 600;
  border-radius: 999px;
  padding: 0.25rem 0.65rem;
}

.number-wizard-choice-tag-included {
  background: var(--c-success-tint, #f0fdf4);
  color: var(--c-success-text, #166534);
}

.number-wizard-choice-tag-upgrade {
  background: var(--c-warning-tint-2, #fef3c7);
  color: var(--c-warning-text, #92400e);
}

.number-wizard-search-tabs {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1rem;
}

.number-wizard-search-tab {
  flex: 1;
  background: var(--c-surface, #fff);
  color: var(--c-text-3, #475569);
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 6px;
  padding: 0.5rem;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
}

.number-wizard-search-tab.active {
  background: var(--c-brand, #6366f1);
  color: var(--c-on-fill, #ffffff);
  border-color: var(--c-brand, #6366f1);
}

.number-wizard-results {
  margin-top: 0.75rem;
  max-height: 280px;
  overflow-y: auto;
}

.number-wizard-result-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 8px;
  padding: 0.7rem 0.9rem;
  margin-bottom: 0.5rem;
  cursor: pointer;
}

.number-wizard-result-row:hover {
  border-color: var(--c-brand, #6366f1);
  background: var(--c-brand-tint, #f5f3ff);
}

.number-wizard-result-name {
  font-weight: 700;
  font-size: 0.9rem;
}

.number-wizard-result-locality {
  color: var(--c-text-dim, #94a3b8);
  font-size: 0.75rem;
}

.number-wizard-result-caps {
  display: flex;
  gap: 0.3rem;
}

.number-wizard-cap-tag {
  background: var(--c-success-tint, #f0fdf4);
  color: var(--c-success-text, #166534);
  font-size: 0.68rem;
  font-weight: 700;
  border-radius: 4px;
  padding: 0.15rem 0.35rem;
}

.number-wizard-confirm-summary {
  background: var(--c-brand-tint, #f5f3ff);
  border: 1px solid var(--c-brand-border, #c7d2fe);
  border-radius: 10px;
  padding: 1.1rem;
  margin-bottom: 1.1rem;
  text-align: center;
}

.number-wizard-confirm-number {
  font-size: 1.4rem;
  font-weight: 800;
  color: var(--c-brand-strong, #4f46e5);
}

.number-wizard-next-steps {
  background: var(--c-surface-subtle, #f8fafc);
  border-radius: 10px;
  padding: 1rem 1.1rem;
  margin-bottom: 1.1rem;
}

.number-wizard-next-steps h4 {
  margin: 0 0 0.5rem;
  font-size: 0.85rem;
}

.number-wizard-next-steps ul {
  margin: 0;
  padding-left: 1.1rem;
  font-size: 0.82rem;
  line-height: 1.7;
}

.number-wizard-note {
  background: var(--c-warning-tint, #fffbeb);
  border: 1px solid var(--c-warning-border, #fde68a);
  color: var(--c-warning-text, #92400e);
  border-radius: 8px;
  padding: 0.65rem 0.85rem;
  font-size: 0.78rem;
}

.number-wizard-upsell {
  background: var(--c-brand-tint, #f5f3ff);
  border: 1px solid var(--c-brand-border, #c7d2fe);
  border-radius: 10px;
  padding: 1rem 1.1rem;
  margin-top: 0.75rem;
  text-align: center;
}

.number-wizard-upsell p {
  margin: 0 0 0.75rem;
  font-size: 0.85rem;
  color: var(--c-violet-text, #6d28d9);
}

.number-wizard-upsell .btn {
  width: 100%;
  border: none;
  cursor: pointer;
}

.number-wizard-success-icon {
  font-size: 3rem;
  text-align: center;
}

.number-wizard-success-lead {
  text-align: center;
  color: var(--c-text-muted, #64748b);
  margin-bottom: 1.1rem;
}

.number-wizard-ready-box {
  background: var(--c-success-tint, #f0fdf4);
  border: 1px solid var(--c-success-border, #bbf7d0);
  border-radius: 10px;
  padding: 1rem 1.1rem;
  margin-bottom: 1rem;
}

.number-wizard-ready-box h4 {
  margin: 0 0 0.5rem;
  color: var(--c-success-text, #166534);
  font-size: 0.85rem;
}

.number-wizard-ready-box ul {
  margin: 0;
  padding-left: 1.1rem;
  font-size: 0.82rem;
  color: var(--c-success-text, #166534);
  line-height: 1.7;
}

.number-wizard-a2p-box {
  background: var(--c-warning-tint, #fffbeb);
  border: 1px solid var(--c-warning-border, #fde68a);
  border-radius: 10px;
  padding: 1rem 1.1rem;
}

.number-wizard-a2p-box h4 {
  margin: 0 0 0.4rem;
  color: var(--c-warning-text, #92400e);
  font-size: 0.85rem;
}

.number-wizard-a2p-box p {
  margin: 0;
  color: var(--c-warning-text, #92400e);
  font-size: 0.8rem;
  line-height: 1.5;
}

.checkout-payment-summary {
  font-size: 0.95rem;
  font-weight: 600;
  margin-bottom: 0.75rem;
}

@media (max-width: 520px) {
  .legal-nav {
    flex-wrap: wrap;
  }

  .sms-highlight,
  .consent-section {
    padding: 1rem;
  }
}

.square-card-container {
  min-height: 90px;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 8px;
  padding: 0.5rem;
  margin-bottom: 0.5rem;
}

.checkout-ready-indicator {
  font-size: 0.75rem;
  color: var(--c-text-dim, #94a3b8);
  margin: 0;
}

.checkout-ready-indicator.ready {
  color: var(--c-success-alt, #10b981);
}

/* Twilio account health in Linxi Settings -- owner-only, see
   GET /api/workspaces/:id/twilio-billing. Muted by default because the
   healthy case is meant to be glanceable and forgettable; the warn/bad
   variants are the ones that need to interrupt someone. */
.ws-billing-status {
  font-size: 0.85rem;
  margin: 0.15rem 0 0.6rem;
  color: var(--c-text-muted, #64748b);
}

.ws-billing-status .ws-billing-pill {
  display: inline-block;
  border-radius: 999px;
  padding: 0.1rem 0.5rem;
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--c-on-accent, #ffffff);
  margin-right: 0.4rem;
  text-transform: capitalize;
}

.ws-billing-pill.is-ok { background: var(--c-success, #16a34a); color: var(--c-on-status, #0f172a); }
.ws-billing-pill.is-warn { background: var(--c-warning-strong, #d97706); color: var(--c-on-fill, #ffffff); }
.ws-billing-pill.is-bad { background: var(--c-danger-strong, #dc2626); }

.ws-billing-status.is-warn,
.ws-billing-status.is-bad { color: var(--c-text-2, #334155); font-weight: 500; }

/* --- A2P Campaign editor (public/js/a2pCampaigns.js) ---------------- */

/* Both classes below are new. The editor's markup was written against
   them and neither existed in this stylesheet before, so without these
   rules the section renders as a bare block and every modal label
   renders as unstyled inline text. .field-label deliberately matches the
   look of the app's existing form <label> elements rather than inventing
   a second label style. */
.settings-section {
  background: var(--c-surface, #fff);
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.75rem;
  padding: 1.25rem;
  margin-bottom: 1.5rem;
}

.field-label {
  display: block;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--c-text-2, #334155);
  margin-bottom: 0.3rem;
}

/* --- Linxi Settings: vertical tab rail ------------------------------ */

/* The rail is ~1/8 of the content width, with a floor so it never gets
   too narrow to read a label, and a ceiling so it doesn't sprawl on a
   wide monitor. */
/* The distance from the left edge of the viewport to the left edge of
   main's CONTENT box. main is `max-width: 1280px; margin: 0 auto;
   padding: 1.5rem` with border-box sizing, so that is 1.5rem while the
   window is narrower than 1280 and grows by half the leftover once it is
   wider. max() covers both without a media query.

   Used to pull the settings rail out to the window edge. Anything else
   that ever needs to break out of the page column should use this too
   rather than re-deriving it -- getting it slightly wrong shows up as a
   hairline of background down one side, only at some widths. */
/* ═══════════════════════════════════════════════════════════════════════
   LEFT SIDEBAR RAIL — SITEWIDE MODULE
   ═══════════════════════════════════════════════════════════════════════
   ONE definition for every left sidebar in the app. Four use it today,
   all in index.html, all marked `data-rail` on the <nav> so they can be
   found with a single grep:

     data-rail="settings"   #ws-settings-rail   Linxi Settings tab
     data-rail="account"    #acct-rail          Account tab
     data-rail="admin"      #admin-rail         Admin Settings tab
     data-rail="users"      #users-rail         Admin Users tab

   Markup contract for a fifth: a .ws-settings-layout wrapper holding a
   <nav class="ws-settings-rail" data-rail="..."> and a
   <div class="ws-settings-panels">, with .ws-rail-group /
   .ws-rail-group-header / .ws-rail-group-items / .ws-rail-btn inside.
   Add its panel attribute to the visibility rule at the bottom of this
   module and it inherits everything else for free.

   EVERYTHING THEMEABLE IS A VARIABLE, on :root, so a change lands on all
   four rails at once and can be overridden per-page or per-brand without
   touching this block. The accent bar colour in particular -- the strip
   down the left of the selected row -- is --rail-accent and nothing else
   hardcodes it.

   Nothing about a rail should be styled outside this module. Rules
   scattered elsewhere are how three of these ended up looking like they
   had been missed; the users-panel visibility rule really had drifted
   1,700 lines away before this was written.
   ═══════════════════════════════════════════════════════════════════════ */

:root {
  /* Distance from the window edge to main's content box. main is
     max-width 1280 / margin auto / padding 1.5rem with border-box, so it
     is 1.5rem under 1280 and grows by half the leftover above it. Used
     to pull the rail out to the glass. */
  --page-bleed: max(1.5rem, calc((100vw - 1280px) / 2 + 1.5rem));

  --rail-width: 240px;
  --rail-bg: var(--c-surface-subtle, #f8fafc);
  /* Translucent black, not a colour token: it darkens whatever it sits
     on, so it holds up on a light or a dark rail without a second
     definition. */
  --rail-divider: color-mix(in srgb, var(--c-shadow, #000000) 10%, transparent);
  --rail-pad-y: 1.25rem;
  --rail-gap: 0.25rem;

  /* Rows */
  --rail-row-pad-y: 0.6rem;
  --rail-row-pad-x: 1.25rem;
  --rail-row-inset: 4px;
  --rail-row-size: 0.9rem;
  --rail-row-text: var(--c-ink, #1a1a1a);
  --rail-row-hover-bg: color-mix(in srgb, var(--c-shadow, #000000) 4%, transparent);

  /* Selected row. --rail-accent is the strip down its left edge. */
  --rail-accent: var(--c-brand-alt, #8b5cf6);
  --rail-accent-width: 3px;
  --rail-active-bg: color-mix(in srgb, var(--c-shadow, #000000) 5%, transparent);
  --rail-active-text: var(--c-brand, #6366f1);

  /* Group headings */
  --rail-group-text: var(--c-text-dim, #94a3b8);
  --rail-group-text-hover: var(--c-text-muted, #64748b);
  --rail-group-size: 0.68rem;

  --rail-danger: var(--c-danger, #ef4444);
  --rail-danger-strong: var(--c-danger-strong, #dc2626);
  --rail-panel-gap: 1.5rem;
  --rail-mobile-max: 900px;
}

/* ── Layout ─────────────────────────────────────────────────────────── */

.ws-settings-layout {
  display: grid;
  /* minmax(0, 1fr) on the content column is not cosmetic: a grid child
     defaults to min-width auto and refuses to shrink below its content,
     so one wide table would shove the whole page sideways. */
  grid-template-columns: var(--rail-width) minmax(0, 1fr);
  /* No gap -- the rail's own right border is the divider, and the panel
     column supplies the breathing room as padding. */
  gap: 0;
  align-items: start;
  /* Flush to the WINDOW edge, not just the settings area. Left margin
     only: the right edge stays inside main's column, so the used width
     grows by exactly the bleed and nothing overflows right. */
  margin-left: calc(-1 * var(--page-bleed));
}

/* ── The rail ───────────────────────────────────────────────────────── */
/*
   padding is VERTICAL ONLY. The rows supply their own side padding,
   which is what lets a row run the full width so its hover and active
   background reach both edges instead of floating in a gutter.

   sticky + top + height + overflow-y are one unit: sticky without an
   explicit height will not pin, and without overflow-y a long rail is
   unreachable past the fold. */
.ws-settings-rail {
  display: flex;
  flex-direction: column;
  gap: var(--rail-gap);
  width: var(--rail-width);
  background: var(--rail-bg);
  border: none;
  border-right: 1px solid var(--rail-divider);
  border-radius: 0;
  padding: var(--rail-pad-y) 0;
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
  overflow-x: hidden;
  /* main's own 1.5rem padding cancelled so the rail meets the header
     above it and runs to the bottom of the view. */
  margin-top: -1.5rem;
  margin-bottom: -1.5rem;
}

/* ── Groups ─────────────────────────────────────────────────────────── */

.ws-rail-group + .ws-rail-group { margin-top: 0.35rem; }

.ws-rail-group-header {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  /* Left inset matches a row's accent border + padding, so the group
     title and the labels under it share one text edge. */
  padding: 0.45rem var(--rail-row-pad-x) 0.45rem
           calc(var(--rail-row-pad-x) + var(--rail-accent-width));
  font-family: inherit;
  background: none;
  border: none;
  box-shadow: none;
  border-radius: 0;
  cursor: pointer;
  font-size: var(--rail-group-size);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--rail-group-text);
  transition: color 0.15s ease;
}

.ws-rail-group-header:hover:not(:disabled) {
  background: none;
  color: var(--rail-group-text-hover);
  box-shadow: none;
  transform: none;
}

.ws-rail-group-title { display: inline-flex; align-items: center; gap: 0.4rem; }
.ws-rail-arrow { font-size: 0.6rem; transition: transform 0.2s ease; }

.ws-rail-group-items {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  /* A max-height large enough never to clip a real group, animated to 0
     on collapse. grid-template-rows: 0fr would animate to the content
     height exactly, but is not supported far enough back to rely on for
     navigation. */
  max-height: 40rem;
  overflow: hidden;
  transition: max-height 0.2s ease;
}

.ws-rail-group.collapsed .ws-rail-group-items { max-height: 0; }
.ws-rail-group.collapsed .ws-rail-arrow { transform: rotate(-90deg); }

.ws-rail-group-danger {
  margin-top: 0.6rem;
  padding-top: 0.5rem;
  border-top: 1px solid var(--c-danger-border, #fecaca);
}

/* The rail's identity row: which Linxi you are looking at. Headerless,
   and never folded -- see isCollapsibleGroup in workspaceSettingsTabs.js. */
.ws-rail-group-identity {
  margin-bottom: 0.6rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
}
.ws-rail-btn-identity {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.05rem;
  padding-top: 0.45rem;
  padding-bottom: 0.45rem;
}
.ws-rail-identity-name {
  font-weight: 700;
  font-size: 0.85rem;
  /* A Linxi name is free text and can be long; the rail has a fixed
     width, so it truncates rather than pushing the rail wider. */
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ws-rail-identity-sub {
  font-size: 0.66rem;
  color: var(--c-text-muted, #64748b);
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── Linxi overview panel ─────────────────────────────────────────── */
.ws-overview { display: flex; flex-direction: column; gap: 1.5rem; }
.ws-ov-section h3 {
  margin: 0 0 0.6rem;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--c-text-muted, #64748b);
  font-weight: 700;
}
.ws-ov-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
  gap: 0.75rem;
}
.ws-ov-stat {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  padding: 0.65rem 0.75rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.6rem;
  background: var(--c-surface-subtle, #f8fafc);
  min-width: 0;
}
.ws-ov-stat-label { font-size: 0.66rem; color: var(--c-text-muted, #64748b); text-transform: uppercase; letter-spacing: 0.04em; }
.ws-ov-stat-value { font-size: 1.05rem; overflow-wrap: anywhere; }
.ws-ov-stat-note { font-size: 0.68rem; color: var(--c-text-muted, #64748b); }
.ws-ov-ok { color: var(--success, #15803d); }
.ws-ov-muted { color: var(--c-text-muted, #64748b); font-weight: 500; }

.ws-ov-meters { display: flex; flex-direction: column; gap: 0.6rem; margin-top: 0.85rem; }
.ws-ov-meter { display: flex; flex-direction: column; gap: 0.25rem; }
.ws-ov-meter-top { display: flex; justify-content: space-between; align-items: baseline; font-size: 0.76rem; gap: 1rem; }
.ws-ov-bar { height: 0.4rem; border-radius: 2rem; background: var(--c-border, #e2e8f0); overflow: hidden; }
.ws-ov-bar-fill { height: 100%; border-radius: 2rem; background: var(--accent, var(--c-brand, #6366f1)); }
.ws-ov-bar-warm { background: var(--warning, var(--c-warning, #f59e0b)); }
.ws-ov-bar-hot { background: var(--danger, var(--c-danger, #ef4444)); }

.ws-ov-flags { display: flex; flex-direction: column; gap: 0.1rem; }
.ws-ov-flag { display: flex; align-items: center; gap: 0.6rem; padding: 0.4rem 0; border-bottom: 1px solid var(--c-border, #e2e8f0); font-size: 0.8rem; }
.ws-ov-flag:last-child { border-bottom: 0; }
.ws-ov-flag-label { flex: 1; min-width: 0; }
.ws-ov-flag-value { color: var(--c-text-muted, #64748b); font-size: 0.74rem; text-align: right; }
.ws-ov-dot { width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--c-border, #e2e8f0); flex: none; }
.ws-ov-dot-on { background: var(--success, var(--c-success-alt, #10b981)); }
.ws-ov-caveat { font-size: 0.72rem; margin: 0.75rem 0 0; }

/* REQ-0034: every figure on the overview is a count of something that
   lives on another screen, so the LABEL is the way there. Styled as a
   link rather than a button -- it navigates, it does not act -- and it
   has to reset the base `button {}` rule, which would otherwise make
   each label a gradient pill. A label with no reachable destination is
   rendered as a <span> by workspaceOverview.js and never reaches this. */
.ws-ov-link {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  box-shadow: none;
  display: inline;
  text-align: left;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-style: dotted;
  text-underline-offset: 2px;
  text-decoration-color: var(--c-border-strong, #cbd5e1);
}
.ws-ov-link:hover,
.ws-ov-link:focus-visible {
  color: var(--c-brand, #6366f1);
  text-decoration-style: solid;
  text-decoration-color: currentColor;
}
.ws-ov-meter-label { min-width: 0; }

.ws-ov-jumps { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin-top: 0.85rem; }
.ws-ov-jumps-danger { padding-top: 0.85rem; border-top: 1px solid var(--c-border, #e2e8f0); }
.ws-ov-jump {
  padding: 0.28rem 0.7rem;
  border-radius: 2rem;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc);
  font-size: 0.74rem;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
  color: inherit;
}
.ws-ov-jump:hover { border-color: var(--accent, var(--c-brand, #6366f1)); }
.ws-ov-jumps-danger .ws-ov-jump { border-color: var(--c-danger-border, #fecaca); color: var(--danger, var(--c-danger-deep, #b91c1c)); }

/* ── Rows ───────────────────────────────────────────────────────────── */
/*
   Real <button> elements, so keyboard focus and Enter work for free --
   these rules only strip the button chrome so they read as list rows.

   font-family: inherit is not decoration. Buttons do NOT inherit font
   from the page; without it the rail renders in the browser's UI
   typeface while everything around it does not.

   The accent border is declared TRANSPARENT in the base state and only
   its COLOUR changes on activation. Add the border on activation instead
   and every row jumps sideways when clicked.

   margin-left insets the row from the window edge; width comes down by
   the same amount because margin sits outside the box and would
   otherwise push the row past the rail's right edge, where its
   overflow-x: hidden would quietly clip it. */
.ws-rail-btn {
  display: flex;
  align-items: center;
  /* ⚠️⚠️ A LABEL MUST BE ONE FLEX ITEM, and this gap is why.
     A flex container makes each child its own item and DISCARDS the
     whitespace between them -- so `<span data-site-brand>X</span> Email`
     renders with no space of its own, and whatever `gap` says is the
     only separation there is. Setting it to 0.6rem showed a double
     space; setting it to 0 showed none at all. Neither is a space.

     The fix is structural: every label that carries an inline slot is
     wrapped in ONE `.ws-rail-label`, so the browser sees one phrase and
     ordinary inline whitespace applies. The gap then does what it was
     written for -- separating the label from `.rail-count` -- and is
     back at its original value. */
  gap: 0.6rem;
  margin-left: var(--rail-row-inset);
  width: calc(100% - var(--rail-row-inset));
  text-align: left;
  background: none;
  border: none;
  border-left: var(--rail-accent-width) solid transparent;
  border-radius: 0;
  box-shadow: none;
  padding: var(--rail-row-pad-y) var(--rail-row-pad-x);
  font-size: var(--rail-row-size);
  font-family: inherit;
  font-weight: 500;
  line-height: 1.3;
  color: var(--rail-row-text);
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}

.ws-rail-btn:hover:not(:disabled) {
  background: var(--rail-row-hover-bg);
  color: var(--rail-row-text);
  box-shadow: none;
  transform: none;
}

/* Four signals at once -- accent bar, faint wash, bolder, accent text.
   No icon needed to tell which row is open. */
.ws-rail-btn.active {
  border-left-color: var(--rail-accent);
  background: var(--rail-active-bg);
  font-weight: 600;
  color: var(--rail-active-text);
}

.ws-rail-btn-danger { color: var(--rail-danger); }
.ws-rail-btn-danger:hover:not(:disabled) { background: var(--c-danger-tint, #fef2f2); color: var(--rail-danger-strong); }
.ws-rail-btn-danger.active {
  background: var(--c-danger-tint, #fef2f2);
  color: var(--rail-danger-strong);
  border-left-color: var(--rail-danger);
}

/* A row whose panel is reporting a problem -- the unconfigured-SMTP
   banner lives inside the Admin Settings Email panel, which is not
   necessarily the one open. */
.ws-rail-btn.ws-rail-btn-alert { color: var(--c-danger-deep, #b91c1c); font-weight: 600; }
.ws-rail-btn.ws-rail-btn-alert::after {
  content: ''; display: inline-block; width: 0.45rem; height: 0.45rem;
  border-radius: 50%; background: var(--rail-danger); margin-left: 0.4rem;
  vertical-align: middle;
}

/* ── Panels ─────────────────────────────────────────────────────────── */

.ws-settings-panels {
  /* Breathing room lives here rather than as a grid gap, so the rail's
     right border sits against the content area. */
  padding-left: var(--rail-panel-gap);
  min-width: 0;
}

/* ONE visibility rule for every rail. Each controller keys on its own
   attribute so none of them can switch another's panels, but they all
   show and hide the same way -- listed together rather than in four
   blocks that drift apart. CSS has no "attribute name ends with"
   selector, so a new rail adds its attribute to these three lines. */
.ws-settings-panels > [data-ws-panel-id],
.ws-settings-panels > [data-acct-panel-id],
.ws-settings-panels > [data-admin-panel-id],
.ws-settings-panels > [data-users-panel-id],
.ws-settings-panels > [data-myai-panel-id],
.ws-settings-panels > [data-compose-panel-id],
.ws-settings-panels > [data-cal-panel-id],
.ws-settings-panels > [data-linxiai-panel-id],
.ws-settings-panels > [data-wssec-panel-id],
.ws-settings-panels > [data-an-panel-id],
.ws-settings-panels > [data-carrier-panel-id] { display: none; }

.ws-settings-panels > [data-ws-panel-id].ws-panel-active,
.ws-settings-panels > [data-acct-panel-id].ws-panel-active,
.ws-settings-panels > [data-admin-panel-id].ws-panel-active,
.ws-settings-panels > [data-users-panel-id].ws-panel-active,
.ws-settings-panels > [data-myai-panel-id].ws-panel-active,
.ws-settings-panels > [data-compose-panel-id].ws-panel-active,
.ws-settings-panels > [data-cal-panel-id].ws-panel-active,
.ws-settings-panels > [data-linxiai-panel-id].ws-panel-active,
.ws-settings-panels > [data-wssec-panel-id].ws-panel-active,
.ws-settings-panels > [data-an-panel-id].ws-panel-active,
.ws-settings-panels > [data-carrier-panel-id].ws-panel-active { display: block; }

/* Panel switching is a SEPARATE mechanism from the `hidden` class that
   role-gates six of the Linxi Settings panels. A panel shows only when
   it is the active tab AND not gated away. `.hidden` carries !important
   app-wide so this holds regardless, but it is stated here too because
   the rule is the contract, not an accident of the cascade. */
.ws-settings-panels > [data-ws-panel-id].ws-panel-active.hidden,
.ws-settings-panels > [data-acct-panel-id].ws-panel-active.hidden,
.ws-settings-panels > [data-admin-panel-id].ws-panel-active.hidden,
.ws-settings-panels > [data-users-panel-id].ws-panel-active.hidden,
.ws-settings-panels > [data-myai-panel-id].ws-panel-active.hidden,
.ws-settings-panels > [data-compose-panel-id].ws-panel-active.hidden,
.ws-settings-panels > [data-cal-panel-id].ws-panel-active.hidden,
.ws-settings-panels > [data-linxiai-panel-id].ws-panel-active.hidden,
.ws-settings-panels > [data-wssec-panel-id].ws-panel-active.hidden,
.ws-settings-panels > [data-an-panel-id].ws-panel-active.hidden,
.ws-settings-panels > [data-carrier-panel-id].ws-panel-active.hidden { display: none; }

/* The cards inside a panel keep their own spacing; the panel is only a
   visibility wrapper. The rule itself lives with .panel-head further
   down, where it also covers a column of cards that are NOT inside a
   panel wrapper -- the Compose tab's two. */

/* ── Narrow viewports ───────────────────────────────────────────────── */
/*
   A fixed side column is unusable here, so the sidebar becomes a
   horizontal strip above the panels. Everything that makes it a
   full-height pinned sidebar comes back off -- height, sticky, the fixed
   width, the row inset and the right-hand divider all mean the wrong
   thing lying on its side. The bleed goes too: the grid is ONE column at
   this width, so a negative margin would drag the panels to the screen
   edge along with the rail. */
@media (max-width: 900px) {
  .ws-settings-layout {
    grid-template-columns: 1fr;
    gap: 1rem;
    margin-left: 0;
  }
  .ws-settings-rail {
    position: static;
    flex-direction: row;
    overflow-x: auto;
    overflow-y: hidden;
    gap: 0.35rem;
    width: 100%;
    height: auto;
    margin-top: 0;
    margin-bottom: 0;
    border: 1px solid var(--c-border, #e2e8f0);
    border-radius: 0.75rem;
    padding: 0.6rem;
  }
  .ws-settings-panels { padding-left: 0; }
  .ws-rail-btn {
    width: auto;
    margin-left: 0;
    white-space: nowrap;
    flex: 0 0 auto;
  }
}

/* ═══════════════════ end LEFT SIDEBAR RAIL module ═══════════════════ */

/* --- Webmail: editor with the toolbar below, plus HTML source view --- */

/* Quill's snow theme borders assume toolbar-then-editor and style the
   seam with `.ql-toolbar + .ql-container`. The order is reversed here
   (that is the point), so that rule never matches and both elements
   draw a full border, doubling the line between them. These re-join
   them the other way round. */
.webmail-editor-stack .ql-container.ql-snow {
  border-bottom: 0;
  border-radius: 6px 6px 0 0;
}

.webmail-editor-stack #webmail-compose-toolbar.ql-toolbar.ql-snow {
  border-radius: 0 0 6px 6px;
}

/* In source mode the toolbar is hidden, so the textarea has to close its
   own bottom edge. */
.webmail-editor-stack-source .webmail-html-source {
  border-radius: 6px;
}

.webmail-html-source {
  width: 100%;
  min-height: 220px;
  max-height: 420px;
  padding: 0.75rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px 6px 0 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.8rem;
  line-height: 1.5;
  tab-size: 2;
  resize: vertical;
  background: var(--c-surface-subtle, #f8fafc);
  color: var(--c-text, #0f172a);
}

.webmail-editor-modebar {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-top: 0.5rem;
}

.webmail-editor-modebar .muted {
  font-size: 0.75rem;
}

.webmail-html-toggle {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.75rem;
  padding: 0.3rem 0.6rem;
}

/* --- Webmail: conversation strip ------------------------------------ */

.webmail-thread {
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 8px;
  /* Sits BELOW the message body now, so the gap belongs on top. */
  margin-top: 1.25rem;
  overflow: hidden;
}

.webmail-thread-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;
  background: var(--c-surface-subtle, #f8fafc);
  border-bottom: 1px solid var(--c-border, #e2e8f0);
}

.webmail-thread-count {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--c-text-2, #334155);
}

.webmail-thread-note {
  font-size: 0.7rem;
}

.webmail-thread-list {
  display: flex;
  flex-direction: column;
  max-height: 220px;
  overflow-y: auto;
}

.webmail-thread-row {
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: center;
  gap: 0.6rem;
  width: 100%;
  text-align: left;
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9);
  padding: 0.5rem 0.75rem;
  font-size: 0.8rem;
  color: var(--c-text-3, #475569);
  cursor: pointer;
}

.webmail-thread-row:last-child { border-bottom: 0; }
.webmail-thread-row:hover { background: var(--c-surface-subtle, #f8fafc); }

.webmail-thread-row.active {
  background: var(--c-violet-tint-2, #ede9fe);
  color: var(--c-violet-text, #6d28d9);
  cursor: default;
}

.webmail-thread-row.unread .webmail-thread-who { font-weight: 700; color: var(--c-text, #0f172a); }

.webmail-thread-who {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.webmail-thread-folder {
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 0.1rem 0.4rem;
  border-radius: 2rem;
  background: var(--c-border, #e2e8f0);
  color: var(--c-text-3, #475569);
}

.webmail-thread-row.active .webmail-thread-folder {
  background: var(--c-violet-border, #ddd6fe);
  color: var(--c-violet-text, #6d28d9);
}

.webmail-thread-meta {
  font-size: 0.72rem;
  color: var(--c-text-dim, #94a3b8);
  white-space: nowrap;
}

@media (max-width: 700px) {
  .webmail-thread-meta { display: none; }
}

.webmail-thread-row.webmail-thread-error {
  display: block;
  color: var(--c-danger-darkest, #991b1b);
  background: var(--c-danger-tint, #fef2f2);
  cursor: default;
  font-size: 0.78rem;
}

/* The Quill toolbar container is static markup, so it exists before (and
   whether or not) Quill ever runs. Hidden until webmail.js confirms Quill
   adopted it -- otherwise a stale cached webmail.js, or a blocked CDN,
   leaves a row of empty unstyled <button> elements sitting under the
   compose box. See the comment at the reveal site. */
#webmail-compose-toolbar:not(.webmail-toolbar-ready) {
  display: none;
}

/* --- Carrier selector (Twilio / Telnyx) ----------------------------- */

.carrier-select-wrap { margin: 1rem 0 1.25rem; }

.carrier-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.75rem;
  margin-top: 0.35rem;
}

@media (max-width: 700px) {
  .carrier-grid { grid-template-columns: 1fr; }
}

/* These are <button>s so they are keyboard-reachable and announce as
   controls, which a clickable <div> would not. That means opting out of
   the app-wide gradient button treatment explicitly. */
.carrier-option {
  display: block;
  text-align: left;
  background: var(--c-surface, #fff);
  border: 2px solid var(--c-border, #e2e8f0);
  border-radius: 0.75rem;
  padding: 1rem;
  cursor: pointer;
  box-shadow: none;
  color: inherit;
  font-weight: 400;
}

.carrier-option:hover:not(:disabled) {
  background: var(--c-surface-subtle, #f8fafc);
  border-color: var(--c-border-strong, #cbd5e1);
  box-shadow: none;
  transform: none;
}

.carrier-option.active,
.carrier-option.active:hover {
  border-color: var(--c-brand, #6366f1);
  background: var(--c-brand-tint, #f5f3ff);
}

.carrier-option-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.35rem;
}

.carrier-option-icon { font-size: 1.25rem; }
.carrier-option-name { font-weight: 700; font-size: 0.875rem; color: var(--c-text, #0f172a); }

.carrier-option-tag {
  font-size: 0.65rem;
  padding: 0.1rem 0.45rem;
  border-radius: 2rem;
  margin-left: auto;
  color: var(--c-on-accent, #ffffff);
  font-weight: 700;
}

.carrier-tag-current { background: var(--c-brand, #6366f1); color: var(--c-on-status, #0f172a); }
.carrier-tag-new { background: var(--c-success-alt, #10b981); color: var(--c-on-status, #0f172a); }

.carrier-option-desc {
  display: block;
  font-size: 0.75rem;
  color: var(--c-text-muted, #64748b);
}

.carrier-badge {
  font-size: 0.7rem;
  font-weight: 700;
  vertical-align: middle;
  margin-left: 0.5rem;
  padding: 0.15rem 0.5rem;
  border-radius: 2rem;
  background: var(--c-brand-tint, #f5f3ff);
  color: var(--c-brand-strong, #4f46e5);
}

.carrier-badge-telnyx { background: var(--c-success-tint, #f0fdf4); color: var(--c-success-text, #166534); }

.telnyx-connected {
  background: var(--c-success-tint, #f0fdf4);
  border: 1px solid var(--c-success-border, #bbf7d0);
  border-radius: 0.5rem;
  padding: 0.65rem 0.85rem;
  color: var(--c-success-text, #166534);
  font-size: 0.85rem;
  margin-bottom: 0.85rem;
}

.telnyx-hint { font-size: 0.72rem; margin-top: 0.2rem; }

.telnyx-number-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.55rem 0.75rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
  margin-bottom: 0.35rem;
  font-size: 0.875rem;
}

.telnyx-number-meta { font-size: 0.75rem; margin-left: 0.5rem; }

/* Flow-tab carrier badge (messages sent through Telnyx). */
.msg-carrier-badge {
  font-size: 0.65rem;
  background: var(--c-success-tint, #f0fdf4);
  color: var(--c-success-text, #166534);
  padding: 0.1rem 0.35rem;
  border-radius: 2rem;
  font-weight: 600;
  /* inline-block + a real margin, because this sits immediately after
     the status badge in the same cell and was running straight into it
     ("received📱 Twilio"). */
  display: inline-block;
  margin-left: 0.4rem;
  white-space: nowrap;
  vertical-align: middle;
  cursor: help;
}

/* Blue rather than green, so the two carriers are distinguishable at a
   glance in a history that contains both. */
.msg-carrier-badge-twilio { background: var(--c-info-tint, #eff6ff); color: var(--c-info-text, #1d4ed8); }

/* --- delivery error explanation ------------------------------------- */
.badge-a2p-blocked {
  background: var(--c-danger-tint, #fef2f2);
  color: var(--c-danger-darkest, #991b1b);
  border: 1px solid var(--c-danger-border, #fecaca);
}

.err-explain {
  background: var(--c-warning-tint, #fffbeb);
  border: 1px solid var(--c-warning-border, #fde68a);
  border-radius: 0.6rem;
  padding: 0.85rem 1rem;
  margin: 0.75rem 0;
}
.err-explain-title { font-weight: 700; font-size: 0.9rem; color: var(--c-warning-text, #92400e); margin-bottom: 0.35rem; }
.err-explain-body { font-size: 0.82rem; line-height: 1.6; color: var(--c-warning-text, #92400e); margin: 0; }
.err-explain-action { width: 100%; margin-top: 0.75rem; font-size: 0.85rem; }
.err-explain-guide { font-size: 0.8rem; margin-left: 0.75rem; }


/* --- Enhanced number search (number wizard) ------------------------- */

.num-search-carrier {
  font-size: 0.78rem;
  font-weight: 600;
  margin: 0 0 0.75rem;
}

.num-search-grid { display: grid; gap: 0.75rem; margin-bottom: 0.75rem; }
.num-search-grid-3 { grid-template-columns: 1fr 1fr 1fr; }
.num-search-grid-2 { grid-template-columns: 1fr 1fr; }

@media (max-width: 640px) {
  .num-search-grid-3,
  .num-search-grid-2 { grid-template-columns: 1fr; }
}

.num-search-grid select,
.num-search-grid input {
  width: 100%;
  padding: 0.55rem 0.6rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
  font-size: 0.875rem;
}

/* A text-style trigger, so it opts out of the app-wide gradient button. */
.num-advanced-toggle {
  background: none;
  border: none;
  color: var(--c-brand, #6366f1);
  font-size: 0.8rem;
  font-weight: 600;
  padding: 0;
  box-shadow: none;
  margin-bottom: 0.75rem;
}

.num-advanced-toggle:hover:not(:disabled) {
  background: none;
  box-shadow: none;
  transform: none;
  text-decoration: underline;
}

.num-advanced-panel {
  background: var(--c-surface-subtle, #f8fafc);
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.75rem;
  padding: 1rem;
  margin-bottom: 0.75rem;
}

.num-advanced-note { font-size: 0.72rem; margin: 0 0 0.6rem; }

.num-toggle-grid {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 0.6rem;
}

@media (max-width: 780px) {
  .num-toggle-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 480px) {
  .num-toggle-grid { grid-template-columns: 1fr; }
}

.num-toggle-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.8rem;
  cursor: pointer;
}

.num-toggle-row.num-toggle-disabled { opacity: 0.45; cursor: not-allowed; }

.num-toggle-name { display: block; font-weight: 600; color: var(--c-text, #0f172a); }
.num-toggle-desc { display: block; font-size: 0.7rem; color: var(--c-text-dim, #94a3b8); }

/* Namespaced .num-toggle-switch, NOT .toggle-switch. A .toggle-switch
   already exists further up this file: a <span> wrapper around a hidden
   checkbox plus a .toggle-slider, used by card.html and friends. These
   rules sit later in the file at equal specificity, so reusing that name
   would have silently overridden it and broken every existing toggle in
   the app. Built as a <button> for keyboard reach, which is why it
   resets the app-wide gradient button styling. */
.num-toggle-switch {
  position: relative;
  flex: 0 0 auto;
  width: 34px;
  height: 20px;
  padding: 0;
  border-radius: 999px;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  background: var(--c-border, #e2e8f0);
  box-shadow: none;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}

.num-toggle-switch::after {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--c-surface, #fff);
  box-shadow: 0 1px 2px color-mix(in srgb, var(--c-scrim, #0f172a) 25%, transparent);
  transition: transform 0.15s ease;
}

.num-toggle-switch.on {
  background: var(--btn-grad);
  border-color: transparent;
}

.num-toggle-switch.on::after { transform: translateX(14px); }

.num-toggle-switch:hover:not(:disabled) { transform: none; box-shadow: none; }
.num-toggle-switch:disabled { cursor: not-allowed; }

.num-search-btn { width: 100%; margin-bottom: 0.75rem; }

/* --- Managed number wizard + usage dashboard ------------------------ */

/* Both of these now sit BELOW the wizard panels (content first, then
   progress, then the pricing block as a footer), so their spacing hangs
   off the top rather than the bottom. */
.mnw-hero {
  background: var(--btn-grad);
  border-radius: 0.75rem;
  padding: 1.25rem;
  color: var(--c-on-accent, #ffffff);
  margin: 0.75rem 0 0;
}
.mnw-hero h4 { margin: 0 0 0.4rem; font-size: 0.95rem; }
.mnw-hero p { margin: 0; font-size: 0.8rem; opacity: 0.92; }
.mnw-hero-prices { display: flex; flex-wrap: wrap; gap: 1.25rem; margin-top: 0.75rem; font-size: 0.85rem; }

.mnw-steps {
  display: flex; gap: 0.5rem; list-style: none; padding: 0;
  margin: 1.25rem 0 0; font-size: 0.68rem; color: var(--c-text-dim, #94a3b8);
  padding-top: 1rem; border-top: 1px solid var(--c-border, #e2e8f0);
}
.mnw-step { flex: 1; text-align: center; }
.mnw-step-dot {
  display: flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; margin: 0 auto 0.25rem;
  border-radius: 50%; background: var(--c-border, #e2e8f0); color: var(--c-text-dim, #94a3b8);
  font-size: 0.78rem; font-weight: 700;
}
.mnw-step.active .mnw-step-dot { background: var(--btn-grad); color: var(--c-on-accent, #ffffff); }

.mnw-panel h4 { font-size: 0.9rem; font-weight: 700; margin: 0 0 0.75rem; }

/* --- configured managed number (replaces the whole setup UI) --------- */
.mnc-card {
  border: 1px solid var(--c-brand-border, #c7d2fe);
  background: linear-gradient(180deg, var(--c-brand-tint, #f5f3ff) 0%, var(--c-surface, #ffffff) 60%);
  border-radius: 0.75rem;
  padding: 1.25rem 1.4rem;
}

.mnc-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin-bottom: 1rem;
}

.mnc-label { font-size: 0.7rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--c-on-dark-brand, #a5b4fc); }
.mnc-number { font-size: 1.5rem; font-weight: 800; color: var(--c-text, #0f172a); letter-spacing: -0.01em; margin-top: 0.15rem; }

.mnc-carrier {
  flex: 0 0 auto;
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--c-brand-deep, #4338ca);
  background: var(--c-brand-tint-3, #e0e7ff);
  border-radius: 2rem;
  padding: 0.25rem 0.7rem;
}

.mnc-rows { display: flex; flex-direction: column; gap: 0.4rem; }

.mnc-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  font-size: 0.82rem;
  color: var(--c-text-muted, #64748b);
  padding-bottom: 0.4rem;
  border-bottom: 1px solid var(--c-violet-tint-2, #ede9fe);
}
.mnc-row:last-child { border-bottom: none; padding-bottom: 0; }
.mnc-row span:last-child { font-weight: 600; color: var(--c-text, #0f172a); text-align: right; }

.mnc-atcost { color: var(--c-success, #16a34a) !important; }

/* The messaging row carries three sub-rows, so it stacks rather than
   sitting on one baseline like the others. */
.mnc-row-caps { align-items: flex-start; flex-direction: column; gap: 0.5rem; }
.mnc-row-caps > span:last-child { text-align: left; width: 100%; font-weight: 400; }

.mnc-caps { display: flex; flex-direction: column; gap: 0.35rem; width: 100%; }

.mnc-cap {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  font-size: 0.82rem;
  color: var(--c-text-muted, #64748b);
}

/* Tabular figures so $0.0040 and $0.0100 line up on the decimal instead
   of drifting against each other. */
.mnc-cap-price { color: var(--c-success, #16a34a); font-weight: 600; font-variant-numeric: tabular-nums; }

/* Voice: the number HAS the capability, Linxi has not built calling on
   it yet -- /webhook/voice currently answers with "This number does not
   accept voice calls". Amber rather than green, because "Coming Soon" is
   a roadmap promise and should not sit in the same colour as the two
   prices next to it that are true today. */
.mnc-cap-soon { color: var(--c-warning, #f59e0b); font-weight: 600; }
.mnc-cap-on { color: var(--c-success, #16a34a); font-weight: 600; }

/* --- per-user compliance pages -------------------------------------- */
.cp-urls { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 0.75rem; margin: 0.75rem 0 1.25rem; }
.cp-url-card { background: var(--c-surface-subtle, #f8fafc); border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.75rem; padding: 0.9rem 1rem; }
.cp-url-label { font-size: 0.7rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--c-text-dim, #94a3b8); margin-bottom: 0.3rem; }
.cp-url { font-size: 0.8rem; font-weight: 600; color: var(--c-brand-strong, #4f46e5); word-break: break-all; margin-bottom: 0.5rem; }
.cp-url-actions { display: flex; align-items: center; gap: 0.35rem; flex-wrap: wrap; }
.cp-mini { font-size: 0.72rem; padding: 0.25rem 0.55rem; }
.cp-accepted { font-size: 0.68rem; font-weight: 700; background: var(--c-success-tint, #f0fdf4); color: var(--c-success-text, #166534); border: 1px solid var(--c-success-border, #bbf7d0); border-radius: 2rem; padding: 0.15rem 0.45rem; }
.cp-section-title { font-weight: 700; font-size: 0.85rem; color: var(--c-text, #0f172a); margin: 1.25rem 0 0.6rem; padding-top: 0.9rem; border-top: 1px solid var(--c-border, #e2e8f0); }
.cp-accept { background: var(--c-warning-tint, #fffbeb); border: 1px solid var(--c-warning-border, #fde68a); border-radius: 0.75rem; padding: 1rem; margin-top: 1rem; }
.cp-accept-title { font-weight: 700; font-size: 0.875rem; color: var(--c-warning-text, #92400e); margin-bottom: 0.35rem; }
.cp-accept p { font-size: 0.78rem; line-height: 1.6; color: var(--c-warning-text, #92400e); margin: 0 0 0.75rem; }
.cp-wizard-hint { background: var(--c-success-tint, #f0fdf4); border: 1px solid var(--c-success-border, #bbf7d0); border-radius: 0.6rem; padding: 0.75rem 0.85rem; margin: 0.75rem 0; }
.cp-wizard-title { font-size: 0.82rem; font-weight: 700; color: var(--c-success-text, #166534); margin-bottom: 0.25rem; }
.cp-wizard-body { font-size: 0.76rem; line-height: 1.55; color: var(--c-success-text, #166534); margin-bottom: 0.6rem; }

/* --- workspace cost panel + account balance ------------------------- */
.ws-bill-head { margin-bottom: 0.9rem; }
.ws-bill-title { font-size: 0.7rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--c-text-dim, #94a3b8); }
.ws-bill-number { font-size: 1.35rem; font-weight: 800; color: var(--c-text, #0f172a); margin-top: 0.15rem; }
.ws-bill-carrier { font-size: 0.78rem; color: var(--c-text-muted, #64748b); margin-top: 0.1rem; }
/* Reuses .mnc-row so this reads like the number panel above it rather
   than as a second, differently-styled table of the same shape. */
.mnc-row-total { border-top: 1px solid var(--c-border, #e2e8f0); border-bottom: none; padding-top: 0.5rem; margin-top: 0.15rem; font-weight: 700; }
.mnc-row-total span:last-child { color: var(--c-brand, #6366f1); }

.acct-balance { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; margin: 0.75rem 0 0.5rem; }
.acct-balance-label { font-size: 0.7rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--c-text-dim, #94a3b8); }
.acct-balance-amount { font-size: 2rem; font-weight: 900; color: var(--c-text, #0f172a); line-height: 1.1; font-variant-numeric: tabular-nums; }
.acct-balance-low { color: var(--c-danger-strong, #dc2626); }
.acct-balance-note { font-size: 0.78rem; color: var(--c-text-muted, #64748b); margin-top: 0.2rem; }

/* --- Messaging / Integrations settings panels ----------------------- */
.ms-toggle-row {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 1rem; padding: 0.75rem 0; border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9);
  cursor: pointer; font-weight: normal;
}
.ms-toggle-name { display: block; font-weight: 600; font-size: 0.875rem; color: var(--c-text, #0f172a); }
.ms-toggle-desc { display: block; font-size: 0.75rem; color: var(--c-text-muted, #64748b); line-height: 1.45; margin-top: 0.15rem; }

.ms-hours-grid { display: flex; flex-direction: column; gap: 0.4rem; margin: 0.75rem 0; }
.ms-hours-row {
  display: grid; grid-template-columns: 150px minmax(0, 1fr) minmax(0, 1fr);
  gap: 0.5rem; align-items: center;
  border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.5rem; padding: 0.4rem 0.6rem;
}
@media (max-width: 640px) { .ms-hours-row { grid-template-columns: minmax(0, 1fr); } }
.ms-hours-day { display: flex; align-items: center; gap: 0.4rem; font-size: 0.82rem; font-weight: 600; cursor: pointer; }
.ms-hours-time { font-size: 0.78rem; padding: 0.35rem 0.4rem; }

.ms-reserved {
  background: var(--c-warning-tint, #fffbeb); border: 1px solid var(--c-warning-border, #fde68a); border-radius: 0.75rem;
  padding: 0.9rem 1rem; margin: 0.75rem 0 1rem;
  font-size: 0.78rem; line-height: 1.7; color: var(--c-warning-text, #92400e);
}
.ms-reserved-title { font-weight: 700; margin-bottom: 0.35rem; }

.ms-list-row {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 0.75rem; border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.5rem;
  padding: 0.6rem 0.75rem; margin-bottom: 0.4rem;
}
.ms-list-left { display: flex; align-items: baseline; gap: 0.5rem; min-width: 0; flex: 1; }
.ms-list-stack { flex-direction: column; align-items: flex-start; gap: 0.15rem; }
.ms-list-name { font-size: 0.875rem; font-weight: 600; color: var(--c-text, #0f172a); }
/* Clamped rather than truncated with a substring: the full text stays in
   the DOM for search and screen readers. */
.ms-list-desc {
  font-size: 0.78rem; color: var(--c-text-muted, #64748b); line-height: 1.45;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; min-width: 0;
}
.ms-list-meta { font-size: 0.7rem; color: var(--c-text-dim, #94a3b8); }
.ms-list-actions { display: flex; gap: 0.35rem; flex: 0 0 auto; }

.ms-keyword-chip {
  font-family: monospace; font-weight: 700; font-size: 0.8rem;
  background: var(--c-brand-tint, #f5f3ff); color: var(--c-brand, #6366f1);
  border-radius: 0.25rem; padding: 0.15rem 0.4rem; flex: 0 0 auto;
}

.ms-use-btn { font-size: 0.72rem; padding: 0.25rem 0.55rem; }
.ms-del-btn {
  background: none; color: var(--c-danger, #ef4444); border: 1px solid var(--c-danger-border, #fecaca); box-shadow: none;
  border-radius: 0.35rem; padding: 0.2rem 0.45rem; font-size: 0.72rem; cursor: pointer;
}
.ms-del-btn:hover:not(:disabled) { background: var(--c-danger-tint, #fef2f2); color: var(--c-danger-strong, #dc2626); box-shadow: none; transform: none; }

.ms-add-box { border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.75rem; padding: 1rem; margin-top: 1rem; }
.ms-add-title { font-weight: 600; font-size: 0.875rem; margin-bottom: 0.6rem; color: var(--c-text, #0f172a); }
.ms-template-meta { display: flex; justify-content: space-between; font-size: 0.72rem; color: var(--c-text-dim, #94a3b8); margin-top: 0.3rem; }
.ms-empty { text-align: center; color: var(--c-text-dim, #94a3b8); font-size: 0.82rem; padding: 1rem; }
.ms-upper { text-transform: uppercase; }

.ws-auto-reply-note {
  font-size: 0.76rem;
  line-height: 1.55;
  color: var(--c-warning-text, #92400e);
  background: var(--c-warning-tint, #fffbeb);
  border: 1px solid var(--c-warning-border, #fde68a);
  border-radius: 0.5rem;
  padding: 0.55rem 0.7rem;
  margin: 0.4rem 0 0;
}
.mnc-voice-state { display: inline-flex; align-items: center; gap: 0.5rem; }
.mnc-voice-btn {
  background: var(--c-brand-tint-2, #eef2ff); color: var(--c-brand-deep, #4338ca); border: 1px solid var(--c-brand-border, #c7d2fe);
  border-radius: 0.35rem; padding: 0.15rem 0.5rem;
  font-size: 0.72rem; font-weight: 700; cursor: pointer; box-shadow: none;
}
.mnc-voice-btn:hover:not(:disabled) { background: var(--c-brand-tint-3, #e0e7ff); box-shadow: none; transform: none; }

/* --- Linxi Voice modals --------------------------------------------- */
.lv-modal {
  position: fixed; inset: 0; z-index: 25000;
  background: color-mix(in srgb, var(--c-scrim, #0f172a) 60%, transparent);
  display: flex; align-items: center; justify-content: center; padding: 1rem;
}
.lv-modal.hidden { display: none; }
.lv-box {
  background: var(--c-surface, #fff); border-radius: 1rem; padding: 1.75rem;
  max-width: 460px; width: 100%; max-height: 90vh; overflow-y: auto;
  box-shadow: 0 20px 60px color-mix(in srgb, var(--c-scrim, #0f172a) 30%, transparent);
}
.lv-head { text-align: center; margin-bottom: 1.25rem; }
.lv-icon { font-size: 2.5rem; margin-bottom: 0.4rem; }
.lv-head h3 { margin: 0 0 0.35rem; font-size: 1.1rem; font-weight: 700; color: var(--c-text, #0f172a); }
.lv-head p { margin: 0; font-size: 0.82rem; color: var(--c-text-muted, #64748b); line-height: 1.55; }

.lv-features { background: var(--c-surface-subtle, #f8fafc); border-radius: 0.75rem; padding: 1rem; margin-bottom: 1rem; }
.lv-features-title {
  font-size: 0.7rem; font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--c-success, #16a34a); margin-bottom: 0.5rem;
}
/* The "not built yet" heading is grey, not green. Two lists in one
   colour is how a roadmap gets read as a feature list. */
.lv-features-title-soon { color: var(--c-text-dim, #94a3b8); margin-top: 0.9rem; }
.lv-feature { display: flex; gap: 0.5rem; font-size: 0.8rem; line-height: 1.5; color: var(--c-text-2, #334155); margin-bottom: 0.35rem; }
.lv-yes { color: var(--c-success, #16a34a); font-weight: 700; flex: 0 0 auto; }
.lv-soon { color: var(--c-text-dim, #94a3b8); }

.lv-note, .lv-warn {
  border-radius: 0.5rem; padding: 0.6rem 0.75rem;
  font-size: 0.76rem; line-height: 1.55; margin-bottom: 1rem;
}
.lv-note { background: var(--c-info-tint, #eff6ff); border: 1px solid var(--c-info-border, #bfdbfe); color: var(--c-info-text, #1d4ed8); }
.lv-warn { background: var(--c-warning-tint, #fffbeb); border: 1px solid var(--c-warning-border, #fde68a); color: var(--c-warning-text, #92400e); }

.lv-settings-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; }
.lv-settings-head h3 { margin: 0; font-size: 1rem; font-weight: 700; color: var(--c-text, #0f172a); }
.lv-close { background: none; border: none; box-shadow: none; color: var(--c-text-dim, #94a3b8); font-size: 1rem; cursor: pointer; padding: 0; }

.lv-row {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 1rem; padding: 0.75rem 0; border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9);
  cursor: pointer; font-weight: normal;
}
.lv-row-name { display: block; font-weight: 600; font-size: 0.875rem; color: var(--c-text, #0f172a); }
.lv-row-desc { display: block; font-size: 0.75rem; color: var(--c-text-muted, #64748b); line-height: 1.45; margin-top: 0.15rem; }
.lv-field { padding: 0.75rem 0; }

.lv-primary { width: 100%; margin-top: 0.75rem; }
.lv-dismiss {
  width: 100%; background: none; border: none; box-shadow: none;
  color: var(--c-text-dim, #94a3b8); font-size: 0.8rem; cursor: pointer; padding: 0.5rem;
}
.lv-dismiss:hover:not(:disabled) { background: none; color: var(--c-text-muted, #64748b); box-shadow: none; transform: none; }
.lv-result { font-size: 0.8rem; text-align: center; margin: 0.5rem 0 0; }
.lv-result-ok { color: var(--c-success, #16a34a); font-weight: 600; }
.lv-result-bad { color: var(--c-danger-strong, #dc2626); font-weight: 600; }

.lv-calls { margin-top: 1.25rem; border-top: 1px solid var(--c-border, #e2e8f0); padding-top: 1rem; }
.lv-calls-title { font-weight: 600; font-size: 0.875rem; margin-bottom: 0.6rem; color: var(--c-text, #0f172a); }
.lv-empty { text-align: center; color: var(--c-text-dim, #94a3b8); font-size: 0.8rem; padding: 0.75rem; }
.lv-call { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0.5rem 0; border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9); }
.lv-call-left { display: flex; align-items: center; gap: 0.5rem; min-width: 0; }
.lv-call-number { font-size: 0.8rem; font-weight: 600; font-family: monospace; color: var(--c-text, #0f172a); }
.lv-call-when { font-size: 0.7rem; color: var(--c-text-dim, #94a3b8); }
.lv-call-right { text-align: right; flex: 0 0 auto; }
.lv-call-status { font-size: 0.78rem; font-weight: 600; text-transform: capitalize; }
.lv-call-ok { color: var(--c-success, #16a34a); }
.lv-call-pending { color: var(--c-brand, #6366f1); }
.lv-call-warn { color: var(--c-warning-text, #92400e); }
.lv-call-bad { color: var(--c-danger-strong, #dc2626); }


.mnc-badge {
  display: inline-block;
  background: var(--c-success-tint, #f0fdf4);
  color: var(--c-success-text, #166534);
  border: 1px solid var(--c-success-border, #bbf7d0);
  font-size: 0.72rem;
  font-weight: 700;
  padding: 0.2rem 0.5rem;
  border-radius: 2rem;
  margin-left: 0.4rem;
  vertical-align: 1px;
}
.mnc-a2p-idle { color: var(--c-text-dim, #94a3b8) !important; }
.mnc-a2p-ok { color: var(--c-success, #16a34a) !important; }
.mnc-a2p-pending { color: var(--c-warning-text, #92400e) !important; }
.mnc-a2p-bad { color: var(--c-danger-strong, #dc2626) !important; }

.mnc-a2p-note {
  margin: 0.75rem 0 0;
  font-size: 0.76rem;
  line-height: 1.55;
  color: var(--c-danger-darkest, #991b1b);
  background: var(--c-danger-tint, #fef2f2);
  border-left: 3px solid var(--c-danger, #ef4444);
  border-radius: 0.4rem;
  padding: 0.55rem 0.7rem;
}

.mnc-foot { margin: 0.9rem 0 0; font-size: 0.74rem; line-height: 1.55; color: var(--c-text-dim, #94a3b8); }

/* Save Progress sits apart from the Back/Next pair rather than in the
   flow of it -- it is not a step in the wizard and should not read like
   the next thing to click. */
.mnw-save {
  margin-left: auto;
  font-size: 0.78rem;
  padding: 0.4rem 0.7rem;
}

.mnw-draft-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
  background: var(--c-info-tint, #eff6ff);
  border: 1px solid var(--c-info-border, #bfdbfe);
  border-radius: 0.6rem;
  padding: 0.6rem 0.8rem;
  margin-bottom: 0.9rem;
  font-size: 0.78rem;
  color: var(--c-info-text, #1d4ed8);
}
/* Amber when the draft came back with a problem -- most often the saved
   number having been sold to someone else. Same box, different weight,
   so it is not mistaken for the ordinary "restored" notice. */
.mnw-draft-banner-warn {
  background: var(--c-warning-tint, #fffbeb);
  border-color: var(--c-warning-border, #fde68a);
  color: var(--c-warning-text, #92400e);
}
.mnw-draft-banner-warn .mnw-draft-discard { color: var(--c-warning-text, #92400e); }

.mnw-draft-discard {
  background: none;
  border: none;
  padding: 0;
  color: var(--c-info-text, #1d4ed8);
  font-size: 0.75rem;
  font-weight: 600;
  text-decoration: underline;
  cursor: pointer;
  box-shadow: none;
}

/* Business-information fields, two per row on desktop.
   Used by the managed wizard's Step 2 and the Setup Wizard's Step 4,
   which ask for the same five A2P fields. Breakpoint matches
   .num-search-grid so the whole wizard reflows to one column together
   rather than at two different widths.

   min-width: 0 on the cells because a grid item's default min-width is
   auto -- a long placeholder (the sample-message ones are long) would
   otherwise push the column past its 1fr share and break the pairing. */
.wiz-field-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.75rem 1rem;
  margin-bottom: 0.75rem;
}
.wiz-field-grid > div { min-width: 0; }
.wiz-field-grid input,
.wiz-field-grid select { width: 100%; }
/* .wizard-step-panel label carries a margin-top for the stacked layout;
   inside a grid cell that offsets the row against its neighbour. */
.wiz-field-grid label { margin-top: 0; }
/* An odd field out spans the row rather than sitting half-width. */
.wiz-field-full { grid-column: 1 / -1; }

@media (max-width: 640px) {
  .wiz-field-grid { grid-template-columns: 1fr; }
}
/* Step 1's Next is the only advance button that is not in a
   .table-toolbar, so it had nothing sizing it and stretched the full
   panel. Capped and pushed right, which lines it up with the Save
   Progress button in the toolbar directly beneath it. */
.mnw-next {
  width: 100%;
  max-width: 260px;
  margin: 1rem 0 0 auto;
}
/* Capped and right-aligned like .mnw-next above. A little wider, since
   its label is longer and its type is a size up. */
.mnw-submit {
  width: 100%;
  max-width: 280px;
  margin: 1rem 0 0 auto;
  font-size: 1rem;
  padding: 0.85rem;
}

.mnw-number-row {
  display: flex; align-items: center; justify-content: space-between;
  width: 100%; text-align: left; background: var(--c-surface, #fff); color: inherit;
  border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.5rem;
  padding: 0.6rem 0.75rem; margin-bottom: 0.35rem;
  font-weight: 400; box-shadow: none;
}
.mnw-number-row:hover:not(:disabled) {
  background: var(--c-brand-tint, #f5f3ff); border-color: var(--c-brand, #6366f1); box-shadow: none; transform: none;
}
.mnw-number { font-family: ui-monospace, Menlo, Consolas, monospace; font-weight: 600; }
.mnw-number-pick { font-size: 0.78rem; color: var(--c-brand, #6366f1); font-weight: 600; }

.mnw-selected {
  display: flex; align-items: center; justify-content: space-between;
  background: var(--c-success-tint, #f0fdf4); border: 1px solid var(--c-success-border, #bbf7d0); border-radius: 0.5rem;
  padding: 0.75rem; margin-top: 0.5rem;
}
.mnw-selected-number { font-weight: 700; font-family: ui-monospace, Menlo, Consolas, monospace; }
.mnw-selected-note { font-size: 0.75rem; color: var(--c-success-text, #166534); }

.mnw-checks { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin: 0.6rem 0; font-size: 0.82rem; }

/* --- Managed wizard Step 3: messaging setup ------------------------- */
/* Four free-text fields that a carrier reviewer reads word for word, so
   they get room to write in and a hint saying what actually gets a
   campaign approved. The two-line inputs this replaces made a 40-char
   minimum look like a one-line answer. */

.mnw-field { margin-bottom: 1rem; }

/* Says which three of the five are actually required, before someone
   counts five starred fields and assumes all of them are. */
.mnw-samples-note {
  font-size: 0.76rem;
  line-height: 1.6;
  color: var(--c-text-muted, #64748b);
  background: var(--c-surface-subtle, #f8fafc);
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
  padding: 0.6rem 0.75rem;
  margin: 0 0 0.9rem;
}
.mnw-samples-note strong { color: var(--c-text-2, #334155); }

.mnw-optional {
  display: inline-block;
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--c-text-muted, #64748b);
  background: var(--c-border, #e2e8f0);
  border-radius: 2rem;
  padding: 0.1rem 0.45rem;
  margin-left: 0.35rem;
  vertical-align: 1px;
}

.mnw-field-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
}
.mnw-field-head .field-label { margin-bottom: 0.3rem; }

.mnw-label-with-help { display: inline-flex; align-items: center; gap: 0.35rem; }

/* --- use case cards (managed wizard Step 3) ------------------------- */
/*
 * These are <button>s so the four options are keyboard-reachable and
 * announce a pressed state -- but that puts them under the app-wide
 * `button` rule, which paints the indigo gradient with white text.
 *
 * A plain `.usecase-card` (0,1,0) beats `button` (0,0,1) at rest, so the
 * cards looked right until the pointer touched one: the hover rule is
 * `button:hover:not(:disabled)` at (0,2,1), which OUT-SPECIFIES a bare
 * `.usecase-card:hover` at (0,2,0). Hovering any card therefore repainted
 * it as a full gradient button, complete with the lift and shadow.
 *
 * Hence the `.usecase-cards` parent on every selector below: (0,3,0)
 * clears (0,2,1) in all states. The neutralising block is explicit about
 * background-image as well as background-color, because the global rule
 * sets a gradient -- a background-color alone leaves the image painted
 * over it.
 */

/* Two up on mobile, four across on desktop. minmax(0, 1fr) rather than a
   bare 1fr: a grid track's default minimum is auto, so the longest
   unbroken word in a description would otherwise widen its column and
   throw the four out of alignment. 641px matches .num-search-grid, so
   the whole step reflows at one width rather than two. */
.usecase-cards {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.5rem;
  margin-bottom: 0.5rem;
}
@media (min-width: 641px) {
  .usecase-cards { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

.usecase-cards .usecase-card,
.usecase-cards .usecase-card:hover,
.usecase-cards .usecase-card:active,
.usecase-cards .usecase-card:focus,
.usecase-cards .usecase-card:focus-visible {
  background-image: none;
  background-color: var(--c-surface, #ffffff);
  color: var(--c-text, #0f172a);
  box-shadow: none;
  transform: none;
}

/* Column, not row: at a quarter of the width there is nowhere to put a
   price beside a title. */
.usecase-cards .usecase-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-start;
  gap: 0.3rem;
  width: 100%;
  min-width: 0;
  height: 100%;
  text-align: left;
  border: 2px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
  padding: 0.7rem;
  cursor: pointer;
  font-weight: 400;
  transition: border-color 0.15s ease, background-color 0.15s ease;
}
.usecase-cards .usecase-card:hover { border-color: var(--c-text-dim, #94a3b8); }
.usecase-cards .usecase-card:focus-visible { outline: 2px solid var(--c-brand, #6366f1); outline-offset: 2px; }

/* After the :hover rule, not before -- same (0,3,0) specificity, so the
   selected card keeps its tint and border while the pointer is on it. */
.usecase-cards .usecase-card.selected,
.usecase-cards .usecase-card.selected:hover {
  background-color: var(--c-brand-tint, #f5f3ff);
  border-color: var(--c-brand, #6366f1);
}

.usecase-card-main { display: block; width: 100%; min-width: 0; }
.usecase-card-name { display: block; font-weight: 700; font-size: 0.82rem; color: var(--c-text, #0f172a); line-height: 1.3; }
.usecase-card-desc { display: block; font-size: 0.72rem; color: var(--c-text-muted, #64748b); margin-top: 0.25rem; line-height: 1.45; }
/* margin-top: auto pins the price to the bottom of its card, so all four
   line up however long the descriptions run. */
.usecase-card-price { margin-top: auto; padding-top: 0.4rem; font-size: 0.8rem; font-weight: 600; color: var(--c-text-muted, #64748b); white-space: nowrap; }
.usecase-card-price-accent { color: var(--c-brand, #6366f1); }
.usecase-card-price-free { color: var(--c-success-alt, #10b981); }

.usecase-badge {
  display: inline-block;
  background: var(--c-success-alt, #10b981);
  color: var(--c-on-fill, #ffffff);
  font-size: 0.65rem;
  font-weight: 700;
  padding: 0.1rem 0.4rem;
  border-radius: 2rem;
  /* Its own line in a quarter-width card -- inline beside a title that is
     already wrapping just makes a third ragged line. */
  margin: 0.25rem 0 0;
  vertical-align: middle;
  white-space: nowrap;
}

/* The (?) beside a label. Deliberately quiet -- it is an offer of help,
   not a control the user has to deal with before filling the field. */
.help-icon-btn {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  padding: 0;
  margin-bottom: 0.3rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 50%;
  background: var(--c-surface, #fff);
  color: var(--c-text-muted, #64748b);
  font-size: 0.65rem;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  box-shadow: none;
  transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
}
.help-icon-btn:hover,
.help-icon-btn:focus-visible {
  border-color: var(--c-brand, #6366f1);
  color: var(--c-brand, #6366f1);
  background: var(--c-brand-tint-2, #eef2ff);
}

.mnw-input {
  width: 100%;
  padding: 0.7rem 0.8rem;
  font-size: 0.9rem;
  font-family: inherit;
  line-height: 1.5;
  color: var(--c-text, #0f172a);
  background: var(--c-surface, #fff);
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.6rem;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.mnw-input:focus {
  outline: none;
  border-color: var(--c-brand, #6366f1);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-brand, #6366f1) 15%, transparent);
}
.mnw-input::placeholder { color: var(--c-text-dim, #94a3b8); }

/* resize: vertical, not both -- horizontal drag inside a fixed-width
   modal just clips the field against the panel edge. */
.mnw-textarea { resize: vertical; min-height: 5.5rem; }

.mnw-hint {
  margin: 0.35rem 0 0;
  font-size: 0.72rem;
  line-height: 1.45;
  color: var(--c-text-dim, #94a3b8);
}

/* Live count for the description, whose 40-character floor was
   previously only discoverable by being rejected at Next. */
.mnw-counter {
  flex: 0 0 auto;
  font-size: 0.7rem;
  font-weight: 600;
  color: var(--c-text-dim, #94a3b8);
  font-variant-numeric: tabular-nums;
}
.mnw-counter.is-short { color: var(--c-danger-strong, #dc2626); }
.mnw-counter.is-ok { color: var(--c-success, #16a34a); }

/* The two capability checkboxes read as cards here rather than as bare
   ticks, matching the toggle rows in the number search above. */
.mnw-checks .num-toggle-row {
  align-items: flex-start;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.6rem;
  padding: 0.6rem 0.7rem;
  background: var(--c-surface, #fff);
  transition: border-color 0.15s ease, background 0.15s ease;
}
.mnw-checks .num-toggle-row:has(input:checked) {
  border-color: var(--c-brand-border, #c7d2fe);
  background: var(--c-brand-tint, #f5f3ff);
}
.mnw-checks label { display: flex; align-items: center; gap: 0.4rem; cursor: pointer; }

.mnw-summary { background: var(--c-surface-subtle, #f8fafc); border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.75rem; padding: 1rem; margin-bottom: 1rem; font-size: 0.82rem; }
.mnw-summary-row { display: flex; justify-content: space-between; margin-bottom: 0.35rem; }

.mnw-estimate { background: var(--c-success-tint, #f0fdf4); border: 1px solid var(--c-success-border, #bbf7d0); border-radius: 0.5rem; padding: 0.75rem; margin-bottom: 1rem; font-size: 0.82rem; color: var(--c-success-text, #166534); }
.mnw-estimate-title { font-weight: 700; margin-bottom: 0.35rem; }
.mnw-estimate-row { display: flex; justify-content: space-between; margin-top: 0.2rem; }
.mnw-estimate-total { border-top: 1px solid var(--c-success-border, #bbf7d0); margin-top: 0.5rem; padding-top: 0.5rem; font-weight: 700; }

.mnw-warning { background: var(--c-warning-tint, #fffbeb); border: 1px solid var(--c-warning-border, #fde68a); border-radius: 0.5rem; padding: 0.75rem; margin-bottom: 1rem; font-size: 0.78rem; color: var(--c-warning-text, #92400e); }
/* Half the panel, hard left. Three consent paragraphs set across a full
   settings panel run to a line length nobody reads -- and these are the
   three sentences on this screen that most need reading. */
.mnw-agree-group {
  width: 50%;
  margin: 0 auto 1rem 0;
}

@media (max-width: 768px) {
  .mnw-agree-group { width: 100%; }
}

/* Three stacked agreements need to read as three, not as one paragraph
   with ticks in it. align-items: flex-start puts the box at the top left
   of its own text rather than centred against three wrapped lines. */
.mnw-agree {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  font-size: 0.8rem;
  margin-bottom: 0.75rem;
  cursor: pointer;
  line-height: 1.5;
  font-weight: normal;
}
.mnw-agree input[type="checkbox"] { margin-top: 0.2rem; flex-shrink: 0; width: auto; }
.mnw-agree:last-child { margin-bottom: 0; }

.usage-card { background: var(--c-surface, #fff); border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.75rem; padding: 1.25rem; margin-bottom: 1rem; }
.usage-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.75rem; }
.usage-head h4 { margin: 0; font-size: 0.9rem; font-weight: 700; }
.usage-bar { background: var(--c-surface-sunken, #f1f5f9); border-radius: 2rem; height: 8px; overflow: hidden; margin-bottom: 0.5rem; }
.usage-bar-fill { height: 100%; border-radius: 2rem; transition: width 0.5s ease; }
.usage-ok { background: var(--c-success-alt, #10b981); color: var(--c-on-status, #0f172a); } .usage-warn { background: var(--c-warning, #f59e0b); color: var(--c-on-status, #0f172a); } .usage-over { background: var(--c-danger, #ef4444); color: var(--c-on-status, #0f172a); }
.usage-ok-text { color: var(--c-success-alt, #10b981); font-weight: 600; } .usage-warn-text { color: var(--c-warning, #f59e0b); font-weight: 600; } .usage-over-text { color: var(--c-danger, #ef4444); font-weight: 600; }
.usage-legend { display: flex; justify-content: space-between; font-size: 0.78rem; color: var(--c-text-muted, #64748b); margin-bottom: 1rem; }
.usage-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.75rem; }
.usage-stats > div { text-align: center; background: var(--c-surface-subtle, #f8fafc); border-radius: 0.5rem; padding: 0.6rem; }
.usage-stats strong { display: block; font-size: 1.1rem; color: var(--c-text, #0f172a); }
.usage-stats span { font-size: 0.65rem; color: var(--c-text-dim, #94a3b8); }

@media (max-width: 560px) { .usage-stats { grid-template-columns: 1fr 1fr; } .mnw-checks { grid-template-columns: 1fr; } }

/* --- Locked carrier badge + Danger Zone rows ------------------------ */

.carrier-locked {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  background: var(--c-surface-subtle, #f8fafc);
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.75rem;
  padding: 0.85rem 1rem;
  margin-top: 0.35rem;
}

.carrier-locked-icon { font-size: 1.25rem; }
.carrier-locked-name { font-weight: 700; font-size: 0.875rem; color: var(--c-text, #0f172a); }

.carrier-locked-tag {
  font-size: 0.7rem;
  background: var(--btn-grad);
  color: var(--c-on-accent, #ffffff);
  padding: 0.1rem 0.45rem;
  border-radius: 2rem;
  margin-left: 0.35rem;
  font-weight: 700;
}

.carrier-locked-desc { font-size: 0.75rem; color: var(--c-text-muted, #64748b); }
.carrier-locked-state { margin-left: auto; font-size: 0.72rem; color: var(--c-text-dim, #94a3b8); white-space: nowrap; }

.danger-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  border-top: 1px solid var(--c-border, #e2e8f0);
  margin-top: 1rem;
  padding-top: 1rem;
}

.danger-item-title { font-weight: 600; font-size: 0.875rem; color: var(--c-text, #0f172a); }
.danger-item-desc { font-size: 0.78rem; color: var(--c-text-dim, #94a3b8); margin-top: 0.15rem; }

/* Outlined rather than filled: it is a reversible settings change, not a
   destructive one like Delete This Linxi directly above it. */
.danger-item-btn {
  background: var(--c-surface, #fff);
  border: 1px solid var(--c-danger-border, #fecaca);
  color: var(--c-danger, #ef4444);
  padding: 0.4rem 0.85rem;
  border-radius: 0.5rem;
  font-size: 0.8rem;
  white-space: nowrap;
  box-shadow: none;
}

.danger-item-btn:hover:not(:disabled) {
  background: var(--c-danger-tint, #fef2f2);
  border-color: var(--c-on-dark-danger, #fca5a5);
  color: var(--c-danger-deep, #b91c1c);
  box-shadow: none;
}

@media (max-width: 560px) {
  .danger-item { flex-direction: column; align-items: flex-start; }
}

/* --- "How do you want to set up your number?" picker ---------------- */

.num-picker-label { display: block; margin-bottom: 0.75rem; }

.num-picker-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; margin-bottom: 1.5rem; }
@media (max-width: 700px) { .num-picker-grid { grid-template-columns: 1fr; } }

/* Real <button>s so they are keyboard-reachable and announce as
   controls -- which means opting out of the app-wide gradient styling. */
.num-picker-card {
  display: block;
  text-align: left;
  background: var(--c-surface, #fff);
  border: 2px solid var(--c-border, #e2e8f0);
  border-radius: 0.75rem;
  padding: 1.25rem;
  cursor: pointer;
  box-shadow: none;
  color: inherit;
  font-weight: 400;
}

.num-picker-card:hover:not(:disabled) {
  border-color: var(--c-brand, #6366f1);
  background: var(--c-surface-subtle, #f8fafc);
  box-shadow: none;
  transform: none;
}

.num-picker-card-primary { border-color: var(--c-brand, #6366f1); background: var(--c-brand-tint, #f5f3ff); }
.num-picker-card-primary:hover:not(:disabled) { background: var(--c-violet-tint-2, #ede9fe); }

.num-picker-icon { display: block; font-size: 1.5rem; margin-bottom: 0.5rem; }
.num-picker-title { display: block; font-weight: 700; font-size: 0.875rem; color: var(--c-text, #0f172a); margin-bottom: 0.25rem; }
.num-picker-desc { display: block; font-size: 0.75rem; color: var(--c-text-muted, #64748b); line-height: 1.45; }

.num-picker-tag {
  display: inline-block;
  margin-top: 0.75rem;
  background: var(--btn-grad);
  color: var(--c-on-accent, #ffffff);
  font-size: 0.72rem;
  font-weight: 600;
  padding: 0.25rem 0.6rem;
  border-radius: 0.3rem;
}

.num-setup-back {
  background: none;
  border: none;
  color: var(--c-brand, #6366f1);
  font-size: 0.8rem;
  font-weight: 600;
  padding: 0;
  margin-bottom: 0.75rem;
  box-shadow: none;
}

.num-setup-back:hover:not(:disabled) {
  background: none;
  box-shadow: none;
  transform: none;
  text-decoration: underline;
}

/* Price line inside a setup-picker card. */
.num-picker-price {
  display: block;
  margin-top: 0.5rem;
  font-size: 0.75rem;
  color: var(--c-brand, #6366f1);
  font-weight: 600;
}

/* "Recommended" reads as an endorsement rather than a state, so it gets
   the same treatment as the primary card's tag. */
.carrier-tag-recommended { background: var(--c-success-alt, #10b981); color: var(--c-on-status, #0f172a); }

/* --- Managed wizard: number search results -------------------------- */

.mnw-results { max-height: 280px; overflow-y: auto; margin-top: 0.5rem; }
.mnw-results-count { font-size: 0.72rem; color: var(--c-text-dim, #94a3b8); margin-bottom: 0.4rem; }

.mnw-results-empty {
  text-align: center;
  padding: 1.25rem;
  background: var(--c-surface-subtle, #f8fafc);
  border: 1px dashed var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
  color: var(--c-text-dim, #94a3b8);
  font-size: 0.82rem;
}

/* The result row is a <button> (keyboard-reachable), so it needs the
   app-wide gradient button styling reset -- same as the picker cards. */
.mnw-number-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  width: 100%;
  text-align: left;
}

.mnw-number-row > span { display: flex; flex-direction: column; }
.mnw-number-meta { font-size: 0.68rem; color: var(--c-text-dim, #94a3b8); margin-top: 0.1rem; }
.mnw-number-price { align-items: flex-end; }
.mnw-number-price-main { font-size: 0.82rem; font-weight: 700; color: var(--c-brand, #6366f1); }
.mnw-number-price-sub { font-size: 0.65rem; color: var(--c-text-dim, #94a3b8); }

/* Checkbox variant of the toggle rows (the BYO wizard uses switches). */
.num-toggle-row input[type="checkbox"] { flex: 0 0 auto; cursor: pointer; }
.num-toggle-row input[type="checkbox"]:disabled { cursor: not-allowed; }/* Linxi Name + Description share a row on desktop and stack on narrow
   screens. The stacking rule is scoped to .ws-name-row rather than added
   to .form-row itself: .form-row is used in 19 other places (City/State,
   and other short paired fields that are fine side by side on a phone),
   and widening the rule would restyle all of them as a side effect. */
.ws-name-row > div { display: flex; flex-direction: column; }
.ws-name-row input { width: 100%; }

@media (max-width: 700px) {
  .ws-name-row { flex-direction: column; gap: 0; }
}

/* --- Setup Wizard: merged "Add Manually" carrier card ---------------- */

/* The BYO card is a <div> wrapping its own buttons, so it needs the
   .carrier-option surface without the button reset that class assumes. */
.wiz-byo-card { padding: 0; overflow: hidden; }@media (max-width: 560px) { }.wiz-byo-option.active {
  border-color: var(--c-brand, #6366f1);
  background: var(--c-brand-tint, #f5f3ff);
}/* Helper line under the combinable number-search filters. */
.num-filter-note {
  font-size: 0.72rem;
  margin: 0 0 0.75rem;
  line-height: 1.45;
}

/* --- Weighting the recommended option -------------------------------
   In both of these pairs the Linxi-managed path is the one almost
   everyone should take, and the alternative exists for the minority who
   already have a carrier account. Equal-sized cards said the opposite.
   The recommended card is scaled up; the alternative is deliberately
   quieter rather than hidden, so it is still an obvious way out. */

/* Setup Wizard Step 2 */
.carrier-option-hero {
  padding: 1.5rem;
  border-width: 2px;
  border-color: var(--c-brand, #6366f1);
  background: linear-gradient(180deg, var(--c-brand-tint, #f5f3ff) 0%, var(--c-surface, #ffffff) 100%);
  box-shadow: 0 6px 20px color-mix(in srgb, var(--c-brand, #6366f1) 16%, transparent);
}

.carrier-option-hero:hover:not(:disabled) {
  background: linear-gradient(180deg, var(--c-violet-tint-2, #ede9fe) 0%, var(--c-surface, #ffffff) 100%);
  border-color: var(--c-brand, #6366f1);
  box-shadow: 0 8px 26px color-mix(in srgb, var(--c-brand, #6366f1) 22%, transparent);
  transform: none;
}

.carrier-option-hero .carrier-option-icon { font-size: 1.9rem; }
.carrier-option-hero .carrier-option-name { font-size: 1.05rem; }
.carrier-option-hero .carrier-option-desc { font-size: 0.85rem; line-height: 1.55; }
.carrier-option-hero .carrier-option-head { margin-bottom: 0.55rem; }

.carrier-option-hero .wiz-carrier-prices {
  margin-top: 0.85rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--c-violet-border, #ddd6fe);
  font-size: 0.85rem;
}

/* The quieter alternative. */
.carrier-option-alt { opacity: 0.92; }
.carrier-option-alt .carrier-option-icon { font-size: 1rem; }
.carrier-option-alt .carrier-option-name { font-size: 0.82rem; }
.carrier-option-alt .carrier-option-desc { font-size: 0.72rem; }
.carrier-option-alt.active { opacity: 1; }

/* Number-setup picker: give the recommended card the wider column too,
   so the weighting reads before any text is scanned. */
.num-picker-grid { grid-template-columns: 1.55fr 1fr; align-items: start; }
@media (max-width: 700px) { .num-picker-grid { grid-template-columns: 1fr; } }

.num-picker-card-primary {
  padding: 1.6rem;
  box-shadow: 0 6px 20px color-mix(in srgb, var(--c-brand, #6366f1) 16%, transparent);
}

.num-picker-card-primary:hover:not(:disabled) {
  box-shadow: 0 8px 26px color-mix(in srgb, var(--c-brand, #6366f1) 22%, transparent);
}

.num-picker-card-primary .num-picker-icon { font-size: 2.1rem; margin-bottom: 0.65rem; }
.num-picker-card-primary .num-picker-title { font-size: 1.05rem; margin-bottom: 0.4rem; }
.num-picker-card-primary .num-picker-desc { font-size: 0.85rem; line-height: 1.55; }

.num-picker-card-alt { opacity: 0.92; padding: 1rem; }
.num-picker-card-alt .num-picker-icon { font-size: 1.15rem; margin-bottom: 0.35rem; }
.num-picker-card-alt .num-picker-title { font-size: 0.8rem; }
.num-picker-card-alt .num-picker-desc { font-size: 0.72rem; }
.num-picker-card-alt:hover:not(:disabled) { opacity: 1; }

/* Single messaging-cost note under the number results, replacing the
   per-row price that repeated identically on every line. */
.mnw-results-note {
  font-size: 0.72rem;
  color: var(--c-text-muted, #64748b);
  margin-top: 0.5rem;
  text-align: center;
  line-height: 1.45;
}

.mnw-results-note-warn {
  color: var(--c-warning-text, #92400e);
  background: var(--c-warning-tint, #fffbeb);
  border: 1px solid var(--c-warning-border, #fde68a);
  border-radius: 0.4rem;
  padding: 0.45rem 0.6rem;
  text-align: left;
}

/* The messaging-cost line under the results is pricing, not fine print --
   it replaced a figure that used to sit on every row, so it needs enough
   weight to be read as the answer to "what do messages cost". */
.mnw-results-note-price {
  margin-top: 0.65rem;
  padding: 0.5rem 0.75rem;
  background: var(--c-success-tint, #f0fdf4);
  border: 1px solid var(--c-success-border, #bbf7d0);
  border-radius: 0.5rem;
  color: var(--c-success-text, #166534);
  font-size: 0.78rem;
  font-weight: 600;
}

/* The prefix filter that is currently unusable because its counterpart
   is filled -- dimmed rather than hidden, so it stays visible as an
   option you could switch to. */
.num-filter-muted { opacity: 0.5; }
.num-filter-muted input { cursor: not-allowed; }

/* ── Number Lookup / carrier display ────────────────────────────────
   Contact-side carrier information. Distinct from the Flow log's own
   carrier pill (.badge in the Status column), which reports OUR carrier
   -- see the header of public/js/carrierInfo.js. */

.carrier-badge-row {
  display: flex; flex-wrap: wrap; gap: 0.35rem;
  align-items: center; margin-top: 0.4rem;
}
.carrier-badge {
  background: var(--c-surface-subtle, #f8fafc); border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.35rem;
  padding: 0.15rem 0.45rem; font-size: 0.72rem; color: var(--c-text-3, #475569); white-space: nowrap;
}
/* --carrier-color is set per line type from the inline style attribute;
   color-mix keeps the fill and border in step with it without three
   more hardcoded pairs per type. */
.carrier-type-badge {
  color: var(--carrier-color, var(--c-text-dim, #94a3b8));
  border-color: color-mix(in srgb, var(--carrier-color, var(--c-text-dim, #94a3b8)) 40%, transparent);
  background: color-mix(in srgb, var(--carrier-color, var(--c-text-dim, #94a3b8)) 12%, transparent);
  font-weight: 600;
}
.carrier-meta { font-size: 0.72rem; color: var(--c-text-dim, #94a3b8); white-space: nowrap; }
.carrier-flag {
  font-size: 0.68rem; border-radius: 0.35rem; padding: 0.1rem 0.35rem; white-space: nowrap;
}
.carrier-flag-ported { background: var(--c-warning-tint, #fffbeb); color: var(--c-warning-text, #92400e); border: 1px solid var(--c-warning-border, #fde68a); }
.carrier-flag-invalid { background: var(--c-danger-tint, #fef2f2); color: var(--c-danger-darkest, #991b1b); border: 1px solid var(--c-danger-border, #fecaca); }
.carrier-note {
  border-radius: 0.35rem; padding: 0.35rem 0.6rem;
  font-size: 0.72rem; margin-top: 0.35rem;
}
.carrier-note-warn { background: var(--c-warning-tint, #fffbeb); border: 1px solid var(--c-warning-border, #fde68a); color: var(--c-warning-text, #92400e); }
.carrier-note-info { background: var(--c-info-tint, #eff6ff); border: 1px solid var(--c-info-border, #bfdbfe); color: var(--c-info-text, #1d4ed8); }
.carrier-line { font-size: 0.72rem; color: var(--c-text-dim, #94a3b8); margin-top: 0.1rem; }

.profile-carrier-info { margin-top: 0.5rem; }
.contact-carrier-icon { font-size: 0.85rem; margin-left: 0.25rem; cursor: default; }

/* Flow tab: Messages / Carriers switch */
.flow-view-toggle { display: flex; gap: 0.4rem; margin-bottom: 0.75rem; flex-wrap: wrap; }
.flow-view-btn {
  background: var(--c-surface, #fff); border: 1px solid var(--c-border, #e2e8f0); color: var(--c-text-muted, #64748b);
  border-radius: 0.5rem; padding: 0.35rem 0.9rem; font-size: 0.82rem;
  font-weight: 600; cursor: pointer;
}
.flow-view-btn.active { background: var(--c-brand, #6366f1); border-color: var(--c-brand, #6366f1); color: var(--c-on-fill, #ffffff); }

.carrier-summary {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 0.6rem; margin-top: 0.75rem;
}
.carrier-tile {
  display: flex; flex-direction: column; align-items: center; gap: 0.15rem;
  background: var(--c-surface-subtle, #f8fafc); border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.6rem;
  padding: 0.7rem 0.5rem; cursor: pointer; text-align: center;
}
.carrier-tile.active {
  border-color: var(--carrier-color, var(--c-brand, #6366f1));
  background: color-mix(in srgb, var(--carrier-color, var(--c-brand, #6366f1)) 10%, transparent);
}
.carrier-tile-icon { font-size: 1.1rem; }
.carrier-tile-count { font-size: 1.25rem; font-weight: 800; color: var(--c-text, #0f172a); }
.carrier-tile-label { font-size: 0.72rem; color: var(--c-text-muted, #64748b); }

.carrier-breakdown { display: flex; flex-direction: column; gap: 0.4rem; margin-top: 0.5rem; }
.carrier-bar-row { display: flex; align-items: center; gap: 0.6rem; font-size: 0.8rem; }
.carrier-bar-label { flex: 0 0 40%; color: var(--c-text-3, #475569); overflow-wrap: anywhere; }
.carrier-bar-track { flex: 1; background: var(--c-surface-sunken, #f1f5f9); border-radius: 2rem; height: 0.5rem; overflow: hidden; }
.carrier-bar-fill { display: block; height: 100%; background: var(--carrier-color, var(--c-text-dim, #94a3b8)); }
.carrier-bar-count { flex: 0 0 2.5rem; text-align: right; color: var(--c-text-dim, #94a3b8); font-size: 0.75rem; }

@media (max-width: 600px) {
  .carrier-bar-label { flex-basis: 55%; }
}

/* ── Number lookup: pay dialogue and batch progress ─────────────────── */

.lookup-pay-modal { max-width: 26rem; }
.lookup-pay-lines {
  background: var(--c-surface-subtle, #f8fafc); border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.6rem;
  padding: 0.85rem 1rem; margin: 1rem 0;
}
.lookup-pay-line {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 1rem; font-size: 0.875rem; padding: 0.22rem 0; color: var(--c-text-3, #475569);
}
.lookup-pay-line strong { color: var(--c-text, #0f172a); }
.lookup-pay-total {
  border-top: 1px solid var(--c-border, #e2e8f0); margin-top: 0.4rem;
  padding-top: 0.5rem; font-size: 1rem;
}
.lookup-pay-total strong { font-size: 1.15rem; }
.lookup-pay-muted { color: var(--c-text-dim, #94a3b8); font-size: 0.8rem; }
.lookup-pay-note { font-size: 0.75rem; margin-top: -0.35rem; }
.lookup-pay-short {
  background: var(--c-danger-tint, #fef2f2); border: 1px solid var(--c-danger-border, #fecaca); color: var(--c-danger-darkest, #991b1b);
  border-radius: 0.5rem; padding: 0.6rem 0.8rem; font-size: 0.8rem; margin-bottom: 0.75rem;
}
.lookup-pay-short a { color: var(--c-danger-darkest, #991b1b); font-weight: 600; }
.lookup-price-note {
  background: var(--c-info-tint, #eff6ff); border: 1px solid var(--c-info-border, #bfdbfe); color: var(--c-info-text, #1d4ed8);
  border-radius: 0.5rem; padding: 0.6rem 0.85rem; font-size: 0.82rem; margin: 0.75rem 0;
}
.lookup-price-note strong { color: var(--c-info-text, #1d4ed8); }

/* Pinned rather than a toast per tick -- a 1,000-number batch reports
   every ten lookups, and a hundred stacked toasts is a wall, not
   progress. */
.lookup-progress-bar {
  position: fixed; left: 50%; bottom: 1.25rem; transform: translateX(-50%) translateY(150%);
  background: var(--c-inverse-surface, #0f172a); color: var(--c-on-accent, #ffffff); border-radius: 0.6rem;
  padding: 0.65rem 1rem; font-size: 0.8rem; z-index: 900;
  display: flex; align-items: center; gap: 0.75rem;
  box-shadow: 0 8px 24px color-mix(in srgb, var(--c-scrim, #0f172a) 25%, transparent);
  transition: transform 0.2s ease; max-width: min(90vw, 26rem);
}
.lookup-progress-bar.visible { transform: translateX(-50%) translateY(0); }
.lookup-progress-label { white-space: nowrap; }
.lookup-progress-track {
  flex: 1; min-width: 5rem; height: 0.35rem;
  background: color-mix(in srgb, var(--c-highlight, #ffffff) 20%, transparent); border-radius: 2rem; overflow: hidden;
}
.lookup-progress-fill {
  display: block; height: 100%; background: var(--c-brand, #6366f1); transition: width 0.3s ease;
}
.carrier-flag-ok { background: var(--c-success-tint, #f0fdf4); color: var(--c-success-text, #166534); border: 1px solid var(--c-success-border, #bbf7d0); }

/* ── Compliance pages: signed-off state ─────────────────────────────── */

.cp-signed {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
  background: var(--c-success-tint, #f0fdf4); border: 1px solid var(--c-success-border, #bbf7d0);
  border-radius: 0.6rem; padding: 0.85rem 1rem; margin-top: 1rem;
}
.cp-signed-head { display: flex; align-items: center; gap: 0.7rem; }
.cp-signed-check { font-size: 1.35rem; line-height: 1; }
.cp-signed-title { font-weight: 700; color: var(--c-success-text, #166534); font-size: 0.9rem; }
.cp-signed-when { font-size: 0.8rem; color: var(--c-success-text, #166534); }

.cp-edit-warning {
  background: var(--c-warning-tint, #fffbeb); border: 1px solid var(--c-warning-border, #fde68a); color: var(--c-warning-text, #92400e);
  border-radius: 0.5rem; padding: 0.6rem 0.85rem;
  font-size: 0.8rem; margin-top: 0.75rem;
}

/* Locked form. Greyed rather than hidden -- the whole point is that the
   signed content stays readable; it just cannot be changed without
   going through the Edit button first. */
.cp-locked input:disabled,
.cp-locked select:disabled,
.cp-locked textarea:disabled {
  background: var(--c-surface-subtle, #f8fafc); color: var(--c-text-muted, #64748b); cursor: default; opacity: 1;
}
.cp-locked button:disabled { opacity: 0.45; cursor: not-allowed; }

/* ── Admin users directory ──────────────────────────────────────────── */


.users-sortable { cursor: pointer; user-select: none; white-space: nowrap; }
.users-sortable:hover { color: var(--c-brand, #6366f1); }
/* The arrow is on a data attribute rather than a class pair so the JS
   sets one thing, not two that can disagree. */
.users-sortable.sorted[data-sort-dir="asc"]::after { content: ' \2191'; color: var(--c-brand, #6366f1); }
.users-sortable.sorted[data-sort-dir="desc"]::after { content: ' \2193'; color: var(--c-brand, #6366f1); }

.users-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.users-muted-cell { color: var(--c-text-dim, #94a3b8); font-size: 0.82rem; white-space: nowrap; }
.users-balance-empty { color: var(--c-text-dim, #94a3b8); }

.users-status { font-size: 0.7rem; }
.users-status-active { background: var(--c-success-alt, #10b981); color: var(--c-on-status, #0f172a); }
.users-status-suspended { background: var(--c-danger, #ef4444); color: var(--c-on-status, #0f172a); }
.users-status-unverified { background: var(--c-warning, #f59e0b); color: var(--c-on-status, #0f172a); }
.users-status-demo { background: var(--c-text-dim, #94a3b8); }
.users-provider {
  font-size: 0.66rem; color: var(--c-text-muted, #64748b); background: var(--c-surface-sunken, #f1f5f9);
  border-radius: 0.3rem; padding: 0.05rem 0.3rem; margin-left: 0.3rem;
}

.users-recent-date { flex-basis: auto; color: var(--c-text-dim, #94a3b8); }

/* ⚠️ `flex: 1 1` so the search still takes the room when the profile
   dropdown joins it in the toolbar -- without a grow it keeps its
   content width and the two sit squeezed at the left. The max-width is
   what stops it running the full width of a wide screen. */
#users-search { max-width: 26rem; flex: 1 1 16rem; }

/* ⚠️ THE DIRECTORY TABLE IS GONE, so the rule that made this card scroll
   sideways went with it. Left as a note rather than kept: a card GRID
   inside an overflow container clips the cards' own shadows at the edges,
   and there is nothing left here that can be wider than the panel. */

/* A table that genuinely is a table -- Sender Scores is a ranking -- gets
   its own scroll container so the page body never moves sideways. There
   are three per-panel versions of this already (.an-table-wrap,
   .subs-table-wrap, .mv-table-scroll); this is the general one, and they
   are worth folding into it when something else takes somebody there. */
.table-scroll { overflow-x: auto; }
.table-scroll > table { min-width: 34rem; }

/* ── Broadcast composer ─────────────────────────────────────────────── */

/* The four type buttons used to borrow .secondary for their base look,
   which meant the unselected state was an ordinary button and the row
   read as four actions rather than one choice. They are chips now, and
   .broadcast-type-btn.active (defined earlier in this file) still paints
   the selected one. */
.broadcast-type-row {
  display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0.35rem 0 1rem;
}
.broadcast-type-btn {
  background: var(--c-surface, #fff); border: 1px solid var(--c-border, #e2e8f0); color: var(--c-text-3, #475569);
  border-radius: 2rem; padding: 0.35rem 0.85rem;
  font-size: 0.82rem; font-weight: 500; cursor: pointer;
  transition: border-color 0.15s, color 0.15s;
}
.broadcast-type-btn:hover:not(.active) { border-color: var(--c-brand-border, #c7d2fe); color: var(--c-brand-strong, #4f46e5); }

.broadcast-preview-wrap { margin-top: 1.25rem; }
.broadcast-preview-icon { font-size: 1.25rem; line-height: 1.4; }
/* Was an inline style on the element. A preview is not a live
   notification -- nothing in it should be clickable. */
#broadcast-preview {
  pointer-events: none;
  background: var(--c-surface-subtle, #f8fafc); border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.6rem;
  padding: 0.85rem 1rem; margin-top: 0.35rem;
}

.broadcast-send-row { margin-top: 1.25rem; }
.broadcast-history-title {
  margin-top: 2rem; padding-top: 1.25rem; border-top: 1px solid var(--c-border, #e2e8f0);
}

/* ── Team management ────────────────────────────────────────────────── */

/* The owner used to be indistinguishable from any other admin -- same
   role text, and only a missing Remove button to tell them apart. */
.ws-owner-badge {
  background: var(--rail-accent, var(--c-brand-alt, #8b5cf6));
  font-size: 0.66rem;
  margin-left: 0.4rem;
  vertical-align: middle; color: var(--c-on-status, #0f172a);
}
.ws-you-tag { font-size: 0.72rem; margin-left: 0.35rem; }
.ws-role-select { width: auto; min-width: 7rem; padding: 0.25rem 0.5rem; font-size: 0.82rem; }

#ws-invite-link { margin-top: 0.5rem; font-family: monospace; font-size: 0.78rem; }
#ws-leave-box { margin-top: 2rem; padding-top: 1.25rem; border-top: 1px solid var(--c-border, #e2e8f0); }

/* ── Managed number wizard: plan step ───────────────────────────────── */

.mnw-plan-list {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 0.6rem; margin: 0.75rem 0 1rem;
}
.mnw-plan-card {
  display: flex; flex-direction: column; align-items: flex-start; gap: 0.15rem;
  background: var(--c-surface, #fff); border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.7rem;
  padding: 0.85rem; cursor: pointer; text-align: left; font-family: inherit;
  transition: border-color 0.15s, box-shadow 0.15s;
}
/* ⚠️ THE BACKGROUND HAS TO BE RESTATED HERE.
   .mnw-plan-card is a <button>, so button:hover:not(:disabled) applies
   `background: var(--btn-grad-hover)` -- the indigo gradient -- under
   text that is --c-text (near-black) and --c-text-muted. The card rule
   below out-specifies it for border-color only, so everything it does
   NOT name falls through, and the tile went dark-ink-on-indigo the
   moment the pointer touched it.
   .carrier-option opts out of the same treatment a few hundred lines up
   and says so in its own comment; this one never did. */
.mnw-plan-card:hover:not(:disabled) {
  border-color: var(--c-brand, #6366f1);
  background: var(--c-brand-tint, #eef2ff);
  box-shadow: none;
  transform: none;
}
.mnw-plan-card.selected { border-color: var(--c-brand, #6366f1); box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-brand, #6366f1) 15%, transparent); }
.mnw-plan-card.current { border-color: var(--c-success-alt, #10b981); }
/* Shown, not hidden: an admin needs to see the plan exists and why it
   cannot be picked, rather than wondering where the $5 tier went. */
.mnw-plan-card.unavailable { opacity: 0.55; cursor: not-allowed; }
.mnw-plan-card:disabled { background: var(--c-surface, #fff); }
.mnw-plan-name { font-weight: 700; font-size: 0.9rem; color: var(--c-text, #0f172a); }
.mnw-plan-tag {
  font-size: 0.62rem; background: var(--c-success-alt, #10b981); color: var(--c-on-fill, #ffffff);
  border-radius: 2rem; padding: 0.05rem 0.4rem; margin-left: 0.3rem; vertical-align: middle;
}
.mnw-plan-price { font-size: 1.35rem; font-weight: 800; color: var(--c-text, #0f172a); }
.mnw-plan-per { font-size: 0.75rem; font-weight: 500; color: var(--c-text-dim, #94a3b8); }
.mnw-plan-meta { font-size: 0.74rem; color: var(--c-text-muted, #64748b); }
.mnw-plan-note { font-size: 0.68rem; color: var(--c-warning-text, #92400e); margin-top: 0.3rem; }

.mnw-bill {
  background: var(--c-surface-subtle, #f8fafc); border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.7rem;
  padding: 0.9rem 1rem; margin: 0.75rem 0;
}
.mnw-bill-row {
  display: flex; justify-content: space-between; gap: 1rem;
  font-size: 0.85rem; color: var(--c-text-3, #475569); padding: 0.18rem 0;
}
.mnw-bill-credit { color: var(--c-success-text, #166534); }
.mnw-bill-total {
  border-top: 1px solid var(--c-border, #e2e8f0); margin-top: 0.4rem; padding-top: 0.5rem;
  font-size: 1rem; font-weight: 700; color: var(--c-text, #0f172a);
}
.mnw-bill-sub { font-size: 0.78rem; color: var(--c-text-dim, #94a3b8); }
.mnw-card-container {
  border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.6rem;
  padding: 0.7rem; background: var(--c-surface, #fff); margin-top: 0.35rem; min-height: 3.2rem;
}

/* ── Access levels ──────────────────────────────────────────────────── */

.ws-roles-section { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid var(--c-border, #e2e8f0); }
.ws-roles-list { display: flex; flex-direction: column; gap: 0.75rem; margin-top: 0.75rem; }
.ws-role-card { background: var(--c-surface-subtle, #f8fafc); border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.7rem; padding: 0.9rem 1rem; }
.ws-role-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem; flex-wrap: wrap; }
.ws-role-name { font-weight: 700; color: var(--c-text, #0f172a); }
.ws-role-tag {
  font-size: 0.62rem; background: var(--c-text-muted, #64748b); color: var(--c-on-fill, #ffffff);
  border-radius: 2rem; padding: 0.05rem 0.4rem; margin-left: 0.35rem; vertical-align: middle;
}
.ws-role-desc { font-size: 0.78rem; color: var(--c-text-muted, #64748b); margin-top: 0.1rem; }
.ws-role-actions { display: flex; align-items: center; gap: 0.6rem; font-size: 0.78rem; }

.ws-cap-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 0.4rem; margin: 0.75rem 0;
}
.ws-cap {
  display: flex; align-items: flex-start; gap: 0.5rem;
  background: var(--c-surface, #fff); border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.5rem;
  padding: 0.5rem 0.6rem; font-size: 0.82rem; cursor: pointer;
}
.ws-cap input { margin-top: 0.15rem; flex-shrink: 0; }
/* Built-in levels, and any capability the editor does not hold
   themselves -- disabled rather than rejected on save. */
.ws-cap-locked { opacity: 0.6; cursor: not-allowed; }
.ws-cap-desc { display: block; font-size: 0.72rem; color: var(--c-text-muted, #64748b); margin-top: 0.1rem; }
.ws-role-save { margin-top: 0.25rem; }

/* ── Accept invitation page ─────────────────────────────────────────── */

.invite-body { background: var(--c-page, #f4f5f7); min-height: 100vh; }
.invite-shell { max-width: 30rem; margin: 0 auto; padding: 2.5rem 1.25rem 4rem; }
.invite-brand { text-align: center; margin-bottom: 1.25rem; }
.invite-logo {
  background: var(--c-brand, #6366f1); color: var(--c-on-fill, #ffffff); padding: 0.3rem 0.8rem;
  border-radius: 0.4rem; font-size: 1rem; font-weight: 800; letter-spacing: -0.02em;
}
.invite-card {
  background: var(--c-surface, #fff); border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.9rem;
  padding: 1.75rem; box-shadow: 0 1px 3px color-mix(in srgb, var(--c-scrim, #0f172a) 6%, transparent);
}
.invite-from {
  display: flex; align-items: center; gap: 0.7rem;
  padding-bottom: 1rem; margin-bottom: 1rem; border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9);
}
.invite-avatar {
  width: 2.5rem; height: 2.5rem; border-radius: 50%; flex-shrink: 0;
  background: var(--rail-accent, var(--c-brand-alt, #8b5cf6)); color: var(--c-on-fill, #ffffff);
  display: flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 1.05rem;
}
.invite-from-name { font-size: 0.9rem; color: var(--c-text, #0f172a); }
.invite-from-sub { font-size: 0.78rem; color: var(--c-text-dim, #94a3b8); }
.invite-title { font-size: 1.4rem; font-weight: 800; margin: 0 0 0.35rem; color: var(--c-text, #0f172a); }
.invite-org { margin-top: -0.2rem; }
.invite-check {
  display: flex; align-items: flex-start; gap: 0.5rem;
  font-size: 0.82rem; color: var(--c-text-3, #475569); margin: 0.9rem 0;
}
.invite-check input { margin-top: 0.2rem; flex-shrink: 0; }
#invite-submit { width: 100%; margin-top: 0.25rem; }
.invite-alt { font-size: 0.82rem; text-align: center; margin-top: 1.25rem; }
.invite-secondary-link { display: inline-block; margin-top: 0.75rem; font-size: 0.85rem; }

/* ── Plan & usage meters ────────────────────────────────────────────── */

.plan-usage-track { position: relative; overflow: hidden; }
.plan-usage-fill { transition: width 0.35s ease, background 0.2s ease; }

/* At the cap: the label goes red too, so the row reads as maxed without
   having to compare two numbers. */
.account-usage-maxed .account-usage-text { color: var(--c-danger-deep, #b91c1c); font-weight: 600; }
.account-usage-maxed .account-usage-label { color: var(--c-danger-deep, #b91c1c); }

/* Limit of zero -- not included on this plan, which is a different
   thing from being at the limit. Muted rather than alarming: there is
   nothing wrong, it is just not part of what they bought. */
.account-usage-none .account-usage-text { color: var(--c-text-dim, #94a3b8); font-style: italic; }
.account-usage-none .plan-usage-track { background: repeating-linear-gradient(
  45deg, var(--c-surface-sunken, #f1f5f9), var(--c-surface-sunken, #f1f5f9) 4px, var(--c-surface-sunken, #f1f5f9) 4px, var(--c-surface-sunken, #f1f5f9) 8px); }

.account-plan-compare { font-size: 0.8rem; margin-left: 0.6rem; }

/* ── /subscriptions ─────────────────────────────────────────────────── */

.subs-body { background: var(--c-page, #f4f5f7); }
.subs-topbar {
  background: var(--c-inverse-surface, #0f172a); padding: 0.875rem 1.5rem; display: flex;
  align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap;
}
.subs-brand { color: var(--c-text-dim, #94a3b8); text-decoration: none; font-size: 0.85rem; display: flex; align-items: center; gap: 0.55rem; }
.subs-logo { background: var(--c-brand, #6366f1); color: var(--c-on-fill, #ffffff); padding: 0.2rem 0.6rem; border-radius: 0.35rem; font-weight: 800; font-size: 0.8rem; }
.subs-back { color: var(--c-on-dark-brand, #a5b4fc); text-decoration: none; font-size: 0.8rem; font-weight: 600; }
.subs-shell { max-width: 1180px; margin: 0 auto; padding: 2.5rem 1.25rem 4rem; }
.subs-title { font-size: 1.8rem; font-weight: 800; margin: 0 0 0.4rem; color: var(--c-text, #0f172a); }
.subs-lede { color: var(--c-text-3, #475569); max-width: 44rem; margin: 0 0 2rem; }

/* Wide screens get the table; narrow get the cards. Same data. */
.subs-table-wrap { overflow-x: auto; background: var(--c-surface, #fff); border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.8rem; }
.subs-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; min-width: 60rem; }
.subs-table th, .subs-table td { padding: 0.7rem 0.9rem; text-align: left; border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9); }
.subs-table thead th { background: var(--c-surface-subtle, #f8fafc); border-bottom: 2px solid var(--c-border, #e2e8f0); vertical-align: bottom; }
.subs-th-name { display: block; font-weight: 700; color: var(--c-text, #0f172a); }
.subs-th-price { display: block; font-size: 0.78rem; color: var(--c-text-muted, #64748b); font-weight: 500; }
.subs-row-label { width: 14rem; color: var(--c-text-3, #475569); font-weight: 600; background: var(--c-surface-subtle, #f8fafc); }
.subs-muted { color: var(--c-text-dim, #94a3b8); }
.subs-cap {
  display: inline-block; font-size: 0.72rem; background: var(--c-surface-sunken, #f1f5f9); color: var(--c-text-3, #475569);
  border-radius: 0.35rem; padding: 0.1rem 0.4rem; margin: 0.1rem 0.15rem 0.1rem 0;
}
.subs-notes { margin-top: 2.5rem; }
.subs-notes h2 { font-size: 1.05rem; margin-bottom: 0.6rem; }
.subs-notes dt { font-weight: 700; color: var(--c-text, #0f172a); margin-top: 0.75rem; font-size: 0.88rem; }
.subs-notes dd { margin: 0.15rem 0 0; color: var(--c-text-3, #475569); font-size: 0.85rem; }

@media (max-width: 900px) {
  .subs-table-wrap { display: none; }
  .subs-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 0.9rem; }
  .subs-card { background: var(--c-surface, #fff); border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.8rem; padding: 1.1rem; }
  .subs-card-featured { border-color: var(--c-brand, #6366f1); box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-brand, #6366f1) 12%, transparent); }
  .subs-card-name { font-weight: 800; color: var(--c-text, #0f172a); }
  .subs-card-price { font-size: 1.6rem; font-weight: 800; color: var(--c-text, #0f172a); }
  .subs-card-price span { font-size: 0.8rem; font-weight: 500; color: var(--c-text-dim, #94a3b8); }
  .subs-card-summary { font-size: 0.82rem; color: var(--c-text-3, #475569); margin: 0.2rem 0 0.75rem; }
  .subs-card-rows > div { display: flex; justify-content: space-between; gap: 1rem; font-size: 0.82rem; padding: 0.15rem 0; }
  .subs-card-rows dt { color: var(--c-text-muted, #64748b); }
  .subs-card-rows dd { margin: 0; color: var(--c-text, #0f172a); font-weight: 600; }
  .subs-card-caps { margin-top: 0.6rem; }
}

/* Why the plan picker opened. Without it the modal reads as an upsell
   that appeared on its own. */
.checkout-reason {
  background: var(--c-warning-tint, #fffbeb); border: 1px solid var(--c-warning-border, #fde68a); color: var(--c-warning-text, #92400e);
  border-radius: 0.5rem; padding: 0.6rem 0.85rem;
  font-size: 0.85rem; margin: 0 0 1rem;
}

/* ── /subscriptions: landing tiles, current-plan state ──────────────── */

.subs-grid { margin-bottom: 2rem; }
.subs-tile-current { border-color: var(--c-success-alt, #10b981); box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-success-alt, #10b981) 12%, transparent); }
.subs-current-badge {
  position: absolute; top: -0.7rem; right: 1rem;
  background: var(--c-success-alt, #10b981); color: var(--c-on-fill, #ffffff); font-size: 0.68rem; font-weight: 700;
  border-radius: 2rem; padding: 0.15rem 0.6rem;
}
.subs-cta-disabled { opacity: 0.6; cursor: default; text-align: center; }
.subs-current {
  background: var(--c-success-tint, #f0fdf4); border: 1px solid var(--c-success-border, #bbf7d0); color: var(--c-success-text, #166534);
  border-radius: 0.5rem; padding: 0.6rem 0.9rem; font-size: 0.85rem;
  display: inline-block; margin: 0 0 1.25rem;
}
.subs-details { margin: 0 0 2rem; }
.subs-details summary {
  cursor: pointer; font-weight: 600; color: var(--c-brand, #6366f1); font-size: 0.9rem;
  padding: 0.6rem 0;
}
.subs-details[open] summary { margin-bottom: 0.75rem; }

/* Seven tiles will not sit in the landing page's three columns. */
@media (min-width: 901px) {
  .subs-grid { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
}
@media (max-width: 900px) {
  .subs-grid { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
}

/* ── AI ASSISTANT ────────────────────────────────────────────────────
   Two levels, one visual language: the floating personal assistant
   (bottom-left, always available) and the per-Linxi assistant embedded
   in its settings panel.

   The bubble sits bottom-LEFT deliberately. Bottom-right is where this
   app's toasts appear and where browsers put their own download and
   translate prompts; a permanently-parked button there covers whichever
   arrives next.

   --ai-accent is overridden inline per workspace from workspaces.ai_color
   (validated as #rrggbb server-side before it is ever stored). It is a
   custom property rather than a hardcoded colour so one attribute
   restyles the whole panel. */

/* ⚠️ THE ASSISTANT PANEL FOLLOWS THE THEME. It was built on the
   permanently-dark family -- --c-inverse-surface, --c-chrome-text -- which
   is what the header, the E2E Secure module and the landing hero use to
   stay dark in BOTH modes on purpose. The panel is not one of those three,
   and nothing said it should be: on a light install it opened as a dark
   box over a light page. It uses the ordinary surface and text tokens now,
   so it is light in light and dark in dark.

   ⚠️ Its borders were --c-text-2, a TEXT token doing a border's job. That
   is the family this stylesheet already had to correct once; --c-border is
   the one that exists for it.

   ⚠️ The ACCENT is untouched. --ai-accent is the brand, overridden inline
   per workspace from workspaces.ai_color, and the user's own bubble stays
   a brand fill with --c-on-accent on it -- text on a fill that stays
   coloured, which is exactly what that token is for. */
#ai-bubble-container {
  --ai-accent: var(--c-brand, #6366f1);
  --ai-accent-2: var(--c-brand-alt, #8b5cf6);
}

/* ⚠️ BOTTOM-RIGHT since the support FAB was removed. It is the corner
   people reach for, and it is now uncontested -- nothing else floats
   there. The panel anchors to the same edge, so expanding grows it
   LEFTWARDS into the page rather than off the screen. */
.ai-bubble-btn {
  position: fixed;
  bottom: 1.5rem;
  right: 1.5rem;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--ai-accent), var(--ai-accent-2));
  border: none;
  cursor: pointer;
  z-index: 9999;
  box-shadow: 0 4px 20px color-mix(in srgb, var(--c-brand, #6366f1) 40%, transparent);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.4rem;
  color: var(--c-on-accent, #ffffff);
  transition: transform 0.18s ease;
}

.ai-bubble-btn:hover { transform: scale(1.06); }
.ai-bubble-btn.open { transform: scale(0.9); }

.ai-chat-panel {
  position: fixed;
  bottom: 5rem;
  right: 1.5rem;
  width: 340px;
  height: 480px;
  max-height: calc(100vh - 7rem);
  background: var(--c-surface, #ffffff);
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 1rem;
  z-index: 9998;
  display: none;
  flex-direction: column;
  overflow: hidden;
  box-shadow: 0 20px 60px color-mix(in srgb, var(--c-shadow, #000000) 50%, transparent);
}

/* .hidden is `display: none !important` app-wide, so the open state has
   to be a class of its own rather than an inline display -- an inline
   style would lose to it and the panel would never appear. */
.ai-chat-panel.ai-open { display: flex; }

/* Below this the fixed 340px panel would hang off the side of the
   screen. Full-width with a margin instead of a shrunken copy of the
   desktop layout. */
@media (max-width: 480px) {
  .ai-chat-panel {
    left: 0.75rem;
    right: 0.75rem;
    width: auto;
    bottom: 4.75rem;
  }
}

.ai-panel-header {
  padding: 0.875rem 1rem;
  background: var(--c-surface-subtle, #f8fafc);
  border-bottom: 1px solid var(--c-border, #e2e8f0);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

.ai-panel-id { display: flex; align-items: center; gap: 0.5rem; min-width: 0; }

.ai-avatar {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--ai-accent), var(--ai-accent-2));
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.875rem;
  flex-shrink: 0;
}

.ai-panel-name {
  font-size: 0.825rem;
  font-weight: 700;
  color: var(--c-text, #0f172a);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ai-panel-sub { font-size: 0.65rem; color: var(--c-text-muted, #64748b); }

.ai-panel-actions { display: flex; gap: 0.35rem; flex-shrink: 0; }
.ai-panel-actions button {
  background: none;
  border: none;
  color: var(--c-text-muted, #64748b);
  cursor: pointer;
  font-size: 0.9rem;
  padding: 0.25rem;
  line-height: 1;
}
.ai-panel-actions button:hover { color: var(--c-text, #0f172a); }

.ai-messages {
  flex: 1;
  overflow-y: auto;
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.ai-msg-user,
.ai-msg-assistant {
  padding: 0.5rem 0.75rem;
  font-size: 0.82rem;
  line-height: 1.45;
  /* overflow-wrap, not word-wrap: a pasted API key or a long URL with no
     spaces in it would otherwise push the panel wider than the screen. */
  overflow-wrap: anywhere;
}

.ai-msg-user {
  align-self: flex-end;
  background: var(--ai-accent);
  color: var(--c-on-accent, #ffffff);
  border-radius: 1rem 1rem 0.25rem 1rem;
  max-width: 85%;
}

.ai-msg-assistant {
  align-self: flex-start;
  background: var(--c-surface-subtle, #f8fafc);
  color: var(--c-text, #0f172a);
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 1rem 1rem 1rem 0.25rem;
  max-width: 90%;
}

.ai-msg-assistant p { margin: 0 0 0.5rem; }
.ai-msg-assistant p:last-child { margin: 0; }
.ai-msg-assistant ul { margin: 0.35rem 0; padding-left: 1.25rem; }
.ai-msg-assistant li { margin: 0.15rem 0; }
.ai-msg-assistant code {
  background: var(--c-surface-subtle, #f8fafc);
  padding: 0.1rem 0.3rem;
  border-radius: 0.2rem;
  font-size: 0.78rem;
}

.ai-typing { padding: 0 1rem 0.5rem; display: none; }
.ai-typing.ai-on { display: block; }
.ai-typing-dots { display: inline-flex; gap: 0.25rem; align-items: center; }

.ai-typing-dot {
  width: 6px;
  height: 6px;
  background: var(--ai-accent);
  border-radius: 50%;
  animation: aiTypingBounce 0.8s infinite;
}
.ai-typing-dot:nth-child(2) { animation-delay: 0.15s; }
.ai-typing-dot:nth-child(3) { animation-delay: 0.3s; }

@keyframes aiTypingBounce {
  0%, 100% { transform: translateY(0); opacity: 0.4; }
  50% { transform: translateY(-4px); opacity: 1; }
}

/* Respect a reduced-motion preference -- this animation runs
   continuously for as long as a reply is pending. */
@media (prefers-reduced-motion: reduce) {
  .ai-typing-dot { animation: none; opacity: 0.7; }
  .ai-bubble-btn { transition: none; }
}

.ai-input-row {
  padding: 0.75rem;
  border-top: 1px solid var(--c-border, #e2e8f0);
  display: flex;
  gap: 0.5rem;
  align-items: flex-end;
}

.ai-input {
  flex: 1;
  background: var(--c-surface, #ffffff);
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
  padding: 0.5rem 0.75rem;
  color: var(--c-text, #0f172a);
  font-size: 0.82rem;
  font-family: inherit;
  line-height: 1.4;
  resize: none;
  max-height: 80px;
  outline: none;
}
.ai-input:focus { border-color: var(--ai-accent); }

.ai-send-btn {
  background: var(--ai-accent);
  border: none;
  color: var(--c-on-accent, #ffffff);
  width: 34px;
  height: 34px;
  border-radius: 0.5rem;
  cursor: pointer;
  font-size: 1rem;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.ai-send-btn:disabled { opacity: 0.55; cursor: default; }

/* ── AI settings panels (account + workspace) ─────────────────────── */

.ai-key-choice { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 0.75rem; }

.ai-key-option {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  cursor: pointer;
  font-size: 0.82rem;
}
.ai-key-option input { margin-top: 0.2rem; flex-shrink: 0; }
.ai-key-option-title { font-weight: 600; display: block; }
.ai-key-option-desc { font-size: 0.72rem; color: var(--c-text-muted, #64748b); }
.ai-key-option.ai-key-disabled { opacity: 0.55; cursor: not-allowed; }

.ai-key-row { display: flex; gap: 0.5rem; }
.ai-key-row .mnw-input { flex: 1; }

.ai-stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem; }

.ai-stat {
  background: var(--c-surface-subtle, #f8fafc);
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
  padding: 0.75rem;
  text-align: center;
}
.ai-stat-value { font-size: 1.2rem; font-weight: 800; color: var(--c-text, #0f172a); }
.ai-stat-label { font-size: 0.7rem; color: var(--c-text-dim, #94a3b8); }

.ai-temp-row { display: flex; align-items: center; gap: 0.75rem; }
.ai-temp-row input[type="range"] { flex: 1; }
.ai-temp-edge { font-size: 0.75rem; color: var(--c-text-muted, #64748b); white-space: nowrap; }
.ai-temp-label { text-align: center; font-size: 0.72rem; color: var(--c-text-dim, #94a3b8); margin-top: 0.25rem; }

.ai-color-row { display: flex; gap: 0.5rem; align-items: center; }
.ai-color-row input[type="color"] {
  width: 40px;
  height: 36px;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.35rem;
  cursor: pointer;
  padding: 2px;
  flex-shrink: 0;
}

.ai-test-box {
  background: var(--c-success-tint, #f0fdf4);
  border: 1px solid var(--c-success-border, #bbf7d0);
  border-radius: 0.75rem;
  padding: 1rem;
  margin-bottom: 1rem;
}
.ai-test-row { display: flex; gap: 0.5rem; }
.ai-test-row .mnw-input { flex: 1; }
.ai-test-output {
  font-size: 0.8rem;
  margin-top: 0.5rem;
  background: var(--c-surface, #fff);
  border: 1px solid var(--c-success-tint-2, #dcfce7);
  border-radius: 0.35rem;
  padding: 0.5rem;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-height: 220px;
  overflow-y: auto;
}

.ai-subsection {
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.75rem;
  padding: 1rem;
  margin-bottom: 1rem;
}
.ai-subsection-title { font-weight: 600; font-size: 0.875rem; margin-bottom: 0.75rem; }

/* ── Add Funds modal ────────────────────────────────────────────────── */

.add-funds-modal { max-width: 440px; }

.topup-presets {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.5rem;
  margin-bottom: 0.6rem;
}

.topup-preset {
  padding: 0.55rem;
  border: 2px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
  background: var(--c-surface, #fff);
  color: var(--c-text, #0f172a);
  font-weight: 600;
  font-size: 0.875rem;
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s;
}
.topup-preset:hover { border-color: var(--c-text-dim, #94a3b8); }
.topup-preset.active { border-color: var(--c-brand, #6366f1); background: var(--c-brand-tint, #f5f3ff); color: var(--c-brand-deep, #4338ca); }

.topup-custom { display: flex; align-items: center; gap: 0.5rem; }
.topup-currency { color: var(--c-text-muted, #64748b); font-size: 0.9rem; }
.topup-custom .mnw-input { flex: 1; }

.topup-secure { font-size: 0.72rem; text-align: center; }

/* ── Header balance ─────────────────────────────────────────────────
   Plain white text on the dark header, no pill. It reads as another
   piece of header information beside the username, which is what it is
   -- a pill made it look like a button that had wandered onto the end
   of the tab strip. */

.header-balance-chip {
  background: none;
  border: none;
  padding: 0.2rem 0.4rem;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--c-head-ink, #ffffff);   /* follows the header, see .bell-btn */
  cursor: pointer;
  white-space: nowrap;
  flex-shrink: 0;
  transition: opacity 0.15s;
}
.header-balance-chip { border-radius: 6px; }

/* Low and empty keep a colour, because they are the whole reason to
   glance at it. Amber and red still read on the dark header, and losing
   the distinction entirely would mean an empty balance -- which pauses
   sending AND the assistant -- looked identical to a healthy one. */
/* --c-head-warning / --c-head-danger, not --c-on-dark-*: those are pale
   tints designed for a dark ground and are near-invisible on the light
   header. The head family carries a readable value for each mode. */
.header-balance-chip.low { color: var(--c-head-warning, #fcd34d); }
.header-balance-chip.empty { color: var(--c-head-danger, #fca5a5); }

/* ── Low balance notices (AI panel + bubble) ────────────────────────── */

.ai-low-balance {
  background: var(--c-warning-tint, #fffbeb);
  border: 1px solid var(--c-warning-border, #fde68a);
  border-radius: 0.5rem;
  padding: 0.75rem;
  margin-top: 0.75rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
  font-size: 0.82rem;
  color: var(--c-warning-text, #92400e);
}

.ai-bubble-low-balance {
  padding: 0.4rem 0.75rem;
  background: var(--c-warning-tint, #fffbeb);
  border-top: 1px solid var(--c-warning-border, #fde68a);
  font-size: 0.72rem;
  color: var(--c-warning-text, #92400e);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}
/* .hidden is display:none !important app-wide, so it beats the flex
   above rather than fighting it -- these two must not be shown with an
   inline style for the same reason. */

.ai-add-funds-btn {
  background: var(--c-warning, #f59e0b);
  color: var(--c-on-fill, #ffffff);
  border: none;
  padding: 0.25rem 0.6rem;
  border-radius: 0.35rem;
  font-size: 0.72rem;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
  flex-shrink: 0;
}
.ai-add-funds-btn:hover { background: var(--c-warning-strong, #d97706); }

/* ── Account details: two rows ──────────────────────────────────────── */

.account-field-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.75rem 1rem;
  margin-bottom: 0.9rem;
}
/* Email + phone sit two-across rather than leaving a dead third column. */
.account-field-grid-2 { grid-template-columns: repeat(2, 1fr); }

.account-field { display: flex; flex-direction: column; min-width: 0; }
.account-field label { margin-bottom: 0.25rem; }
/* min-width:0 above and this together are what stop a long value forcing
   the grid wider than its container -- a grid item's default min-width
   is auto, not 0, so an input inside one does not shrink on its own. */
.account-field input { width: 100%; }

.account-field-hint {
  font-size: 0.72rem;
  color: var(--c-text-dim, #94a3b8);
  margin-top: 0.25rem;
  line-height: 1.35;
}

@media (max-width: 720px) {
  .account-field-grid,
  .account-field-grid-2 { grid-template-columns: 1fr; }
}

/* ── FEED ────────────────────────────────────────────────────────────
   Lifted out of public/feed.html's inline <style> so ONE stylesheet
   serves both the standalone /feed page (for logged-out visitors) and
   the in-app Feed tab. They were the same rules; a copy in each would
   have drifted the first time either was touched. */
* { box-sizing: border-box; }
  body { background: var(--c-surface-subtle, #f8fafc); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 0; }

  /* The one public topbar: 27 pages wear it. The SOCIAL ones
     (network / feed / profile / calendar, the event page, a post page)
     render their markup from `src/services/socialChrome.js`; the /info
     guides still carry their own, which is why these rules are shared
     rather than scoped.

     ⚠️ This comment used to read *"each page keeps its own copy rather
     than a shared component, same convention this whole site already
     follows"*. That convention is what let the Calendar link ship on one
     page out of six. */
  /* ⚠️⚠️ THE BAR WEARS THE ADMIN'S OWN CHROME, not a fixed slate.
     It was `--c-inverse-surface` -- one navy for every deployment -- while
     `--c-chrome` / `-2` / `-3` is the family an admin actually SETS in
     Theme & Colours for exactly this: dark furniture. Measured on
     sensorium, whose admin had set all three to greens (#131f20, #1a3d3c,
     #13292a) and whose public header was still Linxi's navy. Same gradient
     the legal header and hero already use, so the dark furniture on a
     deployment matches itself.

     ⚠️ The family holds the SAME value in light and dark by design -- the
     bar is dark in both modes, so the ink on it is fixed too. That is why
     nothing here reads `--c-surface` or `--c-text`. */
  /* ⚠️ 1.25rem of side padding, the SITE header's number -- the social
     templates match it too, so a reader crossing between the two never
     sees the brand shift sideways. */
  /* ⚠⚠ 0.5rem AND A 40px MARK ARE THE BAR'S HEIGHT, and they are the
     site header's numbers -- siteChromeDefaults' HEADER_LOGO / HEADER_PAD,
     which this rule is the hand-written twin of. The mark is the tallest
     thing on the row, so the bar is 40 + 2x9.6 = 59px and nothing else
     decides it. It was briefly 48 + 2x13.6 = 75px, which is taller than
     this bar has ever been and read as a banner. */
  .info-topbar { background: linear-gradient(135deg, var(--c-chrome, #17121f) 0%, var(--c-chrome-2, #241a3d) 55%, var(--c-chrome-3, #1a1329) 100%); padding: 0.6rem 1.25rem; display: flex; align-items: center; justify-content: space-between; position: sticky; top: 0; z-index: 100; }

  /* ⚠️⚠️ THE SITE'S OWN NAME, IN THE SITE'S OWN INK.
     This was a fixed three-stop gradient -- `--c-brand` then `--c-purple`
     then `--c-pink` -- so a white-label's name was painted in two of
     LINXI's brand colours whatever its own palette said. On sensorium
     (brand green) it rendered green-to-purple-to-pink. Two of those three
     stops are not a token any admin sets.

     Plain `--c-on-accent` instead: 16.9:1 on sensorium's chrome, and the
     colour beside it is the LOGO, which is theirs. A brand word is an
     identity, not a decoration.

     The mark beside the word came out of social.css's `.social-body` scope
     at the same time: the bar is a component and is worn by pages that are
     not social bodies (the event page, every /info guide). */
  /* ⚠️ 48px AND 1.3rem: the SITE header's numbers, which this bar now
     shares -- see the note beside .soc-brand in siteChromeDefaults, which
     is this rule written out for the templates. The 50% stays; roundness
     is not a size, and it is most of why this reads as a social bar. */
  .info-topbar-logo { font-size: 1.3rem; font-weight: 900; color: var(--c-on-accent, #ffffff); text-decoration: none; display: inline-flex; align-items: center; gap: 0.6rem; }
  .info-topbar .header-logo { width: 40px; height: 40px; border-radius: 50%; }
  .info-topbar-links { display: flex; gap: 1.5rem; align-items: center; }
  /* ⚠️ `--c-chrome-dim`, never `--c-text-dim`. They hold the same value in
     LIGHT and the text one DARKENS in dark mode (#7d8da3) -- on a bar that
     stays dark in both, that is the one place it must not follow. Two
     tokens one value apart, and only one of them is about furniture. */
  .info-topbar-links a { color: var(--c-chrome-dim, #94a3b8); text-decoration: none; font-size: 0.875rem; transition: color 0.15s; }
  .info-topbar-links a:hover { color: var(--c-on-accent, #ffffff); }
  .info-topbar-cta { background: var(--c-brand, #6366f1); color: var(--c-on-accent, #ffffff) !important; padding: 0.4rem 1rem; border-radius: 0.5rem; font-weight: 600; }
  .info-topbar-cta:hover { background: var(--c-brand-strong, #4f46e5); color: var(--c-on-accent, #ffffff) !important; }

  .profile-container { max-width: 680px; margin: 0 auto; padding: 1.5rem 1rem 3rem; }

  .feed-toolbar { display: flex; align-items: center; justify-content: space-between; background: var(--c-surface, #fff); border-radius: 1rem; padding: 1rem 1.25rem; box-shadow: 0 1px 3px color-mix(in srgb, var(--c-shadow, #000000) 8%, transparent); }
  .feed-toolbar h1 { font-size: 1.1rem; margin: 0; color: var(--c-text, #0f172a); }
  .feed-sort-group { display: flex; gap: 0.4rem; }
  .feed-sort-btn { background: var(--c-surface-sunken, #f1f5f9); color: var(--c-text-3, #475569); border: 1px solid transparent; padding: 0.35rem 0.75rem; border-radius: 2rem; font-size: 0.78rem; font-weight: 600; cursor: pointer; }
  .feed-sort-btn.active { background: var(--c-brand-tint-2, #eef2ff); color: var(--c-brand, #6366f1); border-color: var(--c-brand-border, #c7d2fe); }

  .post-card { background: var(--c-surface, #fff); border-radius: 1rem; padding: 1.25rem; margin-top: 0.75rem; box-shadow: 0 1px 3px color-mix(in srgb, var(--c-shadow, #000000) 6%, transparent); }
  .post-header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem; }
  .post-avatar { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; background: var(--c-border, #e2e8f0); flex-shrink: 0; }
  .post-meta { flex: 1; min-width: 0; }
  .post-author { font-weight: 700; font-size: 0.9rem; color: var(--c-text, #0f172a); text-decoration: none; }
  .post-time { font-size: 0.75rem; color: var(--c-text-dim, #94a3b8); }
  .post-body { font-size: 0.9rem; color: var(--c-text, #0f172a); line-height: 1.7; margin-bottom: 0.75rem; white-space: pre-wrap; word-break: break-word; }
  .post-body a { color: var(--c-brand, #6366f1); text-decoration: none; }
  .post-image { width: 100%; max-height: 400px; object-fit: cover; border-radius: 0.75rem; margin-bottom: 0.75rem; cursor: pointer; }
  /* ⚠️ A ROW WITH A SQUARE THUMBNAIL, not a full-width letterbox strip.
     It was `width: 100%; height: 160px` -- so on a ~600px feed column the
     preview image was cropped to nearly 4:1 and read as squished, which
     is exactly what it was. A square crops least: it is the shape most
     og:image artwork survives, and it makes the card a compact tile
     rather than a banner. */
  .post-link-preview { border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.75rem; overflow: hidden; margin-bottom: 0.75rem; text-decoration: none; display: flex; align-items: stretch; }
  .post-link-image { width: 132px; height: 132px; aspect-ratio: 1 / 1; object-fit: cover; flex-shrink: 0; display: block; }
  /* ⚠️ min-width: 0, or a long title refuses to shrink and pushes the
     card wider than its column -- a flex item will not go below its
     content width without it. Same rule the composer's two rows carry. */
  .post-link-info { padding: 0.75rem; min-width: 0; display: flex; flex-direction: column; justify-content: center; }
  .post-link-title { font-weight: 700; font-size: 0.875rem; color: var(--c-text, #0f172a); margin-bottom: 0.25rem; overflow-wrap: anywhere; }
  /* Two lines, so a long og:description cannot make the row taller than
     the square beside it and reintroduce the shape this replaced. */
  .post-link-desc { font-size: 0.78rem; color: var(--c-text-muted, #64748b); line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
  .post-link-domain { font-size: 0.72rem; color: var(--c-text-dim, #94a3b8); margin-top: 0.35rem; overflow-wrap: anywhere; }

  /* ⚠️ SMALLER, NOT STACKED. Stacking would put a full-width image back
     on top of the text -- the letterbox this change removes -- on the
     screen where the column is narrowest and it looked worst. */
  @media (max-width: 480px) {
    .post-link-image { width: 96px; height: 96px; }
    .post-link-info { padding: 0.6rem 0.7rem; }
    .post-link-desc { -webkit-line-clamp: 2; }
  }

  /* ⚠️ THE VIDEO FACADE. A thumbnail and a play button, and nothing is
     loaded from YouTube until somebody clicks -- so a feed carrying ten
     videos contacts Google zero times. feed.js swaps in the iframe on the
     click; .post-video-playing is what tells the two apart. */
  .post-video { position: relative; border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.75rem; overflow: hidden; margin-bottom: 0.75rem; cursor: pointer; background: var(--c-inverse-surface, #0f172a); }
  .post-video-thumb { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; display: block; }
  /* The play button is decoration over a click target that is the whole
     card, so it is not focusable twice -- but it IS a real button, so a
     keyboard reaches the video at all. */
  .post-video-play { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 64px; height: 64px; border-radius: 50%; border: none; font-size: 1.5rem; line-height: 1; cursor: pointer; color: var(--c-on-accent, #ffffff); background: color-mix(in srgb, var(--c-scrim, #000000) 62%, transparent); }
  .post-video:hover .post-video-play { background: var(--c-danger, #dc2626); }
  .post-video-meta { padding: 0.7rem 0.75rem; background: var(--c-surface, #ffffff); }
  /* ⚠️ The meta strip is gone once the player is in, so the aspect ratio
     is the frame's own and nothing sits under a playing video. */
  .post-video-playing { cursor: default; aspect-ratio: 16 / 9; }
  .post-video-frame { width: 100%; height: 100%; border: 0; display: block; }

  .compose-link-head { display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 0.75rem; background: var(--c-surface-subtle, #f8fafc); font-size: 0.75rem; color: var(--c-text-muted, #64748b); }
  /* The chosen-images strip in the composer. A row that wraps, because
     twenty thumbnails on one line is a horizontal scrollbar nobody
     expects inside a text box. */
  /* ⚠️ STICKY, so it is reachable after scrolling -- a "3 new posts"
     bar that only exists at the very top is a bar nobody presses. */
  .feed-new-bar { position: sticky; top: 0.5rem; z-index: 5; display: block; width: 100%; margin-bottom: 0.75rem; padding: 0.55rem 0.9rem; border: none; border-radius: 999px; cursor: pointer; font-size: 0.82rem; font-weight: 700; color: var(--c-on-accent, #ffffff); background: var(--c-brand, #6366f1); box-shadow: 0 2px 10px color-mix(in srgb, var(--c-shadow, #000000) 18%, transparent); }
  .feed-new-bar.hidden { display: none; }

  .post-controls { display: inline-flex; gap: 0.1rem; align-items: flex-start; }
  .post-icon-btn { background: none; border: none; color: var(--c-text-dim, #94a3b8); cursor: pointer; font-size: 0.85rem; padding: 0.25rem 0.4rem; border-radius: 0.35rem; line-height: 1; }
  .post-icon-btn:hover { color: var(--c-brand, #6366f1); background: var(--c-brand-tint, #eef2ff); }
  /* Already pinned reads as ON, so the control shows its own state
     rather than looking identical to "pin this". */
  .post-icon-btn.is-pinned { color: var(--c-brand, #6366f1); background: var(--c-brand-tint, #eef2ff); }
  .post-pinned-flag { font-size: 0.72rem; font-weight: 700; color: var(--c-brand, #6366f1); margin-bottom: 0.4rem; }
  /* ⚠️ Muted and small. "edited" is a disclosure, not a badge -- it says
     the post changed, it is not a decoration on the post. */
  .post-edited { font-size: 0.72rem; color: var(--c-text-dim, #94a3b8); font-style: italic; }

  .post-edit-box { width: 100%; min-height: 90px; padding: 0.55rem 0.65rem; font-family: inherit; font-size: 0.9rem; line-height: 1.6; color: var(--c-text, #0f172a); background: var(--c-surface, #ffffff); border: 1px solid var(--c-border-strong, #cbd5e1); border-radius: 0.5rem; resize: vertical; }
  .post-edit-actions { display: flex; gap: 0.4rem; align-items: center; margin-top: 0.45rem; }

  .comment-reply { margin-left: 2.25rem; }
  /* ⚠️ ONE indent, because the server stores one level. A second would
     be describing a shape the data cannot have. */
  .comment-reply .comment-avatar { width: 22px; height: 22px; }
  .comment-reply-btn { background: none; border: none; padding: 0 0 0 0.5rem; cursor: pointer; font-size: 0.72rem; font-weight: 600; color: var(--c-text-dim, #94a3b8); }
  .comment-reply-btn:hover { color: var(--c-brand, #6366f1); }
  .comment-reply-flag { font-size: 0.72rem; color: var(--c-brand, #6366f1); margin: 0 0 0.3rem; }
  .comment-reply-flag.hidden { display: none; }
  .comment-avatar-initial { background: var(--c-border, #e2e8f0); display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 0.65rem; color: var(--c-text-muted, #64748b); }
  .comment-author-link { color: var(--c-text, #0f172a); text-decoration: none; }
  .comment-handle { color: var(--c-text-dim, #94a3b8); font-size: 0.72rem; font-weight: 400; }

  .compose-image-strip { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0.5rem 0; }
  .compose-image-strip.hidden { display: none; }
  .compose-image-cell { position: relative; width: 72px; height: 72px; border-radius: 0.5rem; overflow: hidden; border: 1px solid var(--c-border, #e2e8f0); }
  .compose-image-cell img { width: 100%; height: 100%; object-fit: cover; display: block; }
  /* ⚠️ The remove button sits ON the thumbnail with its own dark scrim:
     an ✕ in the app's grey over an arbitrary photo is invisible against
     roughly half of them. */
  .compose-image-drop { position: absolute; top: 2px; right: 2px; width: 20px; height: 20px; border: none; border-radius: 50%; cursor: pointer; font-size: 0.7rem; line-height: 1; color: var(--c-on-accent, #ffffff); background: color-mix(in srgb, var(--c-scrim, #000000) 62%, transparent); }
  .compose-image-drop:hover { background: var(--c-danger, #dc2626); }
  .compose-image-count { font-size: 0.72rem; margin: 0 0 0.3rem; }

  /* The shelf on a post. ⚠️ The COUNT picks the layout, not a width
     computed in JS -- one picture in a 2-up grid is a picture with a hole
     beside it, and four in a row are thumbnails nobody can see. */
  .post-gallery { display: grid; gap: 0.25rem; margin-bottom: 0.75rem; border-radius: 0.75rem; overflow: hidden; }
  .post-gallery-1 { grid-template-columns: 1fr; }
  .post-gallery-2 { grid-template-columns: 1fr 1fr; }
  .post-gallery-3 { grid-template-columns: 1fr 1fr; }
  .post-gallery-4 { grid-template-columns: 1fr 1fr; }
  /* Three is 1 big + 2 stacked, which is what every reader expects and
     what stops the odd one out being a half-width orphan. */
  .post-gallery-3 .post-gallery-cell:first-child { grid-row: span 2; }
  .post-gallery-cell { position: relative; padding: 0; border: none; background: var(--c-surface-subtle, #f8fafc); cursor: pointer; display: block; min-height: 0; }
  .post-gallery-1 .post-gallery-cell { aspect-ratio: 16 / 10; }
  .post-gallery-2 .post-gallery-cell,
  .post-gallery-3 .post-gallery-cell,
  .post-gallery-4 .post-gallery-cell { aspect-ratio: 1 / 1; }
  .post-gallery-3 .post-gallery-cell:first-child { aspect-ratio: auto; }
  .post-gallery-cell img { width: 100%; height: 100%; object-fit: cover; display: block; }
  /* "+3" on the fourth tile, so a fifth image is not silently dropped. */
  .post-gallery-more { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 1.4rem; font-weight: 800; color: var(--c-on-accent, #ffffff); background: color-mix(in srgb, var(--c-scrim, #000000) 52%, transparent); }

  .image-modal { position: fixed; inset: 0; z-index: 30000; background: color-mix(in srgb, var(--c-scrim, #000000) 90%, transparent); display: flex; align-items: center; justify-content: center; cursor: pointer; }
  .image-modal img { max-width: 90vw; max-height: 90vh; object-fit: contain; border-radius: 0.5rem; }
  .image-modal-nav { position: absolute; top: 50%; transform: translateY(-50%); width: 48px; height: 48px; border: none; border-radius: 50%; cursor: pointer; font-size: 1.8rem; line-height: 1; color: var(--c-on-accent, #ffffff); background: color-mix(in srgb, var(--c-scrim, #000000) 50%, transparent); }
  .image-modal-prev { left: 1rem; }
  .image-modal-next { right: 1rem; }
  .image-modal-count { position: absolute; bottom: 1.25rem; left: 50%; transform: translateX(-50%); font-size: 0.8rem; color: var(--c-chrome-text, #cbd5e1); }

  .compose-link-actions { display: inline-flex; gap: 0.15rem; align-items: center; }
  .compose-link-dismiss,
  .compose-link-refresh { background: none; border: none; cursor: pointer; color: var(--c-text-dim, #94a3b8); font-size: 0.85rem; padding: 0.15rem 0.3rem; border-radius: 0.3rem; line-height: 1; }
  .compose-link-dismiss:hover,
  .compose-link-refresh:hover { color: var(--c-text, #0f172a); background: var(--c-surface-subtle, #f8fafc); }

  /* The ✕ on a post. Was an inline style; it is a class now because an
     admin sees it on posts that are not theirs, where it wants a hover
     state that says "this is destructive" rather than looking like the
     same grey tick everywhere. */
  .post-delete-btn { background: none; border: none; color: var(--c-text-dim, #94a3b8); cursor: pointer; font-size: 0.8rem; padding: 0.25rem 0.4rem; border-radius: 0.35rem; line-height: 1; }
  .post-delete-btn:hover { color: var(--c-danger, #dc2626); background: var(--c-danger-tint, #fef2f2); }

  .post-repost-attribution { font-size: 0.78rem; color: var(--c-text-dim, #94a3b8); margin-bottom: 0.5rem; }
  .post-repost-original { border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.75rem; padding: 0.85rem; margin-bottom: 0.75rem; }

  .post-reactions { display: flex; align-items: center; gap: 0.35rem; flex-wrap: wrap; margin-bottom: 0.75rem; }
  .reaction-btn { display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.3rem 0.6rem; border-radius: 2rem; border: 1px solid var(--c-border, #e2e8f0); background: var(--c-surface, #fff); cursor: pointer; font-size: 0.8rem; }
  .reaction-btn.active { background: var(--c-brand-tint, #f5f3ff); border-color: var(--c-brand-border, #c7d2fe); }
  .reaction-btn .reaction-count { font-weight: 700; color: var(--c-brand, #6366f1); }
  .reaction-more-btn { padding: 0.3rem 0.6rem; border-radius: 2rem; border: 1px solid var(--c-border, #e2e8f0); background: var(--c-surface, #fff); cursor: pointer; font-size: 0.78rem; color: var(--c-text-dim, #94a3b8); }

  .post-actions { display: flex; gap: 0.5rem; padding-top: 0.75rem; border-top: 1px solid var(--c-surface-sunken, #f1f5f9); }
  .post-action-btn { display: flex; align-items: center; gap: 0.35rem; padding: 0.4rem 0.75rem; border-radius: 0.5rem; border: none; background: none; cursor: pointer; font-size: 0.8rem; color: var(--c-text-dim, #94a3b8); font-weight: 600; }

  .post-comments { margin-top: 0.75rem; border-top: 1px solid var(--c-surface-sunken, #f1f5f9); padding-top: 0.75rem; }
  .comment-item { display: flex; gap: 0.5rem; margin-bottom: 0.75rem; }
  .comment-avatar { width: 28px; height: 28px; border-radius: 50%; object-fit: cover; flex-shrink: 0; }
  .comment-bubble { background: var(--c-surface-subtle, #f8fafc); border-radius: 0.75rem; padding: 0.5rem 0.75rem; flex: 1; }
  .comment-author { font-weight: 700; font-size: 0.78rem; color: var(--c-text, #0f172a); }
  .comment-body { font-size: 0.82rem; color: var(--c-text-3, #475569); line-height: 1.5; margin-top: 0.15rem; }
  .comment-time { font-size: 0.7rem; color: var(--c-text-dim, #94a3b8); margin-top: 0.25rem; }
  .comment-input-wrap { display: flex; gap: 0.5rem; align-items: center; margin-top: 0.5rem; }
  .comment-input { flex: 1; border: 1px solid var(--c-border, #e2e8f0); border-radius: 2rem; padding: 0.5rem 0.85rem; font-size: 0.82rem; outline: none; }
  .comment-submit { background: var(--c-brand, #6366f1); color: var(--c-on-accent, #ffffff); border: none; padding: 0.5rem 0.85rem; border-radius: 2rem; font-size: 0.78rem; font-weight: 600; cursor: pointer; }

  .compose-card { background: var(--c-surface, #fff); border-radius: 1rem; padding: 1rem; margin-top: 0.75rem; box-shadow: 0 1px 3px color-mix(in srgb, var(--c-shadow, #000000) 6%, transparent); }
  .compose-input { width: 100%; border: none; outline: none; font-size: 0.95rem; resize: none; font-family: inherit; color: var(--c-text, #0f172a); min-height: 60px; background: transparent; }
  .compose-toolbar { display: flex; align-items: center; gap: 0.5rem; padding-top: 0.75rem; border-top: 1px solid var(--c-surface-sunken, #f1f5f9); margin-top: 0.5rem; }
  .compose-tool-btn { background: none; border: none; padding: 0.4rem; cursor: pointer; color: var(--c-text-dim, #94a3b8); border-radius: 0.35rem; font-size: 1rem; }
  .compose-visibility { margin-left: auto; }
  .compose-visibility select { border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.5rem; padding: 0.35rem 0.6rem; font-size: 0.78rem; color: var(--c-text-3, #475569); background: var(--c-surface, #fff); cursor: pointer; }
  .compose-submit { background: linear-gradient(135deg, var(--c-brand, #6366f1), var(--c-brand-alt, #8b5cf6)); color: var(--c-on-accent, #ffffff); border: none; padding: 0.5rem 1.25rem; border-radius: 2rem; font-weight: 700; font-size: 0.875rem; cursor: pointer; }
  .compose-submit:disabled { opacity: 0.5; cursor: not-allowed; }
  .compose-image-preview { width: 100%; max-height: 200px; object-fit: cover; border-radius: 0.75rem; margin-top: 0.5rem; display: none; }
  .compose-link-preview { border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.75rem; overflow: hidden; margin-top: 0.5rem; display: none; }

  /* ⚠️ A CARD WITH A HIERARCHY, not a tinted paragraph. It was one
     centred 0.85rem line in brand ink carrying two underlined links, so
     the heading, the reason and the actions were all the same weight and
     the eye had nowhere to land first. */
  .feed-login-banner {
    background: var(--c-brand-tint-2, #eef2ff); border: 1px solid var(--c-brand-border, #c7d2fe);
    border-radius: 1rem; padding: 1.15rem 1.25rem; margin-top: 0.75rem;
    display: flex; flex-direction: column; align-items: center; gap: 0.3rem; text-align: center;
  }
  .feed-login-title { margin: 0; font-size: 1rem; font-weight: 700; color: var(--c-brand-deep, #4338ca); }
  /* ⚠️ A MEASURE, because this sits in a column that is wide on a
     desktop -- a single line running the full width is the thing that
     read as filler. */
  .feed-login-sub { margin: 0; font-size: 0.85rem; line-height: 1.45; color: var(--c-text-muted, #64748b); max-width: 44ch; }
  .feed-login-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.5rem; margin-top: 0.55rem; }
  /* ⚠️ ONE FILLED, ONE QUIET. Two buttons of equal weight is the same
     "pick one, I do not mind" the underlined pair already said; signing
     up is the one this banner exists to ask for. */
  .feed-login-cta {
    background: var(--c-brand, #6366f1); color: var(--c-on-accent, #ffffff);
    padding: 0.5rem 1.35rem; border-radius: 2rem;
    font-weight: 700; font-size: 0.875rem; text-decoration: none;
  }
  .feed-login-cta:hover { background: var(--c-brand-strong, #4f46e5); color: var(--c-on-accent, #ffffff); }
  .feed-login-alt {
    background: var(--c-surface, #ffffff); color: var(--c-brand-deep, #4338ca);
    border: 1px solid var(--c-brand-border, #c7d2fe);
    padding: 0.5rem 1.35rem; border-radius: 2rem;
    font-weight: 600; font-size: 0.875rem; text-decoration: none;
  }
  .feed-login-alt:hover { background: var(--c-brand-tint, #f5f3ff); }

  /* ⚠⚠ `gap` IS LOAD-BEARING HERE, NOT DECORATION. This is an
     inline-FLEX box, and the network rail's button is written
     `Open <span data-site-brand>...</span>` -- a bare text node beside an
     element. Flexbox makes that text node an anonymous flex item and
     STRIPS the whitespace at its edges, so the space between "Open" and
     the site's name did not exist at render time and the button read
     "OpenSensorium Events". The space is in the markup and always was;
     nothing but a gap can put it back, because there is no text node left
     to hold it. */
  .btn-secondary-pill {
    background: var(--c-surface-sunken, #f1f5f9); color: var(--c-text-3, #475569); border: none; padding: 0.5rem 1.25rem;
    border-radius: 2rem; font-weight: 600; font-size: 0.875rem; cursor: pointer; text-decoration: none;
    display: inline-flex; align-items: center; gap: 0.35em;
  }

  .emoji-picker-overlay, .app-modal-overlay { position: fixed; inset: 0; z-index: 10000; background: color-mix(in srgb, var(--c-shadow, #000000) 30%, transparent); display: flex; align-items: center; justify-content: center; padding: 1rem; }
  .emoji-picker, .app-modal { background: var(--c-surface, #fff); border-radius: 1rem; padding: 1rem; width: 320px; max-height: 400px; overflow-y: auto; box-shadow: 0 20px 60px color-mix(in srgb, var(--c-shadow, #000000) 20%, transparent); }
  .emoji-picker-search { width: 100%; padding: 0.5rem 0.75rem; border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.5rem; font-size: 0.875rem; margin-bottom: 0.75rem; outline: none; }
  .emoji-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: 0.25rem; }
  .emoji-btn { background: none; border: none; font-size: 1.25rem; padding: 0.35rem; cursor: pointer; border-radius: 0.35rem; text-align: center; }
  .emoji-btn:hover { background: var(--c-brand-tint, #f5f3ff); }

  .reactions-modal { background: var(--c-surface, #fff); border-radius: 1rem; padding: 1.25rem; width: 360px; max-height: 500px; overflow-y: auto; box-shadow: 0 20px 60px color-mix(in srgb, var(--c-shadow, #000000) 20%, transparent); }
  .reaction-user-item { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem 0; border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9); }
  .reaction-user-avatar { width: 32px; height: 32px; border-radius: 50%; object-fit: cover; }

  @media (max-width: 640px) {
    .emoji-grid { grid-template-columns: repeat(6, 1fr); }
    .reactions-modal, .emoji-picker, .app-modal { width: 90vw; }
  }

/* The in-app Feed tab. feed.html wraps the same markup in
   .profile-container, which centres it under that page's own topbar;
   inside the app it sits in <main>, which already has the page gutters,
   so it only needs a readable measure. */
.feed-tab-shell { max-width: 640px; margin: 0 auto; }
.feed-load-more-row { text-align: center; margin-top: 0.75rem; }

/* AI monthly spend limit. Its own block rather than inline styles, so it
   matches the rest of the panel and follows the same dark/compact rules
   if those ever change in one place. */
.ai-spend-limit { margin-top: 0.9rem; }
.ai-spend-row { display: flex; align-items: center; gap: 0.5rem; }
.ai-spend-row .mnw-input { width: 130px; }
.ai-spend-currency { color: var(--c-text-muted, #64748b); font-size: 0.9rem; }
.ai-field-note { font-size: 0.72rem; color: var(--c-text-dim, #94a3b8); }
p.ai-field-note { margin: 0.25rem 0 0; }

/* Top-up cost breakdown. A definition list because that is what it is --
   a set of labelled amounts -- and it keeps label and figure associated
   for a screen reader rather than relying on visual columns. */
.topup-breakdown {
  margin: 0.75rem 0;
  padding: 0.75rem;
  background: var(--c-surface-subtle, #f8fafc);
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
  font-size: 0.82rem;
}
.topup-breakdown > div {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.15rem 0;
}
.topup-breakdown dt { color: var(--c-text-muted, #64748b); margin: 0; }
.topup-breakdown dd { margin: 0; color: var(--c-text, #0f172a); font-variant-numeric: tabular-nums; }

.topup-breakdown-total {
  border-top: 1px solid var(--c-border, #e2e8f0);
  margin-top: 0.35rem;
  padding-top: 0.45rem !important;
}
.topup-breakdown-total dt { color: var(--c-text, #0f172a); font-weight: 600; }
.topup-breakdown-total dd { font-weight: 700; }

/* ── REQUESTS ─────────────────────────────────────────────────────────
   Classes rather than inline styles, matching the rest of the app, so
   the dark/compact rules and the .hidden convention apply here too. */

/* ⚠️ .request-fab IS GONE, and the class is deleted rather than left
   unworn. A rule nothing wears reads as if the control still exists --
   the same reason this stylesheet does not keep undefined tokens around.
   Requests are reached from Account -> My Requests; bottom-right is the
   AI assistant's corner now. */

.request-modal { max-width: 560px; }
.request-mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.8rem; }

.request-cats { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-bottom: 0.4rem; }
.request-cat {
  padding: 0.3rem 0.75rem;
  border: 2px solid var(--c-border, #e2e8f0);
  border-radius: 2rem;
  background: var(--c-surface, #fff);
  color: var(--c-text-3, #475569);
  font-size: 0.78rem;
  font-weight: 600;
  cursor: pointer;
}
.request-cat:hover { border-color: var(--c-text-dim, #94a3b8); }
.request-cat.active { border-color: var(--c-brand, #6366f1); background: var(--c-brand-tint, #f5f3ff); color: var(--c-brand-deep, #4338ca); }
.request-cat-urgent { border-color: var(--c-danger-border, #fecaca); color: var(--c-danger-deep, #b91c1c); }
.request-cat-urgent.active { border-color: var(--c-danger, #ef4444); background: var(--c-danger-tint, #fef2f2); color: var(--c-danger-deep, #b91c1c); }

.request-filters { display: flex; gap: 0.35rem; flex-wrap: wrap; margin-bottom: 0.9rem; }
.request-filter {
  padding: 0.25rem 0.75rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 2rem;
  background: var(--c-surface, #fff);
  color: var(--c-text-muted, #64748b);
  font-size: 0.75rem;
  font-weight: 600;
  cursor: pointer;
}
.request-filter.active { background: var(--c-inverse-surface, #0f172a); border-color: var(--c-inverse-surface, #0f172a); color: var(--c-on-accent, #ffffff); }

.request-list { display: flex; flex-direction: column; gap: 0.5rem; }
.request-empty { text-align: center; padding: 2rem 1rem; }

/* A <button>, not a clickable <div>: these open a panel, so they are
   reachable by keyboard and announced as controls without extra work. */
.request-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.75rem;
  width: 100%;
  text-align: left;
  background: var(--c-surface, #fff);
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.6rem;
  padding: 0.75rem 0.9rem;
  cursor: pointer;
  font: inherit;
}
.request-row:hover { border-color: var(--c-brand, #6366f1); background: var(--c-surface-subtle, #f8fafc); }
.request-row-main { display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; flex: 1; }
.request-row-top { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }
.request-row-number { font-size: 0.7rem; font-weight: 700; color: var(--c-brand, #6366f1); }
.request-row-from { font-size: 0.72rem; color: var(--c-text-dim, #94a3b8); }
.request-row-title {
  font-weight: 600;
  font-size: 0.875rem;
  color: var(--c-text, #0f172a);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.request-row-sub { font-size: 0.72rem; color: var(--c-text-dim, #94a3b8); }
.request-row-icon { font-size: 1rem; flex-shrink: 0; }

.request-pill {
  font-size: 0.66rem;
  font-weight: 700;
  padding: 0.12rem 0.45rem;
  border-radius: 2rem;
  border: 1px solid;
  white-space: nowrap;
}
.request-pill-open { background: var(--c-info-tint, #eff6ff); color: var(--c-info-text, #1d4ed8); border-color: var(--c-info-border, #bfdbfe); }
.request-pill-in_progress { background: var(--c-warning-tint, #fffbeb); color: var(--c-warning-text, #92400e); border-color: var(--c-warning-border, #fde68a); }
.request-pill-needs_info { background: var(--c-brand-tint, #f5f3ff); color: var(--c-violet-text, #6d28d9); border-color: var(--c-violet-border, #ddd6fe); }
.request-pill-resolved { background: var(--c-success-tint, #f0fdf4); color: var(--c-success-text, #166534); border-color: var(--c-success-border, #bbf7d0); }
/* Its own colour, not borrowed from in_progress: "a robot has this" and
   "a person is working on it" are different states and the difference is
   the thing somebody scanning the list wants to see. */
.request-pill-sent_to_ai { background: var(--c-brand-tint, #f5f3ff); color: var(--c-violet-text, #6d28d9); border-color: var(--c-violet-border, #ddd6fe); }
.request-pill-closed { background: var(--c-surface-subtle, #f8fafc); color: var(--c-text-muted, #64748b); border-color: var(--c-border, #e2e8f0); }
.request-pill-wont_fix { background: var(--c-danger-tint, #fef2f2); color: var(--c-danger-darkest, #991b1b); border-color: var(--c-danger-border, #fecaca); }

.request-prio { font-size: 0.66rem; font-weight: 700; white-space: nowrap; }
.request-prio-low { color: var(--c-success-alt, #10b981); }
.request-prio-medium { color: var(--c-warning-text, #92400e); }
.request-prio-high { color: var(--c-danger, #ef4444); }
.request-prio-critical { color: var(--c-danger-deep, #b91c1c); }

.request-badge {
  background: var(--c-danger, #ef4444);
  color: var(--c-on-fill, #ffffff);
  font-size: 0.65rem;
  font-weight: 700;
  padding: 0.05rem 0.35rem;
  border-radius: 2rem;
  margin-left: 0.35rem;
}

.request-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
  gap: 0.6rem;
  margin: 1rem 0;
}
.request-stat {
  background: var(--c-surface-subtle, #f8fafc);
  border: 1px solid var(--c-border, #e2e8f0);
  border-top: 3px solid var(--c-text-dim, #94a3b8);
  border-radius: 0.5rem;
  padding: 0.75rem;
  text-align: center;
}
.request-stat-open { border-top-color: var(--c-brand, #6366f1); }
.request-stat-progress { border-top-color: var(--c-warning, #f59e0b); }
.request-stat-urgent { border-top-color: var(--c-danger, #ef4444); }
.request-stat-done { border-top-color: var(--c-success-alt, #10b981); }
/* No accent: "last 24h" is a volume figure, not a state that needs
   attention, so it keeps the neutral default the base rule sets. */
.request-stat-recent { border-top-color: var(--c-text-dim, #94a3b8); }
.request-stat-num { font-size: 1.4rem; font-weight: 800; color: var(--c-text, #0f172a); }
.request-stat-label { font-size: 0.7rem; color: var(--c-text-dim, #94a3b8); }

.request-toolbar { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1rem; }
.request-toolbar .mnw-input { width: auto; flex: 0 1 auto; }
.request-toolbar input[type="search"] { flex: 1 1 180px; }

/* ── detail drawer ─────────────────────────────────────────────────── */

.request-drawer-overlay {
  position: fixed;
  inset: 0;
  background: color-mix(in srgb, var(--c-scrim, #0f172a) 25%, transparent);
  z-index: 10000;
}
.request-drawer {
  position: fixed;
  top: 0;
  right: 0;
  width: 520px;
  max-width: 100vw;
  height: 100vh;
  background: var(--c-surface, #fff);
  border-left: 1px solid var(--c-border, #e2e8f0);
  z-index: 10001;
  display: flex;
  flex-direction: column;
  box-shadow: -4px 0 24px color-mix(in srgb, var(--c-shadow, #000000) 12%, transparent);
}
/* .hidden is display:none !important app-wide, so it beats the flex
   above -- which is why neither of these is toggled with an inline
   style. */

.request-drawer-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 1.1rem 1.25rem;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
}
.request-drawer-number { font-size: 0.72rem; font-weight: 700; color: var(--c-brand, #6366f1); }
.request-drawer-title { font-size: 0.95rem; font-weight: 700; color: var(--c-text, #0f172a); }
.request-drawer-close {
  background: none; border: none; color: var(--c-text-dim, #94a3b8);
  font-size: 1.1rem; cursor: pointer; line-height: 1; flex-shrink: 0;
}
.request-drawer-body { padding: 1.25rem; overflow-y: auto; flex: 1; }
.request-drawer-controls { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1rem; }
.request-drawer-controls .mnw-input { width: auto; flex: 1 1 140px; }

.request-meta {
  background: var(--c-surface-subtle, #f8fafc);
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
  padding: 0.75rem;
  margin: 0 0 1rem;
  font-size: 0.8rem;
}
.request-meta > div { display: flex; justify-content: space-between; gap: 1rem; padding: 0.12rem 0; }
.request-meta dt { color: var(--c-text-dim, #94a3b8); margin: 0; flex-shrink: 0; }
.request-meta dd { margin: 0; color: var(--c-text, #0f172a); font-weight: 600; text-align: right; overflow-wrap: anywhere; }

.request-section-label { font-weight: 600; font-size: 0.82rem; margin: 1rem 0 0.35rem; }
.request-body-text {
  font-size: 0.85rem;
  color: var(--c-text-3, #475569);
  line-height: 1.6;
  /* pre-wrap keeps the reporter's own line breaks, which in a bug report
     are usually numbered reproduction steps. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.request-error-text {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.76rem;
  background: var(--c-danger-tint, #fef2f2);
  color: var(--c-danger-darkest, #991b1b);
  border-radius: 0.35rem;
  padding: 0.5rem;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.request-comments { display: flex; flex-direction: column; gap: 0.6rem; }
.request-comment { border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9); padding-bottom: 0.6rem; }
.request-comment-internal {
  background: var(--c-warning-tint, #fffbeb);
  border: 1px solid var(--c-warning-border, #fde68a);
  border-radius: 0.5rem;
  padding: 0.6rem;
}
.request-comment-head { display: flex; justify-content: space-between; gap: 0.5rem; margin-bottom: 0.2rem; }
.request-comment-who { font-size: 0.75rem; font-weight: 700; color: var(--c-text, #0f172a); }
.request-comment-when { font-size: 0.68rem; color: var(--c-text-dim, #94a3b8); white-space: nowrap; }
.request-comment-body { font-size: 0.82rem; color: var(--c-text-3, #475569); line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; }
.request-internal-tag {
  font-size: 0.62rem; color: var(--c-warning-text, #92400e); background: var(--c-warning-border, #fde68a);
  padding: 0.05rem 0.3rem; border-radius: 2rem; margin-left: 0.3rem;
}

.request-comment-box {
  background: var(--c-surface-subtle, #f8fafc);
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
  padding: 0.75rem;
  margin-top: 0.9rem;
}
.request-comment-actions { display: flex; align-items: center; gap: 0.75rem; margin-top: 0.5rem; }
.request-internal-label { display: flex; align-items: center; gap: 0.35rem; font-size: 0.74rem; color: var(--c-text-muted, #64748b); cursor: pointer; }
.request-comment-actions button:last-child { margin-left: auto; }

@media (max-width: 560px) {
  .request-drawer { width: 100vw; }
}

/* ── AI data access rows ─────────────────────────────────────────────
   A <label> wrapping the whole row, so the entire strip is the hit
   target for its checkbox rather than only the 40px switch. */
.ai-access { padding: 0; overflow: hidden; }
.ai-access-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.875rem 1rem;
  background: var(--c-surface-subtle, #f8fafc);
  border-bottom: 1px solid var(--c-border, #e2e8f0);
}
.ai-access-head .ai-subsection-title { margin-bottom: 0.1rem; }

.ai-access-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 1rem;
  border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9);
  cursor: pointer;
}
.ai-access-row:last-child { border-bottom: none; }
.ai-access-row:hover { background: var(--c-surface-subtle, #f8fafc); }

.ai-access-icon { font-size: 1.05rem; width: 24px; text-align: center; flex-shrink: 0; }
.ai-access-text { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.ai-access-label { font-size: 0.82rem; font-weight: 600; color: var(--c-text, #0f172a); }
.ai-access-desc { font-size: 0.72rem; color: var(--c-text-muted, #64748b); line-height: 1.35; }
.ai-access-row .toggle-switch { flex-shrink: 0; }

.ai-access + .ai-field-note,
.ai-access > .ai-field-note { padding: 0.6rem 1rem; margin: 0; border-top: 1px solid var(--c-surface-sunken, #f1f5f9); }

/* ── AI campaign card + action history ───────────────────────────────
   Inside the assistant panel, which is dark, so this card carries its
   own light surface rather than inheriting the panel's. */
.ai-campaign-card {
  align-self: stretch;
  background: var(--c-success-tint, #f0fdf4);
  border: 1px solid var(--c-success-border, #bbf7d0);
  border-radius: 0.75rem;
  padding: 0.75rem;
  font-size: 0.78rem;
  color: var(--c-text, #0f172a);
}
.ai-campaign-head {
  display: flex; align-items: center; justify-content: space-between;
  font-weight: 700; color: var(--c-success-text, #166534); margin-bottom: 0.5rem;
}
.ai-campaign-x { background: none; border: none; color: var(--c-text-muted, #64748b); cursor: pointer; font-size: 0.85rem; line-height: 1; }

.ai-campaign-meta { margin: 0 0 0.5rem; }
.ai-campaign-meta > div { display: flex; justify-content: space-between; gap: 0.75rem; padding: 0.1rem 0; }
.ai-campaign-meta dt { color: var(--c-text-muted, #64748b); margin: 0; }
.ai-campaign-meta dd { margin: 0; font-weight: 600; text-align: right; overflow-wrap: anywhere; }

.ai-campaign-msg { background: var(--c-surface, #fff); border: 1px solid var(--c-success-tint-2, #dcfce7); border-radius: 0.4rem; padding: 0.5rem; }
.ai-campaign-msg-label {
  font-size: 0.62rem; color: var(--c-text-dim, #94a3b8); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.2rem;
}
.ai-campaign-msg-body { line-height: 1.45; white-space: pre-wrap; overflow-wrap: anywhere; }
.ai-campaign-msg-foot { font-size: 0.66rem; color: var(--c-text-dim, #94a3b8); margin-top: 0.3rem; }

.ai-campaign-ok {
  background: var(--c-success-tint-2, #dcfce7); border-radius: 0.35rem; padding: 0.35rem 0.55rem;
  font-size: 0.7rem; color: var(--c-success-text, #166534); font-weight: 600; margin-top: 0.5rem;
}
.ai-campaign-issues, .ai-campaign-warnings {
  border-radius: 0.35rem; padding: 0.45rem 0.55rem; margin-top: 0.5rem; font-size: 0.7rem;
}
.ai-campaign-issues { background: var(--c-danger-tint, #fef2f2); border: 1px solid var(--c-danger-border, #fecaca); color: var(--c-danger-darkest, #991b1b); }
.ai-campaign-warnings { background: var(--c-warning-tint, #fffbeb); border: 1px solid var(--c-warning-border, #fde68a); color: var(--c-warning-text, #92400e); }
.ai-campaign-issues ul, .ai-campaign-warnings ul { margin: 0.2rem 0 0; padding-left: 1.1rem; }

.ai-campaign-actions { display: flex; gap: 0.5rem; margin-top: 0.6rem; }
.ai-campaign-open {
  flex: 2; background: var(--c-brand, #6366f1); color: var(--c-on-fill, #ffffff); border: none;
  padding: 0.5rem; border-radius: 0.45rem; font-weight: 700; font-size: 0.76rem; cursor: pointer;
}
.ai-campaign-dismiss {
  flex: 1; background: var(--c-surface, #fff); color: var(--c-text-muted, #64748b); border: 1px solid var(--c-border, #e2e8f0);
  padding: 0.5rem; border-radius: 0.45rem; font-size: 0.76rem; cursor: pointer;
}
.ai-campaign-note { text-align: center; font-size: 0.66rem; color: var(--c-text-muted, #64748b); margin-top: 0.45rem; }

.ai-action-row {
  display: flex; align-items: center; gap: 0.75rem;
  padding: 0.5rem 1rem; border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9);
}
.ai-action-row:last-child { border-bottom: none; }
.ai-action-status { font-size: 0.7rem; font-weight: 700; white-space: nowrap; }
.ai-action-offered { color: var(--c-brand-deep, #4338ca); }
.ai-action-taken { color: var(--c-warning-text, #92400e); }
.ai-action-done { color: var(--c-success-text, #166534); }
.ai-action-failed { color: var(--c-danger-deep, #b91c1c); }

/* ── Admin email configuration: two across on desktop ────────────────
   This panel was one long single column of eight fields, which on a
   wide screen is a lot of scrolling past a lot of empty space. */
.admin-email-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.25rem 1rem;
  margin-bottom: 0.5rem;
}
/* minmax(0, 1fr) rather than 1fr: a grid item's default min-width is
   auto, so a long placeholder or value would otherwise push the column
   wider than its share and break the two-up layout. */

.admin-email-field { display: flex; flex-direction: column; min-width: 0; }
.admin-email-field input { width: 100%; }
.admin-email-field .input-actions { display: flex; gap: 0.5rem; }
.admin-email-field .input-actions input { flex: 1; min-width: 0; }

/* The toggle sits opposite Host, so it is nudged down to line up with
   that field's input rather than its label. */
.admin-email-secure-row { margin-top: 1.55rem; }

@media (max-width: 720px) {
  .admin-email-grid { grid-template-columns: 1fr; }
  .admin-email-secure-row { margin-top: 0.5rem; }
}

/* ═══════════════════════════════════════════════════════════════════
   Campaign composer (Compose tab)

   Classes, not inline styles, so the composer answers to the same
   palette as the rest of the app -- a block of hard-coded hex in the
   markup is a block that does not follow a theme change.
   ═══════════════════════════════════════════════════════════════════ */

/* The composer's own title row is gone -- #compose-section now carries
   the standard .panel-head every other rail page's content card uses,
   and its actions moved up into it. A second heading inside the
   collapsible panel was the same words twice. */

.composer-score {
  font-size: 0.78rem;
  font-weight: 600;
}

.composer-ai-btn {
  background: var(--c-brand, #6366f1);
  color: var(--c-on-fill, #ffffff);
  border: none;
  border-radius: 0.5rem;
  padding: 0.4rem 0.75rem;
  font-size: 0.8rem;
  font-weight: 700;
  cursor: pointer;
}

.composer-ai-btn:hover { background: var(--c-brand-strong, #4f46e5); }

.composer-card {
  background: var(--c-surface, #fff);
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.6rem;
  overflow: hidden;
  margin: 0 0 1rem;
}

.composer-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.6rem 0.85rem;
  background: var(--c-surface-subtle, #f8fafc);
  border-bottom: 1px solid var(--c-border, #e2e8f0);
  font-size: 0.85rem;
}

.composer-card-body { padding: 0.85rem; }

.composer-card-body > label:first-child { margin-top: 0; }

/* ── Composer: the two desktop rows ──────────────────────────────────
   Both are plain flex rows that stack under 900px, which is the same
   breakpoint the rest of the composer's two-up layouts use.

   align-items: start, not stretch. Both rows pair a fixed-height thing
   with a <details> that grows when somebody opens it; stretching would
   drag the shorter card's border down to match an expanded neighbour,
   which reads as a rendering fault rather than as a layout. */
.composer-top-row,
.composer-split-row {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  margin-bottom: 1rem;
}
.composer-top-row > *,
.composer-split-row > * {
  flex: 1 1 0;
  /* flex items refuse to shrink below their content by default, and a
     long placeholder or an unbroken directive line would otherwise push
     the row wider than the column. */
  min-width: 0;
  margin-bottom: 0;
}

/* The name column is already half the row, so the input fills it. Its
   own max-width: 50% is what keeps it to half the FULL column when it
   is not in a row -- inside one it would be a quarter of the page. */
.composer-top-name .campaign-name-input { max-width: 100%; }

/* Linxi Network: the field two thirds of the card, centred.
   The BODY is constrained rather than each child. Constraining children
   does not work here -- a <textarea> is inline-block, and auto margins
   only centre a block-level box, so the field would sit at 2/3 width
   hard against the left edge. Doing it on the body also keeps the meta
   line and the two notes on the same measure; a 2/3 field over
   full-width prose reads as a mistake rather than as a choice. */
#linxi-composer-section .composer-card-body {
  max-width: 66.667%;
  margin-left: auto;
  margin-right: auto;
}

@media (max-width: 900px) {
  .composer-top-row,
  .composer-split-row { display: block; }
  .composer-top-row > *,
  .composer-split-row > * { margin-bottom: 1rem; }
  .composer-top-row > *:last-child,
  .composer-split-row > *:last-child { margin-bottom: 0; }
  #linxi-composer-section .composer-card-body {
    max-width: 100%;
    margin-left: 0;
    margin-right: 0;
  }
}

.composer-card-body textarea,
.composer-card-body select,
.composer-card-body input[type="text"],
.composer-card-body input[type="email"] {
  width: 100%;
  box-sizing: border-box;
}

.composer-meta-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
  margin-top: 0.4rem;
}

.composer-counter { margin: 0; font-size: 0.75rem; }

.composer-tools { display: flex; gap: 0.35rem; flex-wrap: wrap; }

.composer-hint { font-size: 0.75rem; margin: 0.4rem 0 0; }

.composer-secondary-actions {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.6rem;
}

.composer-secondary-actions button { flex: 1; }

#email-subject-count.over-limit { color: var(--c-warning-text, #92400e); font-weight: 700; }

/* One textarea for the body; HTML mode only changes how it reads. */
#email-body.email-body-html {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.8rem;
}

.email-format-toggle { display: inline-flex; gap: 0.25rem; }

.email-format-btn {
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface, #fff);
  color: var(--c-text-muted, #64748b);
  border-radius: 0.35rem;
  padding: 0.15rem 0.5rem;
  font-size: 0.72rem;
  font-weight: 600;
  cursor: pointer;
}

.email-format-btn.active {
  border-color: var(--c-brand, #6366f1);
  background: var(--c-brand-tint, #f5f3ff);
  color: var(--c-brand, #6366f1);
}

.recipient-badge {
  background: var(--c-brand, #6366f1);
  color: var(--c-on-fill, #ffffff);
  border-radius: 2rem;
  padding: 0.1rem 0.55rem;
  font-size: 0.72rem;
  font-weight: 700;
}

.recipient-badge-empty { background: var(--c-text-dim, #94a3b8); }

/* --- compliance results --- */

.compliance-result {
  margin-top: 0.5rem;
  border-radius: 0.4rem;
  padding: 0.55rem 0.7rem;
  font-size: 0.78rem;
}

.compliance-result ul { margin: 0.3rem 0 0; padding-left: 1.1rem; }
.compliance-result li { margin: 0.1rem 0; }
.compliance-sub { margin-top: 0.45rem; font-weight: 700; }

.compliance-ok { background: var(--c-success-tint, #f0fdf4); border: 1px solid var(--c-success-border, #bbf7d0); color: var(--c-success-text, #166534); }
.compliance-bad { background: var(--c-danger-tint, #fef2f2); border: 1px solid var(--c-danger-border, #fecaca); color: var(--c-danger-darkest, #991b1b); }

/* --- cost summary --- */

.cost-summary {
  background: var(--c-surface-subtle, #f8fafc);
  border-radius: 0.45rem;
  padding: 0.6rem 0.75rem;
  font-size: 0.82rem;
}

.cost-row {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.1rem 0;
}

.cost-total {
  border-top: 1px solid var(--c-border, #e2e8f0);
  margin-top: 0.4rem;
  padding-top: 0.4rem;
  font-weight: 700;
}

.cost-free { color: var(--c-success-alt, #10b981); font-weight: 600; }

.composer-balance-notice {
  margin: 0.6rem 0;
  padding: 0.5rem 0.7rem;
  border-radius: 0.4rem;
  background: var(--c-danger-tint, #fef2f2);
  border: 1px solid var(--c-danger-border, #fecaca);
  color: var(--c-danger-darkest, #991b1b);
  font-size: 0.78rem;
}

/* --- AI assist panel --- */

/* The card look is gone -- it is a drawer now (see the shared rule with
   .cal-ai-drawer above). What is left is the internal layout: a fixed
   head, a scrolling middle, and a composer pinned to the bottom, which
   is what stops a long conversation pushing the input off screen. */
.campaign-ai-panel { padding: 0; }
.campaign-ai-panel > .campaign-ai-head {
  padding: 0.8rem 1rem;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
  margin-bottom: 0;
  flex-shrink: 0;
}
.campaign-ai-panel > .campaign-ai-messages {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 1rem;
  max-height: none;
}
.campaign-ai-panel > textarea,
.campaign-ai-panel > .campaign-ai-prompts,
.campaign-ai-panel > .campaign-ai-actions,
.campaign-ai-panel > .result {
  flex-shrink: 0;
  margin-left: 1rem;
  margin-right: 1rem;
}
.campaign-ai-panel > textarea { margin-top: 0.6rem; }
.campaign-ai-panel > *:last-child { margin-bottom: 0.9rem; }

.campaign-ai-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.6rem;
}

.campaign-ai-who { display: flex; align-items: center; gap: 0.5rem; font-size: 0.82rem; }

.campaign-ai-avatar {
  width: 28px;
  height: 28px;
  flex: 0 0 28px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--c-brand, #6366f1), var(--c-brand-alt, #8b5cf6));
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 0.9rem; color: var(--c-on-status, #0f172a);
}

.campaign-ai-tagline { display: block; font-size: 0.7rem; }

.campaign-ai-x {
  background: none;
  border: none;
  color: var(--c-text-muted, #64748b);
  cursor: pointer;
  font-size: 0.9rem;
  line-height: 1;
}

#campaign-ai-input {
  width: 100%;
  box-sizing: border-box;
  border: 1px solid var(--c-brand-border, #c7d2fe);
  border-radius: 0.45rem;
  padding: 0.55rem;
  font-family: inherit;
  font-size: 0.82rem;
  line-height: 1.5;
  resize: vertical;
  background: var(--c-surface, #fff);
}

.campaign-ai-prompts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin: 0.5rem 0 0.6rem;
}

.ai-quick-prompt {
  background: var(--c-surface, #fff);
  border: 1px solid var(--c-brand-border, #c7d2fe);
  border-radius: 2rem;
  padding: 0.15rem 0.55rem;
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--c-brand, #6366f1);
  cursor: pointer;
}

.ai-quick-prompt:hover { background: var(--c-brand, #6366f1); color: var(--c-on-fill, #ffffff); border-color: var(--c-brand, #6366f1); }

.campaign-ai-run {
  width: 100%;
  background: var(--c-brand, #6366f1);
  color: var(--c-on-fill, #ffffff);
  border: none;
  border-radius: 0.45rem;
  padding: 0.55rem;
  font-size: 0.82rem;
  font-weight: 700;
  cursor: pointer;
}

.campaign-ai-run:hover { background: var(--c-brand-strong, #4f46e5); }
.campaign-ai-run:disabled { opacity: 0.65; cursor: default; }

.campaign-ai-status {
  margin: 0.4rem 0 0;
  font-size: 0.76rem;
  white-space: pre-wrap;
  color: var(--c-brand, #6366f1);
}

.campaign-ai-status-ok { color: var(--c-success-text, #166534); }
.campaign-ai-status-error { color: var(--c-danger-deep, #b91c1c); }
.campaign-ai-status-note { color: var(--c-text-3, #475569); }

.campaign-ai-notice {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-top: 0.55rem;
  padding: 0.45rem 0.65rem;
  border-radius: 0.4rem;
  background: var(--c-success-tint, #f0fdf4);
  border: 1px solid var(--c-success-border, #bbf7d0);
  color: var(--c-success-text, #166534);
  font-size: 0.76rem;
}

/* The subject line on the assistant's email draft card. */
.ai-campaign-msg-subject {
  font-weight: 700;
  font-size: 0.8rem;
  margin-bottom: 0.25rem;
}

/* ═══════════════════════════════════════════════════════════════════
   Flow — thread view (REQ-0011)

   Grouped by contact, inbound and outbound under one header. The flat
   Timeline table is kept alongside it; see the markup comment for why.
   ═══════════════════════════════════════════════════════════════════ */

.flow-controls {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin: 0.5rem 0 0.9rem;
}

.flow-mode-group { display: inline-flex; gap: 0.25rem; }

.flow-mode-btn {
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface, #fff);
  color: var(--c-text-muted, #64748b);
  border-radius: 0.4rem;
  padding: 0.25rem 0.6rem;
  font-size: 0.78rem;
  font-weight: 600;
  cursor: pointer;
}

.flow-mode-btn.active {
  border-color: var(--c-brand, #6366f1);
  background: var(--c-brand-tint, #f5f3ff);
  color: var(--c-brand, #6366f1);
}

.flow-sort-label {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.78rem;
  color: var(--c-text-muted, #64748b);
  font-weight: 600;
}

.flow-sort-label select {
  padding: 0.25rem 0.4rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 0.35rem;
  font-size: 0.78rem;
}

.flow-count { font-size: 0.75rem; margin-left: auto; }
.flow-limit-note { font-size: 0.75rem; margin-top: 0.6rem; }

.flow-thread {
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
  margin-bottom: 0.45rem;
  background: var(--c-surface, #fff);
  overflow: hidden;
}

.flow-thread-open { border-color: var(--c-brand-border, #c7d2fe); }

.flow-thread-head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  width: 100%;
  padding: 0.55rem 0.7rem;
  background: none;
  border: none;
  cursor: pointer;
  text-align: left;
  font: inherit;
  color: inherit;
}

.flow-thread-head:hover { background: var(--c-surface-subtle, #f8fafc); }
.flow-thread-open .flow-thread-head { background: var(--c-brand-tint, #f5f3ff); }

.flow-thread-caret { flex: 0 0 0.8rem; color: var(--c-text-dim, #94a3b8); font-size: 0.7rem; }

.flow-thread-who { flex: 1 1 12rem; min-width: 0; }

.flow-thread-name {
  display: block;
  font-weight: 600;
  font-size: 0.88rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.flow-thread-address {
  display: block;
  font-size: 0.74rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.flow-thread-carrier { flex: 0 1 10rem; font-size: 0.74rem; }

.flow-thread-meta { display: inline-flex; gap: 0.4rem; flex: 0 0 auto; font-size: 0.75rem; }

.flow-thread-count-in { color: var(--c-info-text, #1d4ed8); font-weight: 600; }
.flow-thread-count-out { color: var(--c-brand-deep, #4338ca); font-weight: 600; }
.flow-thread-failed { color: var(--c-danger-deep, #b91c1c); font-weight: 700; }

.flow-thread-when { flex: 0 0 auto; font-size: 0.74rem; }

.flow-thread-body:empty { display: none; }

.flow-thread-body {
  padding: 0.6rem 0.7rem;
  border-top: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc);
  max-height: 60vh;
  overflow-y: auto;
}

/* Inbound left, outbound right -- the arrangement every messaging client
   uses, so the direction is readable without reading a badge. */
.flow-msg { display: flex; margin-bottom: 0.4rem; }
.flow-msg-in { justify-content: flex-start; }
.flow-msg-out { justify-content: flex-end; }

.flow-msg-bubble {
  max-width: 82%;
  border-radius: 0.55rem;
  padding: 0.45rem 0.6rem;
  font-size: 0.82rem;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface, #fff);
}

.flow-msg-out .flow-msg-bubble { background: var(--c-brand-tint, #f5f3ff); border-color: var(--c-violet-border, #ddd6fe); }

.flow-msg-subject { font-weight: 700; font-size: 0.8rem; margin-bottom: 0.15rem; }

/* Line breaks and long links both matter here: an SMS written with
   newlines is not one paragraph, and an unbroken URL would push the
   bubble past the panel. */
.flow-msg-body { white-space: pre-wrap; overflow-wrap: anywhere; }

.flow-msg-foot {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  flex-wrap: wrap;
  margin-top: 0.3rem;
  font-size: 0.7rem;
}

@media (max-width: 720px) {
  .flow-thread-carrier { display: none; }
  .flow-msg-bubble { max-width: 92%; }
}

/* ═══════════════════════════════════════════════════════════════════
   AI assistant — expand, and the conversation list
   ═══════════════════════════════════════════════════════════════════ */

/* Expanded is a WIDER PANEL, not a different surface. The conversation
   you are in is the one that gets bigger, so nothing is lost switching
   between the two. Capped rather than full-bleed: a chat column the full
   width of a desktop monitor is harder to read, not easier. */
.ai-chat-panel.ai-expanded {
  width: min(760px, calc(100vw - 3rem));
  height: min(760px, calc(100vh - 7rem));
}

.ai-chat-panel.ai-expanded .ai-msg-user,
.ai-chat-panel.ai-expanded .ai-msg-assistant {
  font-size: 0.9rem;
  max-width: 46rem;
}

@media (max-width: 480px) {
  /* Already full-width down here; expanding only adds height. */
  .ai-chat-panel.ai-expanded { width: auto; }
}

/* The conversation list. Layered OVER the thread rather than replacing
   it, so closing the drawer puts you back exactly where you were. */
.ai-history-drawer {
  position: absolute;
  inset: 3.4rem 0 0 0;
  background: var(--c-surface, #ffffff);
  border-top: 1px solid var(--c-border, #e2e8f0);
  z-index: 2;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.ai-history-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.6rem 0.85rem;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
  color: var(--c-text, #0f172a);
  font-size: 0.85rem;
}

.ai-history-head button {
  background: none;
  border: none;
  color: var(--c-text-dim, #94a3b8);
  cursor: pointer;
  font-size: 0.9rem;
}

.ai-history-list {
  flex: 1;
  overflow-y: auto;
  padding: 0.5rem;
  color: var(--c-text, #0f172a);
  font-size: 0.82rem;
}

.ai-history-list .muted { color: var(--c-text-dim, #94a3b8); }

.ai-history-item {
  display: flex;
  align-items: stretch;
  gap: 0.25rem;
  border-radius: 0.5rem;
  margin-bottom: 0.25rem;
}

.ai-history-item:hover { background: var(--c-surface, #ffffff); }
.ai-history-current { background: color-mix(in srgb, var(--c-brand, #6366f1) 35%, var(--c-surface-subtle, #f8fafc)); }

.ai-history-open {
  flex: 1;
  min-width: 0;
  text-align: left;
  background: none;
  border: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
  padding: 0.45rem 0.55rem;
}

.ai-history-title {
  display: block;
  font-weight: 600;
  color: var(--c-text, #0f172a);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ai-history-meta { display: block; font-size: 0.72rem; color: var(--c-text-dim, #94a3b8); }

.ai-history-delete {
  background: none;
  border: none;
  color: var(--c-text-muted, #64748b);
  cursor: pointer;
  padding: 0 0.5rem;
  font-size: 0.8rem;
}

.ai-history-delete:hover { color: var(--c-danger, #ef4444); }

/* The drawer's `inset` resolves against .ai-chat-panel, which is already
   position: fixed further up this file -- so it is a containing block
   without needing a second declaration here. */

/* ── the composer's own AI thread ─────────────────────────────────── */

.campaign-ai-messages {
  max-height: 15rem;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.5rem;
  margin-bottom: 0.5rem;
  background: color-mix(in srgb, var(--c-highlight, #ffffff) 70%, transparent);
  border: 1px solid var(--c-brand-tint-3, #e0e7ff);
  border-radius: 0.45rem;
}

.campaign-ai-messages:empty { display: none; }

/* This thread sits on a LIGHT panel, unlike the bubble's dark one, so the
   shared .ai-msg-* rules are re-coloured here rather than the markup
   being given different classes -- one renderer, two settings. */
.campaign-ai-messages .ai-msg-user,
.campaign-ai-messages .ai-msg-assistant {
  padding: 0.4rem 0.6rem;
  border-radius: 0.45rem;
  font-size: 0.8rem;
  line-height: 1.5;
  max-width: 100%;
}

.campaign-ai-messages .ai-msg-user {
  align-self: flex-end;
  background: var(--c-brand, #6366f1);
  color: var(--c-on-fill, #ffffff);
}

.campaign-ai-messages .ai-msg-assistant {
  align-self: flex-start;
  background: var(--c-surface, #fff);
  border: 1px solid var(--c-border, #e2e8f0);
  color: var(--c-text, #0f172a);
}

.campaign-ai-messages .ai-msg-assistant p { margin: 0 0 0.4rem; }
.campaign-ai-messages .ai-msg-assistant p:last-child { margin-bottom: 0; }
.campaign-ai-messages .ai-msg-assistant ul { margin: 0.2rem 0; padding-left: 1.1rem; }

/* The draft card renders on a dark background in the bubble; here it is
   on a light one. */
.campaign-ai-messages .ai-campaign-card {
  background: var(--c-surface, #fff);
  border: 1px solid var(--c-brand-border, #c7d2fe);
  color: var(--c-text, #0f172a);
}

.campaign-ai-actions {
  display: flex;
  align-items: center;
  gap: 0.35rem;
}

.campaign-ai-actions .campaign-ai-run { flex: 1; width: auto; }

.campaign-ai-expanded .campaign-ai-messages { max-height: 32rem; }
.campaign-ai-expanded #campaign-ai-input { min-height: 5rem; }

/* ── The standard content-card header ─────────────────────────────── */
/*
   An <h2> and its actions on one row, with a muted description line
   under it. This is the shape the rail pages' cards already had by
   convention -- .campaign-submissions-header was one hand-rolled copy of
   it -- and naming it means the next card gets it without a fifth.
*/
.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}

.panel-head h2 { margin: 0; }

.panel-head-actions {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
}

/* The description line that follows a .panel-head sits tight under it
   rather than taking a paragraph's full leading gap. */
.panel-head + .muted { margin-top: 0.25rem; }

/* Cards stack with the same gap on EVERY rail page. This rule existed
   only for [data-acct-panel-id], so a column of cards that were not
   inside an account panel -- the Compose tab's composer and campaign
   list, for two -- sat flush against each other. */
.ws-settings-panels > .card + .card,
.ws-settings-panels > [data-acct-panel-id].ws-panel-active > .card + .card { margin-top: 1.5rem; }

/* ═══════════════════════════════════════════════════════════════════
   Campaign composer — left rail

   The rail itself needs NO layout rules here. It is a plain
   .ws-settings-rail inside the standard .ws-settings-layout, exactly
   like Account / Linxi Settings / Admin, and it inherits everything from
   those.

   It was briefly given its own grid inside #new-campaign-panel, which
   does not work and cannot be made to: .ws-settings-rail is a
   PAGE-LEVEL element -- height: 100vh, position: sticky, and negative
   margins that cancel main's padding so it meets the header. Nested in a
   card inside a collapsible panel it becomes a full-window-height bar
   wedged into that div, ignoring whatever column width the parent grid
   asked for. Only the two rules below are specific to this rail.
   ═══════════════════════════════════════════════════════════════════ */

/* A label, not a collapse toggle. The other rails group enough entries
   to be worth collapsing; four channels are not, and a disclosure arrow
   that hides four buttons is a control with nothing to do. */
.ws-rail-group-header-static {
  /* Same left inset .ws-rail-group-header computes -- a row's accent
     border plus its padding -- so the heading and the buttons under it
     share one text edge. A hand-picked value here drifts the moment
     --rail-row-pad-x or --rail-accent-width changes. */
  padding: 0.45rem var(--rail-row-pad-x) 0.35rem
           calc(var(--rail-row-pad-x) + var(--rail-accent-width));
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--c-text-muted, #64748b);
}

.composer-rail-note {
  font-size: 0.7rem;
  line-height: 1.5;
  margin: 0.75rem 0 0;
  padding: 0 var(--rail-row-pad-x) 0
           calc(var(--rail-row-pad-x) + var(--rail-accent-width));
}

#linxi-body {
  width: 100%;
  box-sizing: border-box;
}

/* ═══════════════════════════════════════════════════════════════════
   My AI — Conversations browser
   ═══════════════════════════════════════════════════════════════════ */

.ai-convo-layout {
  display: grid;
  grid-template-columns: minmax(0, 18rem) minmax(0, 1fr);
  gap: 1rem;
  align-items: start;
  margin-top: 0.75rem;
}

.ai-convo-list {
  max-height: 60vh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.ai-convo-item {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  width: 100%;
  text-align: left;
  background: none;
  border: 1px solid transparent;
  border-radius: 0.45rem;
  padding: 0.5rem 0.6rem;
  cursor: pointer;
  font: inherit;
  color: inherit;
}

.ai-convo-item:hover { background: var(--c-surface-subtle, #f8fafc); }

.ai-convo-current {
  background: var(--c-brand-tint, #f5f3ff);
  border-color: var(--c-brand-border, #c7d2fe);
}

.ai-convo-title {
  font-weight: 600;
  font-size: 0.85rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ai-convo-meta { font-size: 0.72rem; color: var(--c-text-muted, #64748b); }

.ai-convo-campaigns {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  margin-top: 0.2rem;
}

.ai-convo-campaign {
  font-size: 0.68rem;
  background: var(--c-brand-tint-2, #eef2ff);
  color: var(--c-brand-deep, #4338ca);
  border-radius: 2rem;
  padding: 0.05rem 0.45rem;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The reading pane. Same .ai-msg-* markup the bubble uses, re-coloured
   for a light card -- one renderer, two settings, exactly as the
   composer's own AI thread does it. */
.ai-convo-reader {
  max-height: 60vh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.75rem;
  background: var(--c-surface-subtle, #f8fafc);
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
}

.ai-convo-reader .ai-msg-user,
.ai-convo-reader .ai-msg-assistant {
  padding: 0.45rem 0.65rem;
  border-radius: 0.45rem;
  font-size: 0.82rem;
  line-height: 1.55;
  max-width: 85%;
}

.ai-convo-reader .ai-msg-user {
  align-self: flex-end;
  background: var(--c-brand, #6366f1);
  color: var(--c-on-fill, #ffffff);
}

.ai-convo-reader .ai-msg-assistant {
  align-self: flex-start;
  background: var(--c-surface, #fff);
  border: 1px solid var(--c-border, #e2e8f0);
  color: var(--c-text, #0f172a);
}

.ai-convo-reader .ai-msg-assistant p { margin: 0 0 0.4rem; }
.ai-convo-reader .ai-msg-assistant p:last-child { margin-bottom: 0; }
.ai-convo-reader .ai-msg-assistant ul { margin: 0.2rem 0; padding-left: 1.1rem; }

@media (max-width: 860px) {
  .ai-convo-layout { grid-template-columns: minmax(0, 1fr); }
  .ai-convo-list { max-height: 22rem; }
}

/* ── Composer rail: channel checkboxes ────────────────────────────── */
/*
   A <label> wearing .ws-rail-btn, so a channel row looks exactly like
   every other rail entry while being a real checkbox. The whole row is
   the hit target because the label wraps the input -- no `for=`/id pair
   to keep in step.
*/
.composer-channel-row {
  cursor: pointer;
  user-select: none;
}

.composer-channel-row input[type="checkbox"] {
  width: 15px;
  height: 15px;
  margin: 0;
  flex: 0 0 auto;
  accent-color: var(--rail-accent, var(--c-brand-alt, #8b5cf6));
  cursor: pointer;
}

/* .active is set by syncComposerRail from the checkbox's own state, so
   the row's highlight cannot drift from what is ticked. */
.composer-channel-row.active {
  background: var(--rail-active-bg, color-mix(in srgb, var(--c-shadow, #000000) 5%, transparent));
  border-left-color: var(--rail-accent, var(--c-brand-alt, #8b5cf6));
}

.composer-channel-all {
  align-self: flex-start;
  margin: 0.4rem 0 0
           calc(var(--rail-row-pad-x) + var(--rail-accent-width));
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--c-brand, #6366f1);
  cursor: pointer;
  text-decoration: underline;
}

/* A count beside a rail entry -- how many campaigns are in the list
   without opening it. Sits inside the button, so it inherits the row's
   hover and active states rather than fighting them. */
/* ⚠️ ONE FLEX ITEM, so the whitespace inside it survives. A rail label
   containing an inline slot -- a brand name, the assistant's name -- MUST
   be wrapped in this; unwrapped, flexbox discards the space between the
   slot and the word after it. t-194 asserts no bare slot is a direct
   child of .ws-rail-btn. */
.ws-rail-label { display: inline; min-width: 0; }

.rail-count {
  margin-left: auto;
  background: color-mix(in srgb, var(--c-shadow, #000000) 8%, transparent);
  border-radius: 2rem;
  padding: 0.05rem 0.4rem;
  font-size: 0.7rem;
  font-weight: 700;
  color: var(--c-text-3, #475569);
}

.rail-count:empty { display: none; }

/* ── Composer: picking contacts by hand ───────────────────────────── */

.recipient-manual {
  margin-top: 0.6rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
  overflow: hidden;
}

.recipient-manual-bar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.6rem;
  background: var(--c-surface-subtle, #f8fafc);
  border-bottom: 1px solid var(--c-border, #e2e8f0);
}

.recipient-manual-bar input[type="search"] {
  flex: 1;
  min-width: 0;
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 0.35rem;
  font-size: 0.82rem;
}

.recipient-manual-bar .muted { font-size: 0.74rem; white-space: nowrap; }

/* Bounded, and scrolls. An unbounded list pushes the message box and the
   Send button off the screen on any real contact list. */
.recipient-manual-list {
  max-height: 16rem;
  overflow-y: auto;
  padding: 0.25rem;
}

.recipient-manual-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 0.45rem;
  border-radius: 0.35rem;
  cursor: pointer;
  font-size: 0.82rem;
}

.recipient-manual-row:hover { background: var(--c-surface-subtle, #f8fafc); }

.recipient-manual-row input[type="checkbox"] {
  width: 15px;
  height: 15px;
  margin: 0;
  flex: 0 0 auto;
  accent-color: var(--c-brand, #6366f1);
  cursor: pointer;
}

.recipient-manual-name {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.recipient-manual-sub {
  margin-left: auto;
  font-size: 0.74rem;
  white-space: nowrap;
}

/* Shown, never hidden: somebody who replied STOP is still pickable here,
   because the composer is not the place to silently drop a contact --
   filterByChannel decides that per channel at send time, and on the Linxi
   channel an SMS opt-out is not a reason to exclude anyone at all. */
.recipient-manual-flag {
  flex: 0 0 auto;
  font-size: 0.62rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  color: var(--c-danger-deep, #b91c1c);
  background: var(--c-danger-tint, #fef2f2);
  border: 1px solid var(--c-danger-border, #fecaca);
  border-radius: 0.25rem;
  padding: 0 0.25rem;
}

/* ── Composer: audience chip rows ─────────────────────────────────── */
/*
   Deliberately reuses .filter-chip / .filter-chip-row from the Linx tab's
   Search & Filter panel rather than defining a parallel look. A chip that
   means "include these people" should look like the chip that means
   "show me these people", because it is the same act.
*/
.recipient-filters {
  margin-top: 0.6rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.recipient-filter-block {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.recipient-filter-label {
  flex: 0 0 5.5rem;
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--c-text-muted, #64748b);
}

.recipient-filter-block .filter-chip-row {
  flex: 1;
  min-width: 0;
  margin: 0;
}

.recipient-filter-note { font-size: 0.74rem; }

/* ── Composer: the audience picker's two columns ─────────────────────
   Filters on the left, who they leave on the right. Both are needed at
   the same time -- the whole point is watching the list narrow as chips
   go on -- which is why the people list is no longer behind a button.

   1fr 1.2fr, not 1fr 1fr: the left column holds chips that wrap to their
   own width, the right holds a scrolling list of names with a search box
   above it, and the names are the thing being read. */
.recipient-picker-cols {
  display: grid;
  grid-template-columns: 1fr 1.2fr;
  gap: 1rem;
  align-items: start;
}
.recipient-picker-left,
.recipient-picker-right {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  /* Grid items will not shrink below their content otherwise, and a long
     tag name would push the whole row wider than the card. */
  min-width: 0;
}
/* Stacked in the left column, the label above its chips reads better
   than the baseline-aligned row it uses at full width. */
.recipient-picker-left .recipient-filter-block { flex-direction: column; align-items: stretch; }
.recipient-picker-left .recipient-filter-label { flex: none; }
.recipient-picker-right .recipient-manual { margin-top: 0; }
/* The list is the tall thing on the right; let it use the height the
   chips on the left are not using. */
.recipient-picker-right .recipient-manual-list { max-height: 20rem; }

.recipient-manual-scope {
  font-size: 0.72rem;
  line-height: 1.45;
  margin: 0;
  padding: 0.4rem 0.6rem;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc);
}
.recipient-manual-scope.hidden { display: none; }

@media (max-width: 900px) {
  /* One column, filters first -- you pick what to narrow by before you
     read who is left. The picker stays open here too; "always available"
     was the request on both sizes. */
  .recipient-picker-cols { grid-template-columns: 1fr; }
  .recipient-picker-left .recipient-filter-block {
    flex-direction: row;
    align-items: baseline;
  }
  .recipient-picker-left .recipient-filter-label { flex: 0 0 5.5rem; }
  .recipient-picker-right .recipient-manual-list { max-height: 14rem; }
}

#recipient-selection-summary { font-size: 0.76rem; font-weight: 600; }

/* ── AI bubble: the opening screen's smart buttons ─────────────────── */
/*
   Classes, not inline styles. The bubble has a dark palette of its own
   and these have to live inside it -- a block of hard-coded hex in the
   JS is a block that stops matching the moment the panel is restyled.
*/
.ai-smart-buttons {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  margin-top: 0.15rem;
}

.ai-smart-btn {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  width: 100%;
  text-align: left;
  padding: 0.6rem 0.7rem;
  border-radius: 0.55rem;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface, #ffffff);
  color: inherit;
  font: inherit;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, opacity 0.15s;
}

.ai-smart-btn:hover:not(:disabled) { background: var(--c-surface-subtle, #f8fafc); border-color: var(--c-brand, #6366f1); }

/* Spent, not hidden. The button that was pressed stays on screen as a
   record of what was asked -- removing it would leave the answer with no
   question above it. */
.ai-smart-btn:disabled { cursor: default; }
.ai-smart-spent { opacity: 0.4; }

.ai-smart-icon {
  flex: 0 0 30px;
  height: 30px;
  border-radius: 0.4rem;
  background: color-mix(in srgb, var(--c-highlight, #ffffff) 6%, transparent);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 0.95rem;
}

.ai-smart-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }

.ai-smart-label {
  font-weight: 700;
  font-size: 0.79rem;
  color: var(--c-text, #0f172a);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ai-smart-sub {
  font-size: 0.68rem;
  color: var(--c-text-dim, #94a3b8);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ai-smart-chev { flex: 0 0 auto; color: var(--c-text-3, #475569); font-size: 0.8rem; }

/* Severity carries in the border and the label, not a filled background:
   five solid colour blocks in a small dark panel is noise, and the point
   is that the top one stands out from the rest. */
.ai-smart-urgent { border-color: color-mix(in srgb, var(--c-danger, #ef4444) 45%, var(--c-surface, #ffffff)); background: color-mix(in srgb, var(--c-danger, #ef4444) 12%, var(--c-surface, #ffffff)); }
.ai-smart-urgent .ai-smart-label { color: var(--c-danger, #ef4444); }
.ai-smart-urgent:hover:not(:disabled) { background: color-mix(in srgb, var(--c-danger, #ef4444) 22%, var(--c-surface, #ffffff)); }

.ai-smart-warning { border-color: color-mix(in srgb, var(--c-warning, #f59e0b) 45%, var(--c-surface, #ffffff)); background: color-mix(in srgb, var(--c-warning, #f59e0b) 12%, var(--c-surface, #ffffff)); }
.ai-smart-warning .ai-smart-label { color: var(--c-warning-strong, #d97706); }
.ai-smart-warning:hover:not(:disabled) { background: color-mix(in srgb, var(--c-warning, #f59e0b) 22%, var(--c-surface, #ffffff)); }

.ai-smart-info { border-color: color-mix(in srgb, var(--c-info, #2563eb) 45%, var(--c-surface, #ffffff)); background: color-mix(in srgb, var(--c-info, #2563eb) 12%, var(--c-surface, #ffffff)); }
.ai-smart-info .ai-smart-label { color: var(--c-info, #2563eb); }
.ai-smart-info:hover:not(:disabled) { background: color-mix(in srgb, var(--c-info, #2563eb) 22%, var(--c-surface, #ffffff)); }

.ai-greeting-loading { opacity: 0.6; font-style: italic; }

/* A field the assistant just wrote into.
   A class with an animation, not inline styles: setting
   el.style.borderColor and restoring it from a captured value loses
   whatever the stylesheet was doing for :focus or validity, and the
   restore runs 800ms later against a field the user may have clicked
   into by then. */
@keyframes field-loaded-flash {
  0%   { border-color: var(--c-success-alt, #10b981); background: var(--c-success-tint, #f0fdf4); }
  70%  { border-color: var(--c-success-alt, #10b981); background: var(--c-success-tint, #f0fdf4); }
  100% { border-color: inherit; background: inherit; }
}

.field-loaded {
  animation: field-loaded-flash 1.2s ease-out;
}

/* Respect a system-level request for less motion. The flash is the only
   signal that a field changed, so it becomes an instant tint rather than
   disappearing entirely. */
@media (prefers-reduced-motion: reduce) {
  .field-loaded { animation: none; box-shadow: 0 0 0 2px var(--c-success-alt, #10b981); }
}

/* ═══════════════════════════════════════════════════════════════════════
   MARKETING CALENDAR

   The rail, the panels and the modals come from their own modules above
   -- this is only what is specific to the calendar: the month grid, the
   plan cards, the list rows and the AI planner drawer.

   Colours that vary per row arrive as the --chip / --row / --plan / --type
   custom properties, set inline by calendar.js from a value it has
   already validated as #rrggbb. A colour cannot be made safe by HTML
   escaping -- it is a CSS context -- so the validation is the control and
   these rules just consume it.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── rail extras ─────────────────────────────────────────────────────── */

.cal-rail-fields { padding: 0 var(--rail-row-pad-x); display: grid; gap: 0.6rem; }

.cal-rail-field { display: grid; gap: 0.2rem; }

.cal-rail-field > span {
  font-size: var(--rail-group-size);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--rail-group-text);
}

.cal-rail-field select {
  width: 100%;
  padding: 0.4rem 0.5rem;
  font-size: 0.82rem;
  border: 1px solid var(--rail-divider);
  border-radius: 0.45rem;
  background: var(--c-surface, #fff);
  color: var(--rail-row-text);
}

/* ── month grid ──────────────────────────────────────────────────────── */

.cal-grid-wrap {
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.75rem;
  overflow: hidden;
  margin-top: 1rem;
}

.cal-weekdays {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  background: var(--c-surface-subtle, #f8fafc);
  border-bottom: 1px solid var(--c-border, #e2e8f0);
}

.cal-weekdays span {
  padding: 0.5rem 0.35rem;
  text-align: center;
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--c-text-dim, #94a3b8);
}

/* minmax(0, 1fr) on the columns, not 1fr: a grid track defaults to
   min-width auto and one long event title would otherwise push the whole
   week wider than the card. */
.cal-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }

.cal-cell {
  min-height: 108px;
  padding: 0.3rem;
  border-right: 1px solid var(--c-surface-sunken, #f1f5f9);
  border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9);
  background: var(--c-surface, #fff);
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 0;
  transition: background 0.12s;
}

.cal-cell:nth-child(7n) { border-right: none; }
.cal-cell:nth-last-child(-n + 7) { border-bottom: none; }
.cal-cell:hover { background: var(--c-surface-subtle, #f8fafc); }
.cal-cell:focus-visible { outline: 2px solid var(--rail-accent); outline-offset: -2px; }
.cal-cell-outside { background: var(--c-surface-subtle, #f8fafc); }
.cal-cell-outside .cal-day-num { color: var(--c-text-dim, #94a3b8); }
.cal-cell-today { background: var(--c-brand-tint, #f5f3ff); }
.cal-cell-today:hover { background: var(--c-violet-tint-2, #ede9fe); }

.cal-day-num {
  font-size: 0.74rem;
  font-weight: 700;
  color: var(--c-text, #0f172a);
  align-self: flex-start;
  min-width: 1.35rem;
  height: 1.35rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.cal-cell-today .cal-day-num {
  background: var(--rail-accent);
  color: var(--c-on-accent, #ffffff);
  border-radius: 50%;
  font-weight: 800;
}

.cal-chip {
  display: block;
  width: 100%;
  text-align: left;
  font: inherit;
  font-size: 0.68rem;
  font-weight: 600;
  line-height: 1.3;
  padding: 0.16rem 0.32rem;
  border: none;
  border-left: 3px solid var(--chip, var(--c-brand, #6366f1));
  border-radius: 0 0.25rem 0.25rem 0;
  background: color-mix(in srgb, var(--chip, var(--c-brand, #6366f1)) 14%, var(--c-surface, #fff));
  color: color-mix(in srgb, var(--chip, var(--c-brand, #6366f1)) 78%, var(--c-text, #0f172a));
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.cal-chip:hover { background: color-mix(in srgb, var(--chip, var(--c-brand, #6366f1)) 24%, var(--c-surface, #fff)); }
.cal-chip-done { opacity: 0.55; text-decoration: line-through; }

.cal-more { font-size: 0.64rem; color: var(--c-text-dim, #94a3b8); text-align: right; padding-right: 0.2rem; }

.cal-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 0.9rem;
  margin-top: 0.85rem;
  font-size: 0.7rem;
  color: var(--c-text-muted, #64748b);
}

.cal-legend-item { display: inline-flex; align-items: center; gap: 0.3rem; }
.cal-legend-item i { width: 9px; height: 9px; border-radius: 2px; display: inline-block; }

/* ── toolbar ─────────────────────────────────────────────────────────── */

.cal-nav { display: inline-flex; align-items: center; gap: 0.25rem; }

.cal-nav-btn {
  background: var(--c-surface, #fff);
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.4rem;
  padding: 0.15rem 0.55rem;
  font-size: 1rem;
  line-height: 1.4;
  color: var(--c-text-muted, #64748b);
  cursor: pointer;
}

.cal-nav-btn:hover { border-color: var(--rail-accent); color: var(--rail-accent); }

.cal-period {
  font-weight: 700;
  font-size: 0.85rem;
  color: var(--c-text, #0f172a);
  min-width: 150px;
  text-align: center;
}

/* ── plan cards ──────────────────────────────────────────────────────── */

.cal-plan {
  border: 1px solid var(--c-border, #e2e8f0);
  border-left: 4px solid var(--plan, var(--c-brand, #6366f1));
  border-radius: 0.8rem;
  padding: 1.1rem 1.25rem;
  margin-bottom: 0.9rem;
}

.cal-plan-head { display: flex; justify-content: space-between; gap: 1rem; align-items: flex-start; }
.cal-plan-head h3 { margin: 0 0 0.1rem; font-size: 0.98rem; font-weight: 800; }
.cal-plan-head p { margin: 0; font-size: 0.74rem; }
.cal-plan-actions { display: flex; gap: 0.35rem; flex-shrink: 0; }
.cal-plan-actions button { font-size: 0.74rem; padding: 0.25rem 0.55rem; }

.cal-plan-status {
  font-size: 0.63rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 0.1rem 0.45rem;
  border-radius: 2rem;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc);
  color: var(--c-text-muted, #64748b);
  vertical-align: middle;
}

.cal-plan-status-active { background: var(--c-success-tint, #f0fdf4); color: var(--c-success-text, #166534); border-color: var(--c-success-border, #bbf7d0); }
.cal-plan-status-completed { background: var(--c-info-tint, #eff6ff); color: var(--c-info-text, #1d4ed8); border-color: var(--c-info-border, #bfdbfe); }
.cal-plan-status-paused { background: var(--c-warning-tint, #fffbeb); color: var(--c-warning-text, #92400e); border-color: var(--c-warning-border, #fde68a); }

.cal-plan-goal { font-size: 0.8rem; color: var(--c-text-3, #475569); margin: 0.6rem 0 0.8rem; }

.cal-bar-row {
  display: flex;
  justify-content: space-between;
  font-size: 0.68rem;
  color: var(--c-text-dim, #94a3b8);
  margin: 0.5rem 0 0.2rem;
}

.cal-bar { background: var(--c-surface-sunken, #f1f5f9); border-radius: 2rem; height: 6px; overflow: hidden; }
.cal-bar-fill { height: 6px; border-radius: 2rem; transition: width 0.3s; }
.cal-bar-time { background: var(--c-border-strong, #cbd5e1); }
.cal-bar-plan { background: var(--plan, var(--c-brand, #6366f1)); }

.cal-plan-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 1rem;
  font-size: 0.73rem;
  color: var(--c-text-muted, #64748b);
  margin: 0.85rem 0 0;
}

.cal-plan-actual { color: var(--c-success-text, #166534); font-weight: 700; }

/* ── list view ───────────────────────────────────────────────────────── */

.cal-week { margin-bottom: 1.4rem; }

.cal-week h4 {
  font-size: 0.7rem;
  font-weight: 800;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--c-text-dim, #94a3b8);
  margin: 0 0 0.5rem;
  padding-bottom: 0.35rem;
  border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9);
}

.cal-row {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  width: 100%;
  text-align: left;
  font: inherit;
  padding: 0.55rem 0.7rem;
  margin-bottom: 0.3rem;
  border: 1px solid var(--c-surface-sunken, #f1f5f9);
  border-radius: 0.5rem;
  background: var(--c-surface, #fff);
  cursor: pointer;
  transition: border-color 0.12s, background 0.12s;
}

.cal-row:hover {
  border-color: var(--row, var(--c-brand, #6366f1));
  background: color-mix(in srgb, var(--row, var(--c-brand, #6366f1)) 6%, var(--c-surface, #fff));
}

.cal-row-icon {
  width: 30px;
  height: 30px;
  flex-shrink: 0;
  border-radius: 0.4rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--row, var(--c-brand, #6366f1)) 16%, var(--c-surface, #fff));
}

.cal-row-main { flex: 1; min-width: 0; display: grid; }
.cal-row-title { font-size: 0.82rem; font-weight: 700; color: var(--c-text, #0f172a); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cal-row-done { text-decoration: line-through; opacity: 0.6; }
.cal-row-sub { font-size: 0.7rem; color: var(--c-text-dim, #94a3b8); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.cal-row-status {
  font-size: 0.65rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--c-text-dim, #94a3b8);
  flex-shrink: 0;
}

.cal-status-completed { color: var(--c-success-text, #166534); }
.cal-status-overdue, .cal-status-missed { color: var(--c-danger-strong, #dc2626); }
.cal-status-in_progress { color: var(--c-warning-text, #92400e); }

.cal-empty { text-align: center; padding: 2.5rem 1rem; }
.cal-empty p { margin: 0 0 0.4rem; }
.cal-empty .table-toolbar { justify-content: center; margin-top: 1rem; }

/* ── modals ──────────────────────────────────────────────────────────── */

.cal-modal { max-width: 520px; }

.cal-type-picker { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-bottom: 0.9rem; }

.cal-type-btn {
  font: inherit;
  font-size: 0.72rem;
  font-weight: 600;
  padding: 0.28rem 0.55rem;
  border-radius: 2rem;
  border: 2px solid var(--c-border, #e2e8f0);
  background: var(--c-surface, #fff);
  color: var(--c-text-muted, #64748b);
  cursor: pointer;
  transition: all 0.12s;
}

.cal-type-btn:hover { border-color: var(--type, var(--c-brand, #6366f1)); color: var(--type, var(--c-brand, #6366f1)); }

.cal-type-btn.active {
  border-color: var(--type, var(--c-brand, #6366f1));
  color: var(--type, var(--c-brand, #6366f1));
  background: color-mix(in srgb, var(--type, var(--c-brand, #6366f1)) 12%, var(--c-surface, #fff));
}

.cal-goal-fields {
  background: var(--c-warning-tint, #fffbeb);
  border: 1px solid var(--c-warning-border, #fde68a);
  border-radius: 0.6rem;
  padding: 0.8rem;
  margin: 0.4rem 0 0.9rem;
}

.cal-goal-fields > strong { display: block; font-size: 0.82rem; color: var(--c-warning-text, #92400e); margin-bottom: 0.4rem; }

.cal-color-picker { display: flex; flex-wrap: wrap; gap: 0.45rem; margin-bottom: 1rem; }

.cal-color {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 2px solid transparent;
  cursor: pointer;
  padding: 0;
}

.cal-color.active { border-color: var(--c-inverse-surface, #0f172a); box-shadow: 0 0 0 2px var(--c-surface, #fff) inset; }

.cal-linklike {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: var(--rail-active-text, var(--c-brand, #6366f1));
  text-decoration: underline;
  cursor: pointer;
}

/* ── AI planner drawer ───────────────────────────────────────────────── */

/* REQ-0025. ONE rule for both AI drawers -- the calendar planner and the
   composer's assistant. The composer's used to be a card sitting in the
   form flow, so the two behaved differently on pages that do the same
   thing. Sharing the geometry is also what stops the width drifting
   apart the next time one of them is adjusted.

   480px, up from 380: the panel holds a conversation plus draft cards
   with buttons, and at 380 the cards wrapped onto three lines each. The
   min() keeps it from overflowing a narrow window. */
.cal-ai-drawer,
.campaign-ai-panel {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(480px, 100vw);
  z-index: 900;
  background: var(--c-surface, #fff);
  border-left: 1px solid var(--c-border, #e2e8f0);
  box-shadow: -12px 0 32px color-mix(in srgb, var(--c-scrim, #0f172a) 10%, transparent);
  display: flex;
  flex-direction: column;
  margin: 0;
}

.cal-ai-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.8rem 1rem;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
}

.cal-ai-id { display: flex; align-items: center; gap: 0.55rem; min-width: 0; }
.cal-ai-id strong { display: block; font-size: 0.85rem; }
.cal-ai-id small { display: block; font-size: 0.66rem; color: var(--rail-active-text, var(--c-brand, #6366f1)); }

.cal-ai-avatar {
  width: 30px;
  height: 30px;
  flex-shrink: 0;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(135deg, var(--c-brand, #6366f1), var(--c-brand-alt, #8b5cf6)); color: var(--c-on-status, #0f172a);
}

.cal-ai-x { background: none; border: none; font-size: 1.2rem; line-height: 1; color: var(--c-text-dim, #94a3b8); cursor: pointer; }

.cal-ai-context { padding: 0.7rem 1rem; border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9); }
.cal-ai-context label { font-size: 0.68rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--c-text-dim, #94a3b8); display: block; margin-bottom: 0.25rem; }
.cal-ai-context select { width: 100%; padding: 0.45rem 0.5rem; font-size: 0.82rem; border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.4rem; }

.cal-ai-messages {
  flex: 1;
  overflow-y: auto;
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.cal-ai-msg { font-size: 0.82rem; line-height: 1.55; }

.cal-ai-user { text-align: right; }
.cal-ai-user span {
  display: inline-block;
  max-width: 88%;
  text-align: left;
  padding: 0.45rem 0.7rem;
  border-radius: 0.7rem 0.7rem 0 0.7rem;
  background: var(--rail-accent, var(--c-brand-alt, #8b5cf6));
  color: var(--c-on-fill, #ffffff);
  overflow-wrap: anywhere;
}

.cal-ai-assistant > div {
  max-width: 94%;
  padding: 0.55rem 0.8rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.7rem 0.7rem 0.7rem 0;
  background: var(--c-surface-subtle, #f8fafc);
  color: var(--c-text, #0f172a);
  overflow-wrap: anywhere;
}

.cal-ai-bullet { display: block; padding-left: 0.2rem; }

.cal-ai-system { text-align: center; }
.cal-ai-system span {
  display: inline-block;
  font-size: 0.7rem;
  font-weight: 700;
  padding: 0.2rem 0.65rem;
  border-radius: 2rem;
  background: var(--c-success-tint, #f0fdf4);
  border: 1px solid var(--c-success-border, #bbf7d0);
  color: var(--c-success-text, #166534);
}

.cal-ai-dots { display: inline-flex; gap: 0.25rem; align-items: center; }
.cal-ai-dots i { width: 5px; height: 5px; border-radius: 50%; background: var(--c-text-dim, #94a3b8); animation: calDot 0.9s infinite; }
.cal-ai-dots i:nth-child(2) { animation-delay: 0.15s; }
.cal-ai-dots i:nth-child(3) { animation-delay: 0.3s; }

@keyframes calDot {
  0%, 60%, 100% { opacity: 0.3; transform: translateY(0); }
  30% { opacity: 1; transform: translateY(-3px); }
}

/* Respected rather than assumed: a bouncing indicator is exactly the
   kind of motion a vestibular-disorder setting is asking us to stop. */
@media (prefers-reduced-motion: reduce) {
  .cal-ai-dots i { animation: none; opacity: 0.6; }
  .cal-bar-fill { transition: none; }
}

.cal-ai-quick { display: flex; flex-wrap: wrap; gap: 0.3rem; padding: 0.55rem 1rem; border-top: 1px solid var(--c-surface-sunken, #f1f5f9); }

.cal-quick-btn {
  font: inherit;
  font-size: 0.7rem;
  font-weight: 600;
  padding: 0.2rem 0.55rem;
  border-radius: 2rem;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc);
  color: var(--c-text-3, #475569);
  cursor: pointer;
  white-space: nowrap;
}

.cal-quick-btn:hover { border-color: var(--rail-accent, var(--c-brand-alt, #8b5cf6)); color: var(--rail-active-text, var(--c-brand, #6366f1)); background: var(--c-brand-tint, #f5f3ff); }

.cal-ai-input { display: flex; gap: 0.45rem; align-items: flex-end; padding: 0.7rem 1rem 0.9rem; border-top: 1px solid var(--c-border, #e2e8f0); }

.cal-ai-input textarea {
  flex: 1;
  padding: 0.5rem;
  font: inherit;
  font-size: 0.82rem;
  line-height: 1.4;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
  resize: none;
}

.cal-ai-input textarea:focus { outline: none; border-color: var(--rail-accent, var(--c-brand-alt, #8b5cf6)); }

.cal-ai-input button {
  width: 34px;
  height: 34px;
  flex-shrink: 0;
  padding: 0;
  border: none;
  border-radius: 0.5rem;
  background: var(--rail-accent, var(--c-brand-alt, #8b5cf6));
  color: var(--c-on-fill, #ffffff);
  cursor: pointer;
}

/* The rail already stacks under --rail-mobile-max; the drawer becomes a
   full-width sheet at the same breakpoint rather than a 380px column on
   a 360px screen. */
@media (max-width: 900px) {
  .cal-ai-drawer,
  .campaign-ai-panel { width: 100vw; border-left: none; }
  .cal-cell { min-height: 76px; }
  .cal-chip { font-size: 0.62rem; }
  .cal-period { min-width: 110px; font-size: 0.78rem; }
}

/* ═══════════════════════════════════════════════════════════════════════
   CAMPAIGN DIRECTIVES + AFTER SEND

   Both sections are native <details> wearing .composer-card, so the
   collapsing is the browser's and there is no open/closed state in JS to
   fall out of step with the arrow.
   ═══════════════════════════════════════════════════════════════════════ */

.directive-card > summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

/* Safari draws its own triangle through ::-webkit-details-marker rather
   than list-style, so both have to be removed or the row gets two. */
.directive-card > summary::-webkit-details-marker { display: none; }

/* The disclosure arrow, drawn here so it can rotate with the element's
   own [open] state -- no class to toggle, nothing to keep in sync. */
.directive-card > summary::after {
  content: '▾';
  margin-left: auto;
  color: var(--c-text-dim, #94a3b8);
  font-size: 0.8rem;
  transition: transform 0.15s;
}

.directive-card[open] > summary::after { transform: rotate(180deg); }
.directive-card > summary:hover { background: color-mix(in srgb, var(--c-shadow, #000000) 2%, transparent); }
.directive-card > summary:focus-visible { outline: 2px solid var(--rail-accent, var(--c-brand-alt, #8b5cf6)); outline-offset: -2px; }

.directive-sub {
  font-size: 0.74rem;
  color: var(--c-text-muted, #64748b);
  font-weight: 400;
  flex: 1 1 100%;
  order: 3;
}

.directive-badge {
  font-size: 0.63rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 0.1rem 0.45rem;
  border-radius: 2rem;
  background: var(--c-success-tint, #f0fdf4);
  color: var(--c-success-text, #166534);
  border: 1px solid var(--c-success-border, #bbf7d0);
}

.directive-badge-warn { background: var(--c-warning-tint, #fffbeb); color: var(--c-warning-text, #92400e); border-color: var(--c-warning-border, #fde68a); }

.directive-explainer { margin-top: 0; }

/* REQ-0026: the question on its own line, the two buttons inline on the
   next. As one wrapping flex row the label and the buttons reflowed into
   whatever the width allowed, so the prompt and its answers ended up on
   the same line at one size and scattered at another. */
.directive-upload {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.45rem;
  padding: 0.7rem 0.8rem;
  margin-top: 0.75rem;
  border: 1px dashed var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
}
.directive-upload-buttons { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }

/* REQ-0027: the three ways to add a contact, inline and beneath the
   heading rather than sharing a flex row with it. */
.contacts-add-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-top: 0.6rem;
}

.directive-upload-note { font-size: 0.7rem; }

.directive-prompts {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  flex-wrap: wrap;
  margin-top: 0.6rem;
}

.directive-prompt-btn {
  font: inherit;
  font-size: 0.7rem;
  font-weight: 600;
  padding: 0.2rem 0.55rem;
  border-radius: 2rem;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc);
  color: var(--c-text-3, #475569);
  cursor: pointer;
  white-space: nowrap;
}

.directive-prompt-btn:hover:not(:disabled) {
  border-color: var(--c-warning, #f59e0b);
  color: var(--c-warning-text, #92400e);
  background: var(--c-warning-tint, #fffbeb);
}

.directive-prompt-btn:disabled { opacity: 0.5; cursor: default; }

/* ── after send ──────────────────────────────────────────────────────── */

.after-send-enable {
  align-items: flex-start;
  gap: 0.6rem;
  padding: 0.8rem;
  background: var(--c-warning-tint, #fffbeb);
  border: 1px solid var(--c-warning-border, #fde68a);
  border-radius: 0.5rem;
}

.after-send-enable > span { display: grid; gap: 0.15rem; }
.after-send-enable strong { font-size: 0.85rem; }
.after-send-enable .muted { font-size: 0.75rem; line-height: 1.5; }

/* Louder than the generic warning banner: this one is about a robot
   texting people, and it sits above the settings that arm it. */
.after-send-warning {
  margin: 0.85rem 0;
  font-size: 0.78rem;
  line-height: 1.55;
}

.after-send-numbers { gap: 1.25rem; flex-wrap: wrap; }

.after-send-number {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.8rem;
  color: var(--c-text, #0f172a);
}

.after-send-escalate { margin: 0.75rem 0; }

/* Off, and looking off. The channel exists as a column and a checkbox so
   it is ready, but there is no inbound-email webhook for it to hang off
   -- see the note beside it. */
.after-send-email-option { opacity: 0.55; cursor: not-allowed; }
.after-send-email-option input { cursor: not-allowed; }

#after-send-email-note { font-size: 0.72rem; }

/* ── after-send status on a campaign row ─────────────────────────────── */

.after-send-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  font-size: 0.65rem;
  font-weight: 700;
  padding: 0.08rem 0.4rem;
  border-radius: 2rem;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc);
  color: var(--c-text-muted, #64748b);
  white-space: nowrap;
}

.after-send-chip-active { background: var(--c-success-tint, #f0fdf4); color: var(--c-success-text, #166534); border-color: var(--c-success-border, #bbf7d0); }
.after-send-chip-paused { background: var(--c-warning-tint, #fffbeb); color: var(--c-warning-text, #92400e); border-color: var(--c-warning-border, #fde68a); }
.after-send-chip-completed { background: var(--c-info-tint, #eff6ff); color: var(--c-info-text, #1d4ed8); border-color: var(--c-info-border, #bfdbfe); }

/* ═══════════════════════════════════════════════════════════════════════
   INTEGRATIONS — Google
   ═══════════════════════════════════════════════════════════════════════ */

.gws-card {
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.875rem;
  overflow: hidden;
  margin-bottom: 1.25rem;
}

.gws-head {
  display: flex;
  align-items: center;
  gap: 0.875rem;
  padding: 1rem 1.25rem;
  border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9);
}

.gws-logo {
  width: 40px;
  height: 40px;
  flex-shrink: 0;
  border-radius: 0.5rem;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface, #fff);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.gws-title { display: grid; gap: 0.1rem; min-width: 0; }
.gws-title strong { font-size: 0.95rem; }
.gws-title .muted { font-size: 0.75rem; }
.gws-head-actions { margin-left: auto; display: flex; align-items: center; gap: 0.5rem; }

.gws-connect-btn {
  /* Google brand blue -- deliberately NOT a token; see the note in CLAUDE.md */
  background: #4285f4;
  color: var(--c-on-accent, #ffffff);
  text-decoration: none;
  padding: 0.5rem 1rem;
  border-radius: 0.5rem;
  font-size: 0.82rem;
  font-weight: 700;
  white-space: nowrap;
}

/* Google brand blue, pressed -- deliberately not a token. */
.gws-connect-btn:hover { background: #3367d6; }

.gws-chip {
  font-size: 0.65rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 0.1rem 0.45rem;
  border-radius: 2rem;
  background: var(--c-success-tint, #f0fdf4);
  color: var(--c-success-text, #166534);
  border: 1px solid var(--c-success-border, #bbf7d0);
}

.gws-body { padding: 1.25rem; }
.gws-scopes-note { margin-bottom: 0; }

.gws-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 0.75rem;
}

.gws-service {
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.65rem;
  padding: 0.875rem;
}

.gws-service > p { font-size: 0.74rem; margin: 0.3rem 0 0; }

.gws-service-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  cursor: pointer;
}

.gws-service-name { font-weight: 700; font-size: 0.85rem; }

/* A service the user switched on that Google never granted. Bordered
   rather than hidden: the switch is still theirs, it just cannot work
   until they reconnect. */
.gws-service-ungranted { border-color: var(--c-warning-border, #fde68a); background: var(--c-warning-tint, #fffbeb); }

.gws-ungranted-note {
  font-size: 0.72rem;
  color: var(--c-warning-text, #92400e);
  margin: 0.4rem 0 0;
  font-weight: 600;
}

.gws-service-options {
  margin-top: 0.7rem;
  padding-top: 0.7rem;
  border-top: 1px solid var(--c-surface-sunken, #f1f5f9);
  display: grid;
  gap: 0.45rem;
}

.gws-field { display: grid; gap: 0.2rem; font-size: 0.74rem; }
.gws-field > span { font-weight: 600; color: var(--c-text-muted, #64748b); }

.gws-field select,
.gws-field input {
  width: 100%;
  padding: 0.4rem 0.5rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.4rem;
  font-size: 0.8rem;
}

.gws-service-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-top: 0.2rem;
}

.gws-service-actions button { font-size: 0.74rem; padding: 0.25rem 0.6rem; }
.gws-service-actions .muted { font-size: 0.68rem; }
.gws-warning { font-size: 0.74rem; margin: 0 0 0.3rem; }
#google-gmail-note { font-size: 0.7rem; margin: 0.3rem 0 0; }

.gws-files { margin-top: 1rem; display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; }

.gws-files-title {
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--c-text-dim, #94a3b8);
  width: 100%;
}

.gws-file {
  font-size: 0.74rem;
  padding: 0.2rem 0.55rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 2rem;
  color: var(--c-text-3, #475569);
  text-decoration: none;
  max-width: 260px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Google brand blue -- deliberately not a token. */
.gws-file:hover { border-color: #4285f4; color: var(--c-info, #2563eb); }

.gws-footer {
  margin-top: 1.1rem;
  padding-top: 0.875rem;
  border-top: 1px solid var(--c-surface-sunken, #f1f5f9);
  display: flex;
  justify-content: flex-end;
}

.gws-card-soon {
  padding: 1.1rem;
  text-align: center;
  border-style: dashed;
  display: grid;
  gap: 0.2rem;
}

.gws-card-soon .muted { font-size: 0.75rem; }

/* The G mark on a calendar chip that came from Google. */
.cal-chip-google {
  display: inline-block;
  /* Google brand blue -- deliberately NOT a token; see the note in CLAUDE.md */
  background: #4285f4;
  color: var(--c-on-accent, #ffffff);
  font-size: 0.55rem;
  font-weight: 800;
  line-height: 1.4;
  padding: 0 0.22rem;
  border-radius: 0.15rem;
  margin-right: 0.2rem;
  vertical-align: middle;
}

/* Dashed, because it is not ours to edit -- clicking leaves for Google. */
.cal-chip-external { border-left-style: dashed; }

.cal-legend-item .cal-chip-google { margin-right: 0.3rem; }

/* ── planning AI: the depth chooser ──────────────────────────────────── */

.cal-tier-dialog {
  border: 1px solid var(--rail-accent, var(--c-brand-alt, #8b5cf6));
  border-radius: 0.75rem;
  padding: 0.8rem;
  background: var(--c-surface, #fff);
  display: grid;
  gap: 0.4rem;
}

.cal-tier-summary {
  margin: 0 0 0.2rem;
  font-size: 0.8rem;
  line-height: 1.5;
  color: var(--c-text, #0f172a);
}

.cal-tier-heading {
  font-size: 0.65rem;
  font-weight: 800;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--c-text-dim, #94a3b8);
}

.cal-tier-btn {
  display: grid;
  gap: 0.15rem;
  width: 100%;
  text-align: left;
  font: inherit;
  padding: 0.5rem 0.65rem;
  border: 1.5px solid var(--c-border, #e2e8f0);
  border-radius: 0.55rem;
  background: var(--c-surface, #fff);
  cursor: pointer;
  transition: border-color 0.12s, background 0.12s;
}

.cal-tier-btn:hover:not(:disabled) {
  border-color: var(--rail-accent, var(--c-brand-alt, #8b5cf6));
  background: var(--c-violet-tint, #faf9ff);
}

.cal-tier-btn:disabled { opacity: 0.5; cursor: default; }

.cal-tier-recommended {
  border-color: var(--rail-accent, var(--c-brand-alt, #8b5cf6));
  background: var(--c-violet-tint, #faf9ff);
}

.cal-tier-btn-head {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
}

.cal-tier-btn-head strong { font-size: 0.82rem; }

.cal-tier-tag {
  font-size: 0.6rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 0.05rem 0.35rem;
  border-radius: 2rem;
  background: var(--rail-accent, var(--c-brand-alt, #8b5cf6));
  color: var(--c-on-fill, #ffffff);
}

.cal-tier-cost {
  margin-left: auto;
  font-size: 0.82rem;
  font-weight: 800;
  color: var(--rail-active-text, var(--c-brand, #6366f1));
}

/* Under the name now, not squeezed onto the same row as the price --
   the price is what the row is for. */
.cal-tier-count { font-size: 0.68rem; color: var(--c-text-dim, #94a3b8); }
.cal-tier-blurb { font-size: 0.72rem; color: var(--c-text-muted, #64748b); line-height: 1.4; }

.cal-tier-reason {
  margin: 0.1rem 0 0;
  font-size: 0.72rem;
  color: var(--rail-active-text, var(--c-brand, #6366f1));
  background: var(--c-brand-tint, #f5f3ff);
  border-radius: 0.4rem;
  padding: 0.35rem 0.5rem;
  line-height: 1.4;
}

/* The one thing people misread about a screen of size options. */
.cal-tier-price {
  margin: 0;
  font-size: 0.68rem;
  color: var(--c-text-dim, #94a3b8);
  line-height: 1.4;
}

.cal-tier-cancel { justify-self: start; font-size: 0.72rem; }

/* ── requests: Linxi AI ──────────────────────────────────────────────── */

.request-ai-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  padding: 0.55rem 0.7rem;
  margin-bottom: 0.75rem;
  border: 1px solid var(--c-violet-border, #ddd6fe);
  border-radius: 0.5rem;
  background: var(--c-violet-tint, #faf9ff);
}

.request-ai-row .muted { font-size: 0.74rem; }
.request-ai-row button { font-size: 0.76rem; padding: 0.28rem 0.6rem; }

/* A comment from the robot, marked so it cannot be mistaken for one
   from a colleague. */
.request-comment-ai {
  border-left: 3px solid var(--c-brand-alt, #8b5cf6);
  background: var(--c-violet-tint, #faf9ff);
}

.request-ai-tag {
  font-size: 0.7rem;
  font-weight: 800;
  color: var(--c-violet-text, #6d28d9);
}

/* ── the campaign name ───────────────────────────────────────────────── */
/*
   The first field in the composer, and it used to be a bare label over a
   default input -- the same weight as every other row on the page, which
   made the top of the form read as nothing in particular. It is the
   name of the thing being built, so it gets to look like one.
*/

.campaign-name-label {
  display: block;
  font-size: 0.7rem;
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--rail-active-text, var(--c-brand, #6366f1));
  margin-bottom: 0.35rem;
}

.campaign-name-input {
  /* Half the column on desktop. A campaign name is a few words, and a
     full-width box invites a sentence -- the field beneath it is the one
     that wants the room. Full width below the breakpoint, where half of
     a phone is not enough to read what you typed.

     Superseded while the input sits in .composer-top-row, which is
     already half the column: .composer-top-name overrides this to 100%,
     or the field would be a quarter of the page. Kept as the standalone
     behaviour for anywhere the input is used outside that row. */
  width: 100%;
  max-width: 50%;
  font-size: 1.05rem;
  font-weight: 700;
  padding: 0.7rem 0.85rem;
  border: 1.5px solid var(--c-border, #e2e8f0);
  border-radius: 0.6rem;
  background:
    linear-gradient(var(--c-surface, #fff), var(--c-surface, #fff)) padding-box,
    linear-gradient(120deg, var(--c-brand-tint-3, #e0e7ff), var(--c-brand-tint, #f5f3ff)) border-box;
  color: var(--c-text, #0f172a);
  transition: border-color 0.15s, box-shadow 0.15s;
}

.campaign-name-input::placeholder { font-weight: 500; color: var(--c-text-dim, #94a3b8); }

.campaign-name-input:focus {
  outline: none;
  border-color: var(--rail-accent, var(--c-brand-alt, #8b5cf6));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-brand-alt, #8b5cf6) 14%, transparent);
}

/* ═══════════════════════════════════════════════════════════════════════
   ADMIN — LINXI AI
   ═══════════════════════════════════════════════════════════════════════ */

.lai-profile {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 1rem 1.1rem;
  margin: 0.75rem 0 0.5rem;
  border-radius: 0.75rem;
  background: linear-gradient(135deg, color-mix(in srgb, var(--c-brand, #6366f1) 25%, var(--c-inverse-surface, #0f172a)), var(--c-brand-strong, #4f46e5));
  color: var(--c-on-accent, #ffffff);
}

.lai-avatar {
  width: 52px;
  height: 52px;
  flex-shrink: 0;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 1.5rem;
  background: linear-gradient(135deg, var(--c-brand, #6366f1), var(--c-brand-alt, #8b5cf6));
  border: 3px solid color-mix(in srgb, var(--c-highlight, #ffffff) 25%, transparent); color: var(--c-on-status, #0f172a);
}

.lai-identity { display: grid; gap: 0.1rem; min-width: 0; flex: 1; }
.lai-identity strong { font-size: 1rem; display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }
.lai-email { font-size: 0.76rem; color: color-mix(in srgb, var(--c-highlight, #ffffff) 75%, transparent); }
.lai-role { font-size: 0.7rem; color: color-mix(in srgb, var(--c-highlight, #ffffff) 50%, transparent); }

.lai-verified {
  font-size: 0.6rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 0.08rem 0.35rem;
  border-radius: 2rem;
  background: color-mix(in srgb, var(--c-highlight, #ffffff) 20%, transparent);
}

.lai-master {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.78rem;
  font-weight: 600;
  flex-shrink: 0;
}

.lai-state {
  font-size: 0.65rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 0.1rem 0.45rem;
  border-radius: 2rem;
  background: var(--c-success-tint, #f0fdf4);
  color: var(--c-success-text, #166534);
  border: 1px solid var(--c-success-border, #bbf7d0);
}

.lai-state-off { background: var(--c-surface-subtle, #f8fafc); color: var(--c-text-muted, #64748b); border-color: var(--c-border, #e2e8f0); }

.lai-role-note { font-size: 0.74rem; margin: 0 0 1rem; }
.lai-role-note code { font-size: 0.72rem; }

.lai-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 0.6rem;
  margin-bottom: 1.25rem;
}

.lai-stats > div {
  background: var(--c-surface-subtle, #f8fafc);
  border-radius: 0.5rem;
  padding: 0.7rem;
  text-align: center;
  display: grid;
  gap: 0.1rem;
}

.lai-stats strong { font-size: 1.2rem; font-weight: 800; color: var(--rail-active-text, var(--c-brand, #6366f1)); }
.lai-stats span { font-size: 0.66rem; color: var(--c-text-dim, #94a3b8); }

.lai-subhead {
  font-size: 0.82rem;
  font-weight: 800;
  margin: 1.4rem 0 0.25rem;
  padding-top: 1rem;
  border-top: 1px solid var(--c-surface-sunken, #f1f5f9);
}

/* ── activity ────────────────────────────────────────────────────────── */

.lai-row {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.55rem 0.4rem;
  border-bottom: 1px solid var(--c-surface-subtle, #f8fafc);
}

.lai-row-failed { background: var(--c-danger-tint, #fef2f2); }

.lai-row-icon {
  width: 30px;
  height: 30px;
  flex-shrink: 0;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--c-brand-tint, #f5f3ff);
}

.lai-row-failed .lai-row-icon { background: var(--c-danger-tint-2, #fee2e2); }

.lai-row-main { flex: 1; min-width: 0; display: grid; gap: 0.05rem; }

.lai-row-head {
  font-size: 0.78rem;
  font-weight: 700;
  color: var(--c-text, #0f172a);
  text-transform: capitalize;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.lai-row-head .muted { font-weight: 400; text-transform: none; }

.lai-row-detail {
  font-size: 0.7rem;
  color: var(--c-text-dim, #94a3b8);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.lai-row-meta {
  display: grid;
  justify-items: end;
  gap: 0.05rem;
  flex-shrink: 0;
  font-size: 0.64rem;
  color: var(--c-text-dim, #94a3b8);
}

.lai-row-status { font-size: 0.66rem; font-weight: 800; text-transform: uppercase; }
.lai-status-success { color: var(--c-success-text, #166534); }
.lai-status-failed { color: var(--c-danger-strong, #dc2626); }
.lai-status-pending { color: var(--c-warning-text, #92400e); }

/* ═══════════════════════════════════════════════════════════════════════
   MEDIA — campaign images and the library
   ═══════════════════════════════════════════════════════════════════════ */

.media-not-sent-note { font-size: 0.76rem; line-height: 1.55; margin: 0 0 0.9rem; }
.media-not-sent-note code { font-size: 0.72rem; }

.media-drop {
  display: grid;
  gap: 0.25rem;
  justify-items: center;
  text-align: center;
  padding: 1.9rem 1rem;
  border: 2px dashed var(--c-border, #e2e8f0);
  border-radius: 0.7rem;
  background: var(--c-surface, #fff);
  cursor: pointer;
  transition: border-color 0.14s, background 0.14s;
}

.media-drop:hover,
.media-drop:focus-visible,
.media-drop-over {
  border-color: var(--rail-accent, var(--c-brand-alt, #8b5cf6));
  background: var(--c-violet-tint, #faf9ff);
  outline: none;
}

.media-drop-icon { font-size: 1.7rem; }
.media-drop strong { font-size: 0.85rem; }
.media-drop .muted { font-size: 0.72rem; }

.media-actions {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
  margin-top: 0.7rem;
}

.media-actions button { font-size: 0.76rem; padding: 0.35rem 0.65rem; }

.media-soon {
  font-size: 0.6rem;
  font-weight: 700;
  padding: 0.05rem 0.25rem;
  border-radius: 0.2rem;
  background: var(--c-surface-sunken, #f1f5f9);
  color: var(--c-text-dim, #94a3b8);
  margin-left: 0.25rem;
}

/* ── the attached one ────────────────────────────────────────────────── */

.media-attached {
  display: flex;
  align-items: flex-start;
  gap: 0.8rem;
  padding: 0.8rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.65rem;
}

.media-attached-thumb {
  width: 84px;
  height: 84px;
  flex-shrink: 0;
  object-fit: cover;
  border-radius: 0.5rem;
  background: var(--c-surface-subtle, #f8fafc);
  border: 1px solid var(--c-border, #e2e8f0);
}

.media-attached-info { flex: 1; min-width: 0; display: grid; gap: 0.3rem; }
.media-attached-info > strong { font-size: 0.83rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.media-attached-info label { font-size: 0.7rem; font-weight: 600; color: var(--c-text-muted, #64748b); margin: 0.1rem 0 0; }
.media-attached-info input { width: 100%; font-size: 0.78rem; padding: 0.4rem 0.5rem; }

.media-badges { display: flex; gap: 0.3rem; flex-wrap: wrap; }

.media-badge {
  font-size: 0.66rem;
  font-weight: 600;
  padding: 0.1rem 0.4rem;
  border-radius: 2rem;
  background: var(--c-success-tint, #f0fdf4);
  color: var(--c-success-text, #166534);
  border: 1px solid var(--c-success-border, #bbf7d0);
}

.media-badge:first-child { background: var(--c-surface-subtle, #f8fafc); color: var(--c-text-muted, #64748b); border-color: var(--c-border, #e2e8f0); }
.media-badge-warn { background: var(--c-warning-tint, #fffbeb); color: var(--c-warning-text, #92400e); border-color: var(--c-warning-border, #fde68a); }

.media-remove {
  background: none;
  border: none;
  font-size: 1.3rem;
  line-height: 1;
  color: var(--c-text-dim, #94a3b8);
  cursor: pointer;
  padding: 0 0.2rem;
  flex-shrink: 0;
}

.media-remove:hover { color: var(--c-danger-strong, #dc2626); }

/* ── the library ─────────────────────────────────────────────────────── */

.media-modal { max-width: 640px; }

.media-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(128px, 1fr));
  gap: 0.6rem;
  max-height: 52vh;
  overflow-y: auto;
  padding: 0.15rem;
  margin: 0.5rem 0;
}

.media-tile {
  position: relative;
  display: grid;
  gap: 0;
  padding: 0;
  font: inherit;
  text-align: left;
  border: 2px solid var(--c-border, #e2e8f0);
  border-radius: 0.55rem;
  overflow: hidden;
  background: var(--c-surface, #fff);
  cursor: pointer;
  transition: border-color 0.12s;
}

.media-tile:hover, .media-tile:focus-visible {
  border-color: var(--rail-accent, var(--c-brand-alt, #8b5cf6));
  outline: none;
}

.media-tile img {
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  background: var(--c-surface-subtle, #f8fafc);
  display: block;
}

.media-tile-caption { display: grid; gap: 0.05rem; padding: 0.3rem 0.4rem 0.4rem; }

.media-tile-name {
  font-size: 0.68rem;
  font-weight: 700;
  color: var(--c-text, #0f172a);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.media-tile-meta { font-size: 0.62rem; color: var(--c-text-dim, #94a3b8); }

/* Only on hover: a delete cross on every tile of a grid is an accident
   waiting to happen. */
.media-tile-delete {
  position: absolute;
  top: 0.25rem;
  right: 0.25rem;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  display: none;
  align-items: center;
  justify-content: center;
  font-size: 0.9rem;
  line-height: 1;
  background: color-mix(in srgb, var(--c-scrim, #0f172a) 72%, transparent);
  color: var(--c-on-accent, #ffffff);
}

.media-tile:hover .media-tile-delete,
.media-tile:focus-within .media-tile-delete { display: flex; }
.media-tile-delete:hover { background: var(--c-danger-strong, #dc2626); }

@media (max-width: 640px) {
  .media-grid { grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); }
  .media-attached-thumb { width: 64px; height: 64px; }
}

/* ═══════════════════════════════════════════════════════════════════════
   ADMIN — what the assistant knows
   ═══════════════════════════════════════════════════════════════════════ */

.kb-settings {
  display: grid;
  gap: 0.5rem;
  padding: 0.85rem;
  margin: 0.5rem 0 1rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.6rem;
  background: var(--c-surface-subtle, #f8fafc);
}

.kb-setting { align-items: flex-start; gap: 0.5rem; font-size: 0.8rem; }
.kb-setting .muted { font-weight: 400; }
.kb-setting-num { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; }
.kb-setting-num input { flex-shrink: 0; }

.kb-filter { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-bottom: 0.85rem; }

.kb-entry {
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.6rem;
  margin-bottom: 0.5rem;
  overflow: hidden;
}

.kb-entry > summary {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  padding: 0.6rem 0.75rem;
  cursor: pointer;
  list-style: none;
  font-size: 0.82rem;
}

.kb-entry > summary::-webkit-details-marker { display: none; }
.kb-entry > summary::after { content: '▾'; margin-left: auto; color: var(--c-text-dim, #94a3b8); font-size: 0.75rem; }
.kb-entry[open] > summary::after { transform: rotate(180deg); }
.kb-entry > summary:hover { background: var(--c-surface-subtle, #f8fafc); }
.kb-entry-off { opacity: 0.55; }
.kb-entry-flat { padding: 0.75rem; }

.kb-body { padding: 0 0.75rem 0.75rem; border-top: 1px solid var(--c-surface-sunken, #f1f5f9); }
.kb-body textarea { width: 100%; font-family: inherit; font-size: 0.78rem; line-height: 1.55; }

.kb-content {
  white-space: pre-wrap;
  font-size: 0.78rem;
  line-height: 1.55;
  color: var(--c-text-3, #475569);
  margin: 0.6rem 0;
}

.kb-cat {
  font-size: 0.62rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 0.08rem 0.4rem;
  border-radius: 2rem;
  background: var(--c-brand-tint, #f5f3ff);
  color: var(--c-violet-text, #6d28d9);
  border: 1px solid var(--c-violet-border, #ddd6fe);
  flex-shrink: 0;
}

.kb-cat-compliance { background: var(--c-danger-tint, #fef2f2); color: var(--c-danger-darkest, #991b1b); border-color: var(--c-danger-border, #fecaca); }
.kb-cat-pricing { background: var(--c-success-tint, #f0fdf4); color: var(--c-success-text, #166534); border-color: var(--c-success-border, #bbf7d0); }
.kb-cat-brand { background: var(--c-violet-tint-2, #ede9fe); color: var(--c-violet-text, #6d28d9); border-color: var(--c-violet-border, #ddd6fe); }
.kb-cat-technical { background: var(--c-info-tint, #eff6ff); color: var(--c-info-text, #1d4ed8); border-color: var(--c-info-border, #bfdbfe); }

.kb-meta { font-size: 0.68rem; color: var(--c-text-dim, #94a3b8); }

.kb-example-head { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.kb-example-q { font-size: 0.82rem; font-weight: 700; margin: 0.4rem 0 0.2rem; }
.kb-example-why { font-size: 0.72rem; font-style: italic; margin: 0.3rem 0 0; }

/* ── try it ──────────────────────────────────────────────────────────── */

.console-warning { font-size: 0.76rem; margin: 0 0 0.8rem; }
.console-modes { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-bottom: 0.75rem; }

.console-output {
  min-height: 120px;
  max-height: 340px;
  overflow-y: auto;
  padding: 0.75rem;
  margin-bottom: 0.75rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.6rem;
  background: var(--c-surface-subtle, #f8fafc);
  display: grid;
  gap: 0.6rem;
  align-content: start;
}

.console-line { display: grid; gap: 0.15rem; font-size: 0.82rem; line-height: 1.55; }
.console-line > span:last-child { white-space: pre-wrap; overflow-wrap: anywhere; }

.console-who {
  font-size: 0.62rem;
  font-weight: 800;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--c-text-dim, #94a3b8);
}

.console-reply > span:last-child { color: var(--c-text, #0f172a); }
.console-error > span:last-child { color: var(--c-danger-strong, #dc2626); }
#console-usage { font-size: 0.7rem; }

/* ── requests: AI resolution state, audit log and queue ──────────────── */

.request-ai-status {
  padding: 0.6rem 0.75rem;
  margin-bottom: 0.75rem;
  border-radius: 0.5rem;
  border-left: 3px solid var(--c-brand-alt, #8b5cf6);
  background: var(--c-violet-tint, #faf9ff);
}
.request-ai-status-head { font-weight: 700; font-size: 0.8rem; }
.request-ai-status-body { font-size: 0.78rem; color: var(--c-text-3, #475569); margin-top: 0.2rem; }
.request-ai-status-when { font-size: 0.7rem; color: var(--c-text-dim, #94a3b8); margin-top: 0.25rem; }

/* One colour per outcome, on the state rather than on the words. The
   labels are model output and can be anything; the state is a known
   enum. */
.request-ai-state-resolved { border-left-color: var(--c-success, #16a34a); background: var(--c-success-tint, #f0fdf4); }
.request-ai-state-failed { border-left-color: var(--c-danger-strong, #dc2626); background: var(--c-danger-tint, #fef2f2); }
.request-ai-state-requires_human { border-left-color: var(--c-warning-strong, #d97706); background: var(--c-warning-tint, #fffbeb); }
.request-ai-state-analyzing,
.request-ai-state-executing { border-left-color: var(--c-brand, #6366f1); background: var(--c-brand-tint, #f5f3ff); }

.request-ai-actions { margin-bottom: 0.75rem; }

.request-ai-action {
  border: 1px solid var(--c-border, #e2e8f0);
  border-left-width: 3px;
  border-radius: 0.45rem;
  padding: 0.5rem 0.65rem;
  margin-bottom: 0.4rem;
  font-size: 0.78rem;
}
.request-ai-action-success { border-left-color: var(--c-success, #16a34a); }
.request-ai-action-failed { border-left-color: var(--c-danger-strong, #dc2626); }
.request-ai-action-blocked { border-left-color: var(--c-danger-strong, #dc2626); background: var(--c-danger-tint, #fef2f2); }
.request-ai-action-skipped { border-left-color: var(--c-border-strong, #cbd5e1); }
.request-ai-action-requires_human { border-left-color: var(--c-warning-strong, #d97706); }

.request-ai-action-head {
  display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap;
}
.request-ai-action-head code {
  font-size: 0.74rem; background: var(--c-surface-sunken, #f1f5f9); padding: 0.05rem 0.3rem; border-radius: 0.25rem;
}
.request-ai-action-verdict {
  font-size: 0.68rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.03em; color: var(--c-text-muted, #64748b);
}
.request-ai-action-body { margin-top: 0.25rem; color: var(--c-text-2, #334155); }
.request-ai-action-why { margin-top: 0.2rem; color: var(--c-text-muted, #64748b); font-style: italic; }
.request-ai-action-when { margin-top: 0.3rem; font-size: 0.68rem; color: var(--c-text-dim, #94a3b8); }

.request-ai-queue { margin-bottom: 1rem; }

.request-ai-queue-row {
  display: flex; align-items: center; gap: 0.6rem; width: 100%;
  text-align: left; cursor: pointer;
  padding: 0.5rem 0.7rem; margin-bottom: 0.35rem;
  border: 1px solid var(--c-violet-border, #ddd6fe); border-radius: 0.45rem;
  background: var(--c-violet-tint, #faf9ff); font: inherit; font-size: 0.8rem;
}
.request-ai-queue-row:hover { background: var(--c-brand-tint, #f5f3ff); }
/* The title is the flexible part; the number and the chip keep their
   width so a long title cannot push the state off the row. */
.request-ai-queue-row .request-row-title {
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.request-ai-chip {
  flex-shrink: 0;
  font-size: 0.66rem; font-weight: 700;
  padding: 0.1rem 0.4rem; border-radius: 2rem;
  border: 1px solid var(--c-border, #e2e8f0); background: var(--c-surface, #fff); color: var(--c-text-muted, #64748b);
}
.request-ai-chip.request-ai-state-resolved { border-color: var(--c-success-border, #bbf7d0); background: var(--c-success-tint, #f0fdf4); color: var(--c-success-text, #166534); }
.request-ai-chip.request-ai-state-failed { border-color: var(--c-danger-border, #fecaca); background: var(--c-danger-tint, #fef2f2); color: var(--c-danger-darkest, #991b1b); }
.request-ai-chip.request-ai-state-requires_human { border-color: var(--c-warning-border, #fde68a); background: var(--c-warning-tint, #fffbeb); color: var(--c-warning-text, #92400e); }
.request-ai-chip.request-ai-state-analyzing,
.request-ai-chip.request-ai-state-executing { border-color: var(--c-violet-border, #ddd6fe); background: var(--c-brand-tint, #f5f3ff); color: var(--c-brand, #6366f1); }

/* ── requests: the code agent ────────────────────────────────────────── */

.request-code-row {
  display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;
  padding: 0.55rem 0.7rem; margin-bottom: 0.75rem;
  border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.5rem; background: var(--c-surface-subtle, #f8fafc);
}
.request-code-row .muted { font-size: 0.74rem; }
.request-code-row button { font-size: 0.76rem; padding: 0.28rem 0.6rem; }
.request-code-flag {
  display: flex; align-items: center; gap: 0.35rem;
  font-size: 0.78rem; font-weight: 600; cursor: pointer;
}

.request-code-status {
  padding: 0.6rem 0.75rem; margin-bottom: 0.75rem;
  border-radius: 0.5rem; border-left: 3px solid var(--c-text-muted, #64748b); background: var(--c-surface-subtle, #f8fafc);
}
.request-code-status-head { font-weight: 700; font-size: 0.8rem; }

/* Coloured on the state, which is a known enum — never on the label,
   which carries a branch name somebody typed. */
.request-code-state-review_ready { border-left-color: var(--c-success, #16a34a); background: var(--c-success-tint, #f0fdf4); }
.request-code-state-failed { border-left-color: var(--c-danger-strong, #dc2626); background: var(--c-danger-tint, #fef2f2); }
.request-code-state-queued,
.request-code-state-analyzing,
.request-code-state-branching,
.request-code-state-generating,
.request-code-state-writing,
.request-code-state-committing,
.request-code-state-pushing { border-left-color: var(--c-brand, #6366f1); background: var(--c-brand-tint, #f5f3ff); }

.request-code-branch {
  display: flex; align-items: center; gap: 0.4rem; margin-top: 0.35rem; flex-wrap: wrap;
}
.request-code-branch code {
  font-size: 0.74rem; background: var(--c-surface-sunken, #f1f5f9);
  padding: 0.1rem 0.35rem; border-radius: 0.25rem;
  /* A branch name is long and must not push the Copy button off the row. */
  overflow-wrap: anywhere;
}
.request-code-copy {
  background: none; border: none; color: var(--c-brand, #6366f1); cursor: pointer;
  font: inherit; font-size: 0.72rem; font-weight: 600; padding: 0;
}
.request-code-pr {
  display: inline-block; margin-top: 0.45rem;
  background: var(--c-brand, #6366f1); color: var(--c-on-fill, #ffffff); text-decoration: none;
  padding: 0.3rem 0.6rem; border-radius: 0.35rem;
  font-size: 0.74rem; font-weight: 700;
}
.request-code-note-ok {
  margin-top: 0.45rem; font-size: 0.74rem; color: var(--c-success-text, #166534);
}
.request-code-note-bad {
  margin-top: 0.45rem; font-size: 0.74rem; color: var(--c-danger-darkest, #991b1b); overflow-wrap: anywhere;
}
.request-code-working { margin-top: 0.4rem; font-size: 0.72rem; color: var(--c-text-dim, #94a3b8); }

/* ── legal pages: the Google scope table ─────────────────────────────── */

.legal-doc .legal-table {
  width: 100%;
  border-collapse: collapse;
  margin: 1.25rem 0 1.5rem;
  font-size: 0.9rem;
}
.legal-doc .legal-table th {
  text-align: left;
  padding: 0.6rem 0.75rem;
  background: var(--c-surface-sunken, #f1f5f9);
  border-bottom: 2px solid var(--c-border, #e2e8f0);
  font-weight: 700;
}
.legal-doc .legal-table td {
  padding: 0.7rem 0.75rem;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
  vertical-align: top;
}
.legal-doc .legal-table tr:last-child td { border-bottom: none; }
/* A scope name is a long unbroken string; without this the first column
   sets the table's width and the second one gets squeezed to nothing. */
.legal-doc .legal-table td:first-child { width: 30%; }
.legal-doc .legal-table code {
  font-size: 0.82rem;
  background: var(--c-surface-sunken, #f1f5f9);
  padding: 0.1rem 0.3rem;
  border-radius: 0.25rem;
  overflow-wrap: anywhere;
}

.legal-doc .legal-note {
  background: var(--c-warning-tint, #fffbeb);
  border: 1px solid var(--c-warning-border, #fde68a);
  border-left-width: 3px;
  border-radius: 0.5rem;
  padding: 0.9rem 1.1rem;
  margin: 1.25rem 0;
}

/* The table is the widest thing on these pages; on a phone it has to be
   able to scroll rather than force the whole document sideways. */
@media (max-width: 640px) {
  .legal-doc .legal-table { display: block; overflow-x: auto; white-space: normal; }
  .legal-doc .legal-table td:first-child { width: auto; min-width: 9rem; }
}

/* The open-request count beside "My Requests" in the user menu
   (REQ-0021). Hidden at zero rather than showing a grey 0, which reads
   as a broken badge. */
.nav-menu-count {
  display: inline-block;
  margin-left: 0.4rem;
  min-width: 1.15rem;
  padding: 0 0.3rem;
  border-radius: 1rem;
  background: var(--c-brand, #6366f1);
  color: var(--c-on-fill, #ffffff);
  font-size: 0.68rem;
  font-weight: 700;
  line-height: 1.15rem;
  text-align: center;
}

/* ── calendar: the Events panel (REQ-0019) ───────────────────────────── */

/* The table is the widest thing in the panel; it scrolls inside its own
   box rather than making the page scroll sideways. */
.cal-manage-wrap { overflow-x: auto; }

.cal-manage {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.85rem;
}
.cal-manage th {
  text-align: left;
  padding: 0.5rem 0.6rem;
  border-bottom: 2px solid var(--c-border, #e2e8f0);
  color: var(--c-text-muted, #64748b);
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
}
.cal-manage td {
  padding: 0.55rem 0.6rem;
  border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9);
  vertical-align: middle;
}
.cal-manage tbody tr:hover { background: var(--c-surface-subtle, #f8fafc); }

.cal-manage-date { white-space: nowrap; color: var(--c-text-3, #475569); }
.cal-manage-title { font-weight: 600; color: var(--c-text, #0f172a); }
.cal-manage-plan { color: var(--c-text-muted, #64748b); }

/* The event's own colour, as a dot rather than a row tint — a full-row
   background at these widths fights the hover state and the overdue
   marker for the same pixels. */
.cal-manage-dot {
  display: inline-block;
  width: 0.55rem; height: 0.55rem;
  border-radius: 50%;
  margin-right: 0.35rem;
  vertical-align: baseline;
}

.cal-manage-overdue .cal-manage-date { color: var(--c-warning-text, #92400e); font-weight: 600; }
.cal-manage-late {
  display: inline-block;
  margin-left: 0.35rem;
  padding: 0 0.3rem;
  border-radius: 1rem;
  background: var(--c-warning-tint, #fffbeb);
  border: 1px solid var(--c-warning-border, #fde68a);
  color: var(--c-warning-text, #92400e);
  font-size: 0.62rem;
  font-weight: 700;
  text-transform: uppercase;
}

.cal-manage-actions { white-space: nowrap; text-align: right; }
.cal-manage-actions button {
  font-size: 0.72rem;
  padding: 0.2rem 0.5rem;
  margin-left: 0.3rem;
}

/* ── admin Users: the platform's own identities ──────────────────────── */

/* Listed since the is_system exclusion was lifted, and tinted so they do
   not read as colleagues. They sort to the top of the table. */
.users-system-row { background: var(--c-violet-tint, #faf9ff); }
.users-system-row:hover { background: var(--c-brand-tint, #f5f3ff); }

.users-system-badge {
  font-size: 0.62rem;
  font-weight: 700;
  padding: 0.05rem 0.35rem;
  border-radius: 1rem;
  color: var(--c-on-accent, #ffffff);
  white-space: nowrap;
}
.users-system-ai { background: linear-gradient(135deg, var(--c-brand, #6366f1), var(--c-brand-alt, #8b5cf6)); }
.users-system-support { background: linear-gradient(135deg, var(--c-sky, #0ea5e9), var(--c-cyan, #06b6d4)); color: var(--c-on-fill, #ffffff); }

/* ── admin analytics ─────────────────────────────────────────────────── */

/* The panes are rail panels now (#tab-admin-analytics), so their
   visibility is the shared .ws-settings-panels contract above rather
   than a rule of their own. */

/* The date range and Refresh, above the panels and outside all of them. */
.an-toolbar {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin-bottom: 1rem;
}
.an-toolbar .muted { font-size: 0.75rem; }
.an-toolbar select { max-width: 12rem; }

.an-kpis {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 0.6rem;
  margin-bottom: 1rem;
}
.an-kpi {
  border: 1px solid var(--c-border, #e2e8f0);
  border-top: 3px solid var(--c-brand, #6366f1);
  border-radius: 0.6rem;
  padding: 0.75rem 0.85rem;
  background: var(--c-surface, #fff);
}
.an-kpi-good { border-top-color: var(--c-success, #16a34a); }
.an-kpi-warn { border-top-color: var(--c-warning-strong, #d97706); }
.an-kpi-bad { border-top-color: var(--c-danger-strong, #dc2626); }
.an-kpi-value { font-size: 1.4rem; font-weight: 800; color: var(--c-text, #0f172a); line-height: 1.2; }
.an-kpi-label { font-size: 0.72rem; color: var(--c-text-muted, #64748b); margin-top: 0.15rem; }
.an-kpi-sub { font-size: 0.68rem; color: var(--c-text-dim, #94a3b8); margin-top: 0.15rem; }

.an-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 0.85rem;
}

.an-card {
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.7rem;
  padding: 0.9rem 1rem;
  background: var(--c-surface, #fff);
  margin-bottom: 0.85rem;
}
.an-card h3 {
  font-size: 0.8rem;
  font-weight: 700;
  color: var(--c-text, #0f172a);
  margin: 0 0 0.7rem;
}
/* A canvas in a grid cell will grow without bound unless the cell is
   allowed to be narrower than its content. */
.an-card canvas { max-width: 100%; }
.an-grid > .an-card { min-width: 0; margin-bottom: 0; }

/* Wide tables scroll inside their own card rather than the page. */
.an-table-wrap { overflow-x: auto; }
.an-table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
.an-table th {
  text-align: left;
  padding: 0.4rem 0.5rem;
  border-bottom: 2px solid var(--c-border, #e2e8f0);
  color: var(--c-text-muted, #64748b);
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
}
.an-table td { padding: 0.45rem 0.5rem; border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9); }
.an-table tbody tr:last-child td { border-bottom: none; }
.an-right { text-align: right; }
.an-cell-sub { font-size: 0.68rem; color: var(--c-text-dim, #94a3b8); }

.an-bar-row { margin-bottom: 0.7rem; }
.an-bar-head {
  display: flex;
  justify-content: space-between;
  gap: 0.5rem;
  font-size: 0.78rem;
  margin-bottom: 0.25rem;
}
.an-bar-head span:last-child { color: var(--c-text-dim, #94a3b8); white-space: nowrap; }
.an-bar-track { background: var(--c-surface-sunken, #f1f5f9); border-radius: 1rem; height: 6px; }
.an-bar-fill { background: var(--c-brand, #6366f1); border-radius: 1rem; height: 6px; }

/* ── My AI: the shared save bar ──────────────────────────────────────── */

/* Sits outside every [data-myai-panel-id], because one Save writes the
   settings from four different panels. */
.myai-save-bar {
  display: flex;
  gap: 0.5rem;
  justify-content: flex-end;
  flex-wrap: wrap;
  padding-top: 1rem;
  margin-top: 0.25rem;
  border-top: 1px solid var(--c-border, #e2e8f0);
}
.myai-save-bar[hidden] { display: none; }

@media (max-width: 640px) {
  .myai-save-bar { justify-content: stretch; }
  .myai-save-bar button { flex: 1; }
}

@media (max-width: 768px) {
  .campaign-name-input { max-width: 100%; }
}

/* ── calendar: the Master Plan panel ─────────────────────────────────── */

.mp-state { text-align: center; padding: 2.5rem 1rem; }
.mp-state-icon { font-size: 2.5rem; margin-bottom: 0.5rem; }
.mp-state h3 { font-size: 1.1rem; font-weight: 800; margin: 0 0 0.4rem; }
.mp-state p { max-width: 34rem; margin: 0 auto 1rem; }
.mp-state-actions { display: flex; gap: 0.5rem; justify-content: center; flex-wrap: wrap; }

.mp-choose-list { max-width: 32rem; margin: 0 auto; text-align: left; }
.mp-choose-row {
  display: flex; align-items: center; gap: 0.7rem; width: 100%;
  padding: 0.7rem 0.85rem; margin-bottom: 0.5rem;
  border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.65rem;
  background: var(--c-surface, #fff); cursor: pointer; font: inherit; text-align: left;
}
.mp-choose-row:hover { border-color: var(--c-brand-border, #c7d2fe); background: var(--c-violet-tint, #faf9ff); }
.mp-choose-current { border-color: var(--c-brand, #6366f1); background: var(--c-brand-tint, #f5f3ff); }
.mp-choose-dot {
  width: 2rem; height: 2rem; border-radius: 50%; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center; font-size: 0.95rem;
}
.mp-choose-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.mp-choose-name { font-weight: 700; font-size: 0.85rem; color: var(--c-text, #0f172a); }
.mp-choose-meta { font-size: 0.7rem; color: var(--c-text-dim, #94a3b8); }
.mp-choose-mark { font-size: 0.7rem; font-weight: 700; color: var(--c-brand, #6366f1); flex-shrink: 0; }

/* Three columns wide; stacked below 1100px, where 360px of chat beside
   two other columns is nobody's idea of readable. */
.mp-grid {
  display: grid;
  grid-template-columns: minmax(0, 17rem) minmax(0, 1fr) minmax(0, 22rem);
  gap: 1rem;
  align-items: start;
}
@media (max-width: 1100px) {
  .mp-grid { grid-template-columns: minmax(0, 1fr); }
}
.mp-col { min-width: 0; }

.mp-section { margin-top: 1.25rem; }
.mp-section-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.5rem; margin-bottom: 0.6rem;
}
.mp-section-head h3 { font-size: 0.8rem; font-weight: 700; margin: 0; }
.mp-section-head button { font-size: 0.72rem; padding: 0.2rem 0.5rem; }

.mp-overview-head { display: flex; align-items: center; gap: 0.45rem; }
.mp-overview-head strong { font-size: 0.95rem; color: var(--c-text, #0f172a); }
.mp-dot { width: 0.6rem; height: 0.6rem; border-radius: 50%; flex-shrink: 0; }
.mp-tagline { font-size: 0.8rem; color: var(--c-brand, #6366f1); font-style: italic; margin: 0.35rem 0 0; }
.mp-dates { font-size: 0.72rem; color: var(--c-text-dim, #94a3b8); margin: 0.25rem 0 0.6rem; }
.mp-goal {
  background: var(--c-surface-subtle, #f8fafc); border-left: 3px solid var(--c-brand, #6366f1); border-radius: 0.4rem;
  padding: 0.55rem 0.7rem; font-size: 0.78rem; color: var(--c-text-3, #475569); margin-bottom: 0.5rem;
}

.mp-bars { margin-top: 0.8rem; }
.mp-bar-row { margin-bottom: 0.55rem; }
.mp-bar-head {
  display: flex; justify-content: space-between; gap: 0.5rem;
  font-size: 0.72rem; color: var(--c-text-muted, #64748b); margin-bottom: 0.2rem;
}
.mp-bar-track { background: var(--c-surface-sunken, #f1f5f9); border-radius: 1rem; height: 5px; }
.mp-bar-fill { background: var(--c-brand, #6366f1); border-radius: 1rem; height: 5px; }

.mp-milestone {
  display: flex; align-items: flex-start; gap: 0.5rem;
  padding: 0.4rem 0; border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9); font-size: 0.78rem;
}
.mp-milestone:last-child { border-bottom: none; }
.mp-milestone-main { display: flex; flex-direction: column; min-width: 0; }
.mp-milestone-title { font-weight: 600; color: var(--c-text, #0f172a); overflow-wrap: anywhere; }
.mp-milestone-meta { font-size: 0.68rem; color: var(--c-text-dim, #94a3b8); }
.mp-milestone-achieved .mp-milestone-title { color: var(--c-text-dim, #94a3b8); text-decoration: line-through; }

.mp-month {
  font-size: 0.72rem; font-weight: 700; color: var(--c-brand, #6366f1);
  text-transform: uppercase; letter-spacing: 0.04em;
  margin: 1rem 0 0.5rem; padding-bottom: 0.25rem;
  border-bottom: 1px solid var(--c-brand-tint-2, #eef2ff);
}
.mp-month:first-child { margin-top: 0; }

.mp-event {
  display: flex; align-items: center; gap: 0.6rem; width: 100%;
  padding: 0.5rem 0.65rem; margin-bottom: 0.35rem;
  border: 1px solid var(--c-surface-sunken, #f1f5f9); border-left: 3px solid var(--row, var(--c-brand, #6366f1));
  border-radius: 0.5rem; background: var(--c-surface, #fff);
  cursor: pointer; font: inherit; text-align: left;
}
.mp-event:hover { background: var(--c-surface-subtle, #f8fafc); }
.mp-event-icon { flex-shrink: 0; }
.mp-event-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.mp-event-title { font-size: 0.8rem; font-weight: 600; color: var(--c-text, #0f172a); overflow-wrap: anywhere; }
.mp-event-meta { font-size: 0.68rem; color: var(--c-text-dim, #94a3b8); }
.mp-event-mark { flex-shrink: 0; font-size: 0.8rem; color: var(--c-text-dim, #94a3b8); }
.mp-event-completed { opacity: 0.65; }
.mp-event-completed .mp-event-title { text-decoration: line-through; }
.mp-event-completed .mp-event-mark { color: var(--c-success, #16a34a); }
.mp-event-missed .mp-event-mark { color: var(--c-danger-strong, #dc2626); }
.mp-event-in_progress .mp-event-mark { color: var(--c-warning-text, #92400e); }

.mp-col-chat {
  display: flex; flex-direction: column;
  border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.7rem; overflow: hidden;
}
.mp-chat-head {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.6rem 0.75rem; border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9); background: var(--c-violet-tint, #faf9ff);
}
.mp-chat-avatar {
  width: 1.75rem; height: 1.75rem; border-radius: 50%;
  background: linear-gradient(135deg, var(--c-brand, #6366f1), var(--c-brand-alt, #8b5cf6));
  display: flex; align-items: center; justify-content: center; flex-shrink: 0; color: var(--c-on-status, #0f172a);
}
.mp-chat-head strong { display: block; font-size: 0.8rem; }
.mp-chat-head small { display: block; font-size: 0.68rem; color: var(--c-brand, #6366f1); }

.mp-chat {
  flex: 1; min-height: 14rem; max-height: 28rem; overflow-y: auto;
  padding: 0.75rem; display: flex; flex-direction: column; gap: 0.45rem;
}
.mp-msg {
  font-size: 0.78rem; line-height: 1.5; padding: 0.45rem 0.65rem;
  border-radius: 0.6rem; max-width: 92%;
  /* The replies are plain text and are inserted with textContent, so the
     line breaks have to survive here rather than through <br> parsing. */
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.mp-msg-user { align-self: flex-end; background: var(--c-brand, #6366f1); color: var(--c-on-fill, #ffffff); }
.mp-msg-assistant { align-self: flex-start; background: var(--c-surface-subtle, #f8fafc); border: 1px solid var(--c-border, #e2e8f0); }
.mp-msg-system {
  align-self: center; background: var(--c-success-tint, #f0fdf4); border: 1px solid var(--c-success-border, #bbf7d0);
  color: var(--c-success-text, #166534); font-size: 0.7rem; font-weight: 600; border-radius: 1rem;
}
.mp-msg-typing { color: var(--c-text-dim, #94a3b8); }

.mp-chat-prompts {
  display: flex; flex-wrap: wrap; gap: 0.3rem;
  padding: 0.5rem 0.75rem; border-top: 1px solid var(--c-surface-sunken, #f1f5f9);
}
.mp-prompt {
  background: var(--c-brand-tint, #f5f3ff); color: var(--c-brand-strong, #4f46e5); border: 1px solid var(--c-brand-tint-3, #e0e7ff);
  border-radius: 1rem; padding: 0.2rem 0.55rem;
  font: inherit; font-size: 0.68rem; font-weight: 600; cursor: pointer;
  box-shadow: none;
}
.mp-prompt:hover:not(:disabled) { background: var(--c-violet-tint-2, #ede9fe); box-shadow: none; transform: none; }

.mp-chat-input {
  display: flex; gap: 0.4rem; align-items: flex-end;
  padding: 0.6rem 0.75rem; border-top: 1px solid var(--c-border, #e2e8f0);
}
.mp-chat-input textarea {
  flex: 1; min-width: 0; resize: none; font: inherit; font-size: 0.8rem;
  padding: 0.45rem 0.55rem; border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.45rem;
}
.mp-chat-input button {
  flex-shrink: 0; width: 2.1rem; height: 2.1rem; padding: 0;
  display: flex; align-items: center; justify-content: center;
}
.mp-chat-note { font-size: 0.68rem; padding: 0 0.75rem 0.6rem; margin: 0; }

/* ── the contact sheet ───────────────────────────────────────────────── */

.contact-sheet-overlay {
  position: fixed; inset: 0; z-index: 1200;
  background: color-mix(in srgb, var(--c-scrim, #0f172a) 35%, transparent);
}

/* Full width, anchored to the bottom: the point of this panel is seeing
   every field at once, which a centred modal column cannot do. */
.contact-sheet {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 1201;
  max-height: 92vh;
  display: flex; flex-direction: column;
  background: var(--c-surface, #fff);
  border-top: 1px solid var(--c-border, #e2e8f0);
  border-radius: 1rem 1rem 0 0;
  box-shadow: 0 -12px 40px color-mix(in srgb, var(--c-scrim, #0f172a) 18%, transparent);
}
.contact-sheet.hidden, .contact-sheet-overlay.hidden { display: none; }

.contact-sheet-head {
  display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;
  padding: 0.9rem 1.25rem;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
  /* Sticky, so Save is reachable without scrolling back up a sheet that
     can be thirty fields long. */
  position: sticky; top: 0; background: var(--c-surface, #fff); z-index: 1;
  border-radius: 1rem 1rem 0 0;
}
.cs-identity { display: flex; align-items: center; gap: 0.65rem; flex: 1; min-width: 0; }
.cs-avatar {
  width: 2.4rem; height: 2.4rem; border-radius: 50%; flex-shrink: 0;
  background: linear-gradient(135deg, var(--c-brand, #6366f1), var(--c-brand-alt, #8b5cf6)); color: var(--c-on-fill, #ffffff);
  display: flex; align-items: center; justify-content: center;
  font-weight: 800; font-size: 1rem;
}
.cs-identity-text { min-width: 0; display: flex; flex-direction: column; }
.cs-identity-text strong { font-size: 1rem; color: var(--c-text, #0f172a); overflow-wrap: anywhere; }
.cs-identity-text .muted { font-size: 0.75rem; overflow-wrap: anywhere; }
.cs-badges { display: flex; gap: 0.3rem; flex-wrap: wrap; }
.cs-badge {
  font-size: 0.65rem; font-weight: 700; padding: 0.1rem 0.4rem;
  border-radius: 1rem; white-space: nowrap;
}
.cs-badge-linxi { background: var(--c-brand-tint, #f5f3ff); color: var(--c-brand, #6366f1); border: 1px solid var(--c-violet-border, #ddd6fe); }
.cs-badge-stop { background: var(--c-danger-tint, #fef2f2); color: var(--c-danger-darkest, #991b1b); border: 1px solid var(--c-danger-border, #fecaca); }
.cs-badge-ok { background: var(--c-success-tint, #f0fdf4); color: var(--c-success-text, #166534); border: 1px solid var(--c-success-border, #bbf7d0); }
.cs-head-actions { display: flex; gap: 0.4rem; align-items: center; flex-wrap: wrap; }
.cs-head-actions button { font-size: 0.76rem; padding: 0.3rem 0.6rem; }
.contact-sheet-close {
  background: none; border: none; color: var(--c-text-muted, #64748b);
  font-size: 1rem; padding: 0.2rem 0.45rem; box-shadow: none;
}
.contact-sheet-close:hover:not(:disabled) {
  background: var(--c-surface-sunken, #f1f5f9); box-shadow: none; transform: none;
}
.contact-sheet #cs-result:empty { display: none; }
.contact-sheet #cs-result { margin: 0.6rem 1.25rem 0; }

.contact-sheet-body {
  overflow-y: auto;
  padding: 1rem 1.25rem 1.5rem;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: 0.85rem;
  align-items: start;
}
.cs-card {
  border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.7rem;
  padding: 0.85rem 0.95rem; min-width: 0;
}
.cs-card h3 {
  font-size: 0.72rem; font-weight: 700; color: var(--c-text-muted, #64748b);
  text-transform: uppercase; letter-spacing: 0.05em; margin: 0 0 0.7rem;
}
/* Taxonomy and notes want the whole row rather than a 17rem column. */
.cs-card-wide { grid-column: 1 / -1; }
.cs-note { font-size: 0.7rem; margin: 0.6rem 0 0; }

/* The delete card at the foot of the contact sheet. Tinted rather than
   plain, because it sits under Notes and a destructive button in an
   ordinary-looking card reads as one more field. Every colour is a token,
   so it follows the palette and dark mode like the rest of the sheet. */
.cs-danger-card {
  border-color: var(--c-danger-border);
  background: var(--c-danger-tint);
}
.cs-danger-card h3 { color: var(--c-danger-darkest); }
.cs-danger-card p { margin: 0 0 0.7rem; font-size: 0.75rem; }

/* Contact sheet -- Web search card (migration 072). Colours come from
   the existing tokens rather than hard-coded hexes so the card follows
   the theme like every other card in the sheet. */
.cs-web-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.cs-web-actions { display: flex; align-items: center; gap: 0.6rem; }
.cs-web-actions .muted { font-size: 0.7rem; white-space: nowrap; }
.cs-web-body { margin-top: 0.9rem; display: flex; flex-direction: column; gap: 0.9rem; }
.cs-web-banner { display: flex; flex-direction: column; gap: 0.2rem; padding: 0.7rem 0.85rem;
  border-radius: 0.55rem; border: 1px solid var(--c-border, #e2e8f0); background: var(--c-surface-subtle, #f8fafc); font-size: 0.78rem; }
.cs-web-banner strong { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em; }
.cs-web-banner-ok { border-color: var(--success-border, var(--c-success-border, #bbf7d0)); background: var(--success-bg, var(--c-success-tint, #f0fdf4)); }
.cs-web-banner-ok strong { color: var(--success, #15803d); }
.cs-web-banner-warn { border-color: var(--warning-border, var(--c-warning-border, #fde68a)); background: var(--warning-bg, var(--c-warning-tint, #fffbeb)); }
.cs-web-banner-warn strong { color: var(--warning, #b45309); }
.cs-web-banner-muted strong { color: var(--c-text-muted, #64748b); }
.cs-web-section { display: flex; flex-direction: column; gap: 0.35rem; }
.cs-web-section h4 { margin: 0 0 0.15rem; font-size: 0.68rem; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--c-text-muted, #64748b); font-weight: 700; }
.cs-web-row { display: flex; gap: 0.75rem; font-size: 0.8rem; align-items: baseline; }
.cs-web-label { flex: 0 0 7.5rem; color: var(--c-text-muted, #64748b); font-size: 0.72rem; }
.cs-web-row a { word-break: break-all; }
.cs-web-bio { margin: 0; font-size: 0.8rem; line-height: 1.55; }
/* The notes block is newline-separated by buildNotes and is set with
   textContent, so the breaks have to survive in CSS rather than markup. */
.cs-web-notes { margin: 0; font-size: 0.78rem; line-height: 1.5; white-space: pre-wrap; }
.cs-web-note { margin: 0; font-size: 0.78rem; line-height: 1.5; }
.cs-web-note-muted { color: var(--c-text-muted, #64748b); }
.cs-web-note-ok { color: var(--success, #15803d); }
.cs-web-note-warn { color: var(--warning, #b45309); }
.cs-web-chips { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.cs-web-chip { padding: 0.2rem 0.6rem; border-radius: 2rem; border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc); font-size: 0.72rem; font-weight: 600; text-decoration: none;
  text-transform: capitalize; }
.cs-web-news { display: flex; flex-direction: column; gap: 0.1rem; padding: 0.4rem 0;
  border-bottom: 1px solid var(--c-border, #e2e8f0); text-decoration: none; }
.cs-web-news:last-child { border-bottom: 0; }
.cs-web-news strong { font-size: 0.78rem; }
.cs-web-news span { font-size: 0.7rem; color: var(--c-text-muted, #64748b); }
@media (max-width: 640px) {
  .cs-web-row { flex-direction: column; gap: 0.1rem; }
  .cs-web-label { flex: none; }
}

.cs-fields { display: flex; flex-direction: column; gap: 0.55rem; }
.cs-field { display: flex; flex-direction: column; gap: 0.2rem; }
.cs-field span { font-size: 0.7rem; font-weight: 600; color: var(--c-text-muted, #64748b); }
.cs-field input,
.cs-field textarea {
  width: 100%; font: inherit; font-size: 0.82rem;
  padding: 0.4rem 0.5rem; border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.4rem;
}
/* `bio` is a paragraph. Vertical resize only -- horizontal would let it
   escape the card it sits in, and the sheet is already a responsive grid
   whose columns are the thing deciding width. */
.cs-field textarea { resize: vertical; min-height: 4.2rem; line-height: 1.4; }

.cs-check { display: flex; align-items: center; gap: 0.4rem; font-size: 0.82rem; flex-wrap: wrap; }
.cs-check small { width: 100%; font-size: 0.68rem; color: var(--c-text-dim, #94a3b8); padding-left: 1.3rem; }

.cs-readonly { display: flex; flex-direction: column; gap: 0.3rem; }
.cs-ro { display: flex; justify-content: space-between; gap: 0.75rem; font-size: 0.78rem; }
.cs-ro-label { color: var(--c-text-muted, #64748b); flex-shrink: 0; }
.cs-ro-value { color: var(--c-text, #0f172a); text-align: right; overflow-wrap: anywhere; }
.cs-ro-bad { color: var(--c-warning-text, #92400e); }

.cs-taxonomy {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 0.85rem;
}
.cs-tax-label {
  display: block; font-size: 0.7rem; font-weight: 600;
  color: var(--c-text-muted, #64748b); margin-bottom: 0.35rem;
}
.contact-sheet #cs-notes {
  width: 100%; font: inherit; font-size: 0.82rem; resize: vertical;
  padding: 0.5rem 0.6rem; border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.45rem;
}

@media (max-width: 640px) {
  .contact-sheet { max-height: 96vh; border-radius: 0; }
  .cs-head-actions { width: 100%; }
  .cs-head-actions button { flex: 1; }
}

/* ── Lead Finder ─────────────────────────────────────────────────────── */

.lf-consent {
  background: var(--c-warning-tint, #fffbeb); border: 1px solid var(--c-warning-border, #fde68a); border-left-width: 3px;
  border-radius: 0.5rem; padding: 0.7rem 0.9rem; margin: 0.75rem 0 1rem;
  font-size: 0.8rem; color: var(--c-warning-text, #92400e); line-height: 1.5;
}
.lf-consent a { color: var(--c-warning-text, #92400e); font-weight: 600; }

.lf-form { display: flex; gap: 0.5rem; margin-bottom: 0.6rem; flex-wrap: wrap; }
.lf-form input { flex: 1; min-width: 14rem; }
.lf-form.hidden { display: none; }

.lf-chips { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-bottom: 0.75rem; }
.lf-chips.hidden { display: none; }
.lf-chip {
  background: var(--c-surface-sunken, #f1f5f9); color: var(--c-text-3, #475569); border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 1rem; padding: 0.15rem 0.55rem;
  font: inherit; font-size: 0.72rem; cursor: pointer; box-shadow: none;
}
.lf-chip:hover:not(:disabled) {
  background: var(--c-brand-tint-2, #eef2ff); color: var(--c-brand-strong, #4f46e5); box-shadow: none; transform: none;
}

.lf-results-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.5rem; flex-wrap: wrap; margin: 0.75rem 0 0.5rem;
}
.lf-results-head.hidden { display: none; }
.lf-results-actions { display: flex; gap: 0.4rem; }
.lf-results-actions button { font-size: 0.76rem; padding: 0.28rem 0.6rem; }

.lf-result {
  display: flex; align-items: flex-start; gap: 0.6rem;
  border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.6rem;
  padding: 0.6rem 0.75rem; margin-bottom: 0.4rem; cursor: pointer;
}
.lf-result:hover { background: var(--c-surface-subtle, #f8fafc); }
.lf-result-imported { opacity: 0.6; cursor: default; }
.lf-result input { margin-top: 0.2rem; flex-shrink: 0; }
.lf-result-main { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.lf-result-title {
  font-weight: 700; font-size: 0.84rem; color: var(--c-text, #0f172a);
  display: flex; align-items: center; gap: 0.35rem; flex-wrap: wrap;
  overflow-wrap: anywhere;
}
.lf-result-contact { font-size: 0.78rem; color: var(--c-brand-strong, #4f46e5); overflow-wrap: anywhere; }
.lf-result-src { font-size: 0.7rem; color: var(--c-text-dim, #94a3b8); }
.lf-result-desc {
  font-size: 0.72rem; color: var(--c-text-muted, #64748b); margin-top: 0.15rem;
  /* A meta description off a stranger's site can be a paragraph. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.lf-badge {
  font-size: 0.62rem; font-weight: 700; padding: 0.05rem 0.35rem;
  border-radius: 1rem; white-space: nowrap;
}
.lf-badge-ok { background: var(--c-success-tint, #f0fdf4); color: var(--c-success-text, #166534); border: 1px solid var(--c-success-border, #bbf7d0); }
.lf-badge-mid { background: var(--c-warning-tint, #fffbeb); color: var(--c-warning-text, #92400e); border: 1px solid var(--c-warning-border, #fde68a); }
.lf-badge-low { background: var(--c-surface-sunken, #f1f5f9); color: var(--c-text-muted, #64748b); border: 1px solid var(--c-border, #e2e8f0); }
.lf-badge-done { background: var(--c-brand-tint-2, #eef2ff); color: var(--c-brand-strong, #4f46e5); border: 1px solid var(--c-brand-border, #c7d2fe); }

/* ── Linxi Secure Channels ───────────────────────────────────────────
   Deliberately darker than the rest of the app. It is the one surface
   where the content is not readable by Linxi, and looking different is
   part of how somebody knows which mode they are in. */
/* ⚠️⚠️ THIS PALETTE WAS BUILT FROM TOKENS THAT INVERT, SO THE MODULE
   INVERTED WITH THEM. It is dark in BOTH themes on purpose -- this file
   says so where the dark-mode exceptions are listed, alongside the
   header and the legal hero -- but it took its ground from --c-text and
   its ink from --c-border, which are a text token and a border token:

       --sec-bg    var(--c-text)     #0f172a light -> #e2e8f0 DARK
       --sec-text  var(--c-border)   #e2e8f0 light -> #334155 DARK
       --sec-line  var(--c-text-2)   #334155 light -> #cbd5e1 DARK

   In dark mode the whole module turned into a near-WHITE slab with dark
   ink and near-white rules, while --sec-panel stayed a hardcoded #1e293b
   -- so the channel rows and the bubbles were dark on white inside it.

   This is the exact pair of mistakes recorded in this file already:
   --c-surface is never a text colour and --c-text is never a background.
   The first pass at dark mode tokenised by VALUE, which is a no-op in
   light and wrong everywhere else; this block was missed by it.

   ⚠️ THE REPLACEMENTS ARE THE FAMILIES THAT EXIST FOR THIS. --c-chrome-*
   and --c-on-dark-* hold the SAME value in all three theme blocks, so a
   white-label can recolour this module's furniture and the theme cannot;
   --c-inverse-surface is the documented "a dark bar in BOTH modes"
   token, lifting from #0f172a to #1e293b in dark so the module still
   separates from the page behind it.

   ⚠️ --sec-accent IS A FILL, not a label -- .sec-btn-primary, .sec-unread,
   the outgoing bubble and the send button all set it as a background
   with --c-on-accent (white) on top. So it is --c-brand-strong, which
   stays dark in both modes, and NOT --c-on-dark-brand, which is the pale
   brand meant for TEXT on a dark ground and would be white-on-lilac.

   ⚠️ --sec-ok IS GONE. It was declared here and read by nothing -- a
   name that reads as if a state exists. */
.sec-shell { --sec-bg:var(--c-inverse-surface, #0f172a); --sec-panel:var(--c-inverse-surface-2, #1e293b);
  --sec-line:var(--c-chrome-border, #4b5563); --sec-text:var(--c-chrome-text, #cbd5e1);
  --sec-dim:var(--c-chrome-dim, #94a3b8); --sec-accent:var(--c-brand-strong, #4f46e5);
  background:var(--sec-bg); color:var(--sec-text); border-radius:0.75rem;
  min-height:70vh; display:flex; flex-direction:column; overflow:hidden; }
/* ⚠️ THE MODULES THAT ARE DARK IN BOTH THEMES GET THEIR OWN. .sec-shell
   carries its own --sec-* palette precisely because it does not follow
   the page, and a page-coloured scrollbar inside it would be a light
   rail down the side of a dark panel in light mode. Same for the header
   dropdowns, which are chrome. */
.sec-shell {
  scrollbar-color: var(--sec-line) var(--sec-bg);
}

.sec-shell *::-webkit-scrollbar-track,
.sec-shell *::-webkit-scrollbar-corner {
  background: var(--sec-bg);
}

.sec-shell *::-webkit-scrollbar-thumb {
  background: var(--sec-line);
  background-clip: content-box;
}

.sec-shell *::-webkit-scrollbar-thumb:hover {
  background: var(--sec-accent);
  background-clip: content-box;
}

.sec-shell h2 { color: var(--c-on-accent, #ffffff); font-size:1.15rem; margin:0 0 0.4rem; }
.sec-shell .muted { color:var(--sec-dim); }

.sec-screen { flex:1; display:flex; align-items:center; justify-content:center; padding:2rem 1rem; }
.sec-screen.hidden { display:none; }
.sec-screen-inner { max-width:30rem; width:100%; text-align:center; }
.sec-screen-inner.sec-wide { max-width:38rem; }
.sec-hero { font-size:2.5rem; margin-bottom:0.5rem; }
.sec-lede { color:var(--sec-dim); font-size:0.85rem; line-height:1.6; margin:0 0 1.1rem; }
.sec-note { color:var(--sec-dim); font-size:0.76rem; margin:0.5rem 0; }
.sec-current { font-size:0.78rem; }

.sec-features { display:grid; grid-template-columns:1fr 1fr; gap:0.5rem; margin-bottom:1.1rem; text-align:left; }
.sec-feature { background:var(--sec-panel); border-radius:0.5rem; padding:0.55rem 0.75rem;
  font-size:0.78rem; display:flex; gap:0.45rem; align-items:center; }
.sec-plan-card { background:var(--sec-panel); border:1px solid var(--sec-line);
  border-radius:0.75rem; padding:1rem; text-align:left; margin-bottom:0.75rem; }
.sec-plan-head { display:flex; justify-content:space-between; color: var(--c-on-accent, #ffffff); margin-bottom:0.5rem; }
.sec-plan-list { margin:0; padding-left:1.1rem; color:var(--sec-dim); font-size:0.8rem; line-height:1.7; }
.sec-warn { background:color-mix(in srgb, var(--c-warning, #f59e0b) 22%, var(--c-inverse-surface, #0f172a)); border:1px solid color-mix(in srgb, var(--c-orange, #ea580c) 45%, var(--c-inverse-surface, #0f172a)); color:var(--c-warning, #f59e0b); border-radius:0.5rem;
  padding:0.7rem 0.85rem; font-size:0.78rem; line-height:1.55; text-align:left; margin-bottom:1rem; }

.sec-btn { display:inline-block; background:var(--sec-panel); color:var(--sec-text);
  border:1px solid var(--sec-line); border-radius:0.6rem; padding:0.6rem 1.2rem;
  font-size:0.85rem; font-weight:600; font-family:inherit; cursor:pointer; text-decoration:none; }
.sec-btn-primary { background:var(--sec-accent); border-color:var(--sec-accent); color: var(--c-on-accent, #ffffff); }
.sec-btn:disabled { opacity:0.55; cursor:default; }
.sec-actions { display:flex; gap:0.5rem; justify-content:center; flex-wrap:wrap; }
.sec-result { font-size:0.78rem; min-height:1.1rem; margin:0.6rem 0; color:var(--sec-dim); }
.sec-result-error { color:var(--c-on-dark-danger, #fca5a5); }

.sec-words { display:grid; grid-template-columns:repeat(3,1fr); gap:0.5rem; margin-bottom:1rem; }
.sec-word { background:var(--sec-panel); border:1px solid var(--sec-line); border-radius:0.5rem;
  padding:0.45rem; display:flex; flex-direction:column; align-items:center; }
.sec-word-n { font-size:0.6rem; color:var(--sec-dim); }
.sec-word strong { font-size:0.85rem; }
.sec-verify-fields { display:grid; gap:0.6rem; margin-bottom:0.6rem; }
.sec-verify-field { display:flex; align-items:center; gap:0.6rem; }
.sec-verify-field span { flex:0 0 5rem; text-align:right; font-size:0.78rem; color:var(--sec-dim); }
.sec-shell input[type="text"], .sec-shell input[type="search"], .sec-shell textarea {
  flex:1; width:100%; background:var(--sec-panel); border:1px solid var(--sec-line);
  border-radius:0.5rem; color:var(--sec-text); padding:0.5rem 0.65rem; font-family:inherit;
  font-size:0.85rem; }

/* ── the messenger ── */
.sec-main { flex:1; display:grid; grid-template-columns:19rem 1fr; min-height:70vh; overflow:hidden; }
.sec-main.hidden { display:none; }
.sec-rail { border-right:1px solid var(--sec-line); display:flex; flex-direction:column; min-width:0; }
.sec-rail-head { display:flex; align-items:center; justify-content:space-between;
  padding:0.85rem 0.9rem; border-bottom:1px solid var(--sec-line); font-size:0.85rem; }
.sec-rail-search { padding:0.5rem 0.7rem; border-bottom:1px solid var(--sec-line); }
.sec-rail-foot { padding:0.6rem 0.9rem; border-top:1px solid var(--sec-line);
  font-size:0.7rem; color:var(--sec-dim); }
.sec-channel-list { flex:1; overflow-y:auto; }
.sec-list-note { padding:1.5rem 1rem; text-align:center; color:var(--sec-dim); font-size:0.8rem; }

.sec-channel { width:100%; display:flex; gap:0.6rem; align-items:center; text-align:left;
  padding:0.7rem 0.9rem; background:none; border:none; border-bottom:1px solid var(--sec-line);
  color:inherit; font-family:inherit; cursor:pointer; }
.sec-channel:hover, .sec-channel.active { background:var(--sec-panel); }
.sec-avatar { width:2.1rem; height:2.1rem; border-radius:50%; background:var(--sec-line);
  display:flex; align-items:center; justify-content:center; flex:none; }
.sec-channel-body { flex:1; min-width:0; display:flex; flex-direction:column; gap:0.1rem; }
.sec-channel-top, .sec-channel-bottom { display:flex; justify-content:space-between; gap:0.5rem; align-items:baseline; }
.sec-channel-top strong { font-size:0.82rem; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.sec-channel-when { font-size:0.65rem; color:var(--sec-dim); flex:none; }
.sec-channel-bottom span:first-child { font-size:0.72rem; color:var(--sec-dim);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.sec-unread { background:var(--sec-accent); color: var(--c-on-accent, #ffffff); border-radius:2rem; font-size:0.65rem;
  font-weight:800; padding:0 0.35rem; min-width:1.1rem; text-align:center; flex:none; }

.sec-chat { display:flex; flex-direction:column; min-width:0; }
.sec-empty { flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center;
  color:var(--sec-dim); gap:0.15rem; }
.sec-conversation { flex:1; display:flex; flex-direction:column; min-height:0; }
.sec-conversation.hidden { display:none; }
.sec-chat-head { display:flex; align-items:center; justify-content:space-between;
  padding:0.75rem 1rem; border-bottom:1px solid var(--sec-line); }
.sec-chat-who { display:flex; gap:0.6rem; align-items:center; min-width:0; }
.sec-chat-title { display:flex; align-items:center; gap:0.4rem; flex-wrap:wrap; }
.sec-chat-sub { display:block; font-size:0.7rem; }
.sec-badge { font-size:0.62rem; background:color-mix(in srgb, var(--c-success, #16a34a) 30%, var(--c-inverse-surface, #0f172a)); color:var(--c-on-dark-success, #6ee7b7); border-radius:2rem;
  padding:0.1rem 0.4rem; font-weight:700; white-space:nowrap; }

.sec-messages { flex:1; overflow-y:auto; padding:1rem; display:flex; flex-direction:column; gap:0.55rem; }
.sec-msg { display:flex; flex-direction:column; align-items:flex-start; max-width:75%; }
.sec-msg-mine { align-self:flex-end; align-items:flex-end; }
.sec-msg-who { font-size:0.68rem; font-weight:600; color:var(--c-on-dark-brand, #a5b4fc); margin-bottom:0.12rem; }
.sec-bubble { background:var(--sec-panel); border-radius:0.75rem 0.75rem 0.75rem 0;
  padding:0.5rem 0.75rem; font-size:0.83rem; line-height:1.5;
  /* Newlines survive because of this, NOT because of a <br> built from a
     replace() over decrypted text. */
  white-space:pre-wrap; overflow-wrap:anywhere; }
.sec-msg-mine .sec-bubble { background:var(--sec-accent); color: var(--c-on-accent, #ffffff);
  border-radius:0.75rem 0.75rem 0 0.75rem; }
.sec-msg-meta { font-size:0.6rem; color:var(--sec-dim); margin-top:0.12rem; }
.sec-undecryptable { color:var(--c-on-dark-danger, #fca5a5); font-size:0.78rem; }
.sec-file-link { background:none; border:none; color:inherit; font:inherit;
  text-decoration:underline; cursor:pointer; padding:0; text-align:left; }

.sec-pending { display:flex; align-items:center; gap:0.5rem; padding:0.5rem 1rem;
  background:var(--sec-panel); border-top:1px solid var(--sec-line); font-size:0.78rem; }
.sec-pending.hidden { display:none; }
.sec-composer { display:flex; gap:0.5rem; align-items:flex-end; padding:0.7rem 1rem 0.35rem; }
.sec-composer textarea { resize:none; }
.sec-icon-btn { background:var(--sec-panel); color:var(--sec-text); border:1px solid var(--sec-line);
  border-radius:0.5rem; width:2.2rem; height:2.2rem; cursor:pointer; font-size:0.95rem;
  display:flex; align-items:center; justify-content:center; flex:none; font-family:inherit; }
.sec-send { background:var(--sec-accent); border-color:var(--sec-accent); }
.sec-foot-note { text-align:center; font-size:0.63rem; color:var(--c-text-3, #475569); margin:0 0 0.6rem; }

.sec-modal { max-width:26rem; }
.sec-type-row { display:flex; gap:1rem; margin:0.5rem 0; font-size:0.85rem; }
.sec-people-list { max-height:11rem; overflow-y:auto; margin:0.4rem 0; }
.sec-person { display:block; width:100%; text-align:left; background:none; border:none;
  border-bottom:1px solid var(--c-border, #e2e8f0); padding:0.45rem 0.35rem; font:inherit; cursor:pointer; }
.sec-person:hover { background:var(--c-surface-subtle, #f8fafc); }
.sec-picked { display:flex; flex-wrap:wrap; gap:0.3rem; margin-bottom:0.4rem; }
.sec-chip { background:var(--c-brand-tint-2, #eef2ff); border:1px solid var(--c-brand-border, #c7d2fe); border-radius:2rem;
  padding:0.15rem 0.55rem; font-size:0.74rem; cursor:pointer; font-family:inherit; }

.sec-nav-dot { background:var(--c-danger, #ef4444); color: var(--c-on-fill, #ffffff); border-radius:2rem; font-size:0.6rem;
  font-weight:800; padding:0 0.3rem; margin-left:0.35rem; min-width:1rem;
  display:inline-block; text-align:center; }

@media (max-width: 900px) {
  .sec-main { grid-template-columns:1fr; }
  .sec-rail { border-right:0; border-bottom:1px solid var(--sec-line); max-height:16rem; }
  .sec-features, .sec-words { grid-template-columns:1fr 1fr; }
}

/* ── Google Ads ──────────────────────────────────────────────────────── */
.ads-shell { display: flex; flex-direction: column; gap: 1rem; }
.ads-banner { background: var(--c-warning-tint, #fffbeb); border: 1px solid var(--c-warning-border, #fde68a); color: var(--c-warning-text, #92400e);
  border-radius: 0.6rem; padding: 0.6rem 0.85rem; font-size: 0.8rem; margin: 0; }
.ads-banner.hidden { display: none; }
.ads-screen { display: flex; justify-content: center; padding: 2rem 1rem; }
.ads-screen.hidden { display: none; }
.ads-screen-inner { max-width: 32rem; text-align: center; }
.ads-hero { font-size: 2.5rem; }
.ads-loop { text-align: left; margin: 1rem 0; padding-left: 1.1rem; font-size: 0.85rem;
  line-height: 1.9; color: var(--c-text-muted, #64748b); }
.ads-loop span { margin-right: 0.35rem; }
.ads-fine { font-size: 0.72rem; margin-top: 0.5rem; }

.ads-btn { display: inline-block; background: var(--c-surface-subtle, #f8fafc); border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.55rem; padding: 0.5rem 1rem; font-size: 0.82rem; font-weight: 600;
  font-family: inherit; cursor: pointer; color: inherit; text-decoration: none; }
.ads-btn-primary { background: var(--c-brand, #6366f1); border-color: var(--c-brand, #6366f1); color: var(--c-on-fill, #ffffff); }
.ads-btn-sm { padding: 0.28rem 0.6rem; font-size: 0.74rem; }
.ads-btn-off { opacity: 0.55; pointer-events: none; }
.ads-btn:disabled { opacity: 0.55; cursor: default; }

.ads-tabs { display: flex; gap: 0.25rem; border-bottom: 1px solid var(--c-border, #e2e8f0);
  overflow-x: auto; }
.ads-tab { background: none; border: none; border-bottom: 2px solid transparent;
  padding: 0.6rem 0.9rem; font: inherit; font-size: 0.82rem; font-weight: 600;
  color: var(--c-text-muted, #64748b); cursor: pointer; white-space: nowrap; }
.ads-tab.active { border-bottom-color: var(--c-brand, #6366f1); color: var(--c-brand, #6366f1); font-weight: 700; }
.ads-panel { display: flex; flex-direction: column; gap: 1rem; padding-top: 1rem; }
.ads-panel.hidden { display: none; }

.ads-kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
  gap: 0.75rem; }
.ads-kpi { background: var(--c-surface-subtle, #f8fafc); border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.6rem; padding: 0.8rem; display: flex; flex-direction: column; }
.ads-kpi-value { font-size: 1.35rem; font-weight: 800; }
.ads-kpi-label { font-size: 0.7rem; color: var(--c-text-muted, #64748b); text-transform: uppercase;
  letter-spacing: 0.04em; }

.ads-row { display: flex; gap: 0.75rem; align-items: baseline; flex-wrap: wrap;
  padding: 0.5rem 0; border-bottom: 1px solid var(--c-border, #e2e8f0); font-size: 0.82rem; }
.ads-row:last-child { border-bottom: 0; }
.ads-row-test { opacity: 0.7; }
.ads-strong { font-weight: 700; }
.ads-note { font-size: 0.78rem; margin: 0.4rem 0 0; }
.ads-note.error { color: var(--c-danger-deep, #b91c1c); }

.ads-campaign { border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.7rem; padding: 0.9rem;
  margin-bottom: 0.75rem; display: flex; flex-direction: column; gap: 0.4rem; }
.ads-campaign-head { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.ads-status { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.04em;
  background: var(--c-surface-subtle, #f8fafc); border: 1px solid var(--c-border, #e2e8f0); border-radius: 2rem;
  padding: 0.05rem 0.45rem; font-weight: 700; }
.ads-chip { font-size: 0.66rem; font-weight: 700; background: var(--c-brand-tint-2, #eef2ff); color: var(--c-brand-deep, #4338ca);
  border-radius: 2rem; padding: 0.05rem 0.4rem; }
.ads-chip:empty { display: none; }
/* The "auto-texts leads" chip is amber on purpose: it is the one setting
   on this screen that spends money and messages strangers. */
.ads-chip-warn { background: var(--c-warning-tint, #fffbeb); color: var(--c-warning-text, #92400e); }
.ads-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.5rem;
  padding-top: 0.5rem; border-top: 1px solid var(--c-border, #e2e8f0); }
.ads-stats div { display: flex; flex-direction: column; font-size: 0.72rem; }
.ads-actions { display: flex; gap: 0.4rem; flex-wrap: wrap; padding-top: 0.5rem; }

.ads-label { display: block; font-size: 0.68rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--c-text-muted, #64748b); margin-top: 0.6rem; }
.ads-draft-value { margin: 0.1rem 0 0; font-size: 0.82rem; line-height: 1.5; }
.ads-code { display: block; background: var(--c-surface-subtle, #f8fafc); border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.4rem; padding: 0.4rem 0.6rem; font-size: 0.75rem; word-break: break-all;
  margin-top: 0.2rem; }
.ads-check { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.6rem;
  font-size: 0.85rem; }
#tab-ads select, #tab-ads textarea, #tab-ads input[type="number"] { width: 100%; }

@media (max-width: 700px) {
  .ads-stats { grid-template-columns: repeat(2, 1fr); }
}

/* ── Traits ──────────────────────────────────────────────────────────
   A trait chip opens a note panel beneath it, so the chips stack in a
   column rather than flowing like the tag pills do. */
.cs-traits { display: flex; flex-direction: column; gap: 0.5rem; margin-top: 0.6rem; }
.trait-item { display: flex; flex-direction: column; gap: 0.3rem; }
.trait-chip-row { display: flex; align-items: center; gap: 0.3rem; }
.trait-chip { display: inline-flex; align-items: center; gap: 0.35rem;
  background: var(--c-surface-subtle, #f8fafc); border: 1px solid var(--c-border, #e2e8f0); border-radius: 2rem;
  padding: 0.22rem 0.7rem; font: inherit; font-size: 0.8rem; font-weight: 600;
  cursor: pointer; max-width: 100%; }
.trait-chip:hover { filter: brightness(0.97); }
.trait-has-note { font-size: 0.66rem; }
.trait-remove { background: none; border: none; color: var(--c-text-muted, #64748b); font-size: 1rem;
  line-height: 1; cursor: pointer; padding: 0 0.25rem; font-family: inherit; }
.trait-remove:hover { color: var(--c-danger-deep, #b91c1c); }

.trait-note-panel { border: 1px solid var(--c-border, #e2e8f0); border-left: 3px solid var(--c-brand, #6366f1);
  border-radius: 0 0.5rem 0.5rem 0; padding: 0.6rem 0.75rem; display: flex;
  flex-direction: column; gap: 0.3rem; margin-left: 0.5rem; }
.trait-note-panel.hidden { display: none; }
.trait-desc { font-size: 0.72rem; font-style: italic; margin: 0; }
.trait-note-label { font-size: 0.66rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--c-text-muted, #64748b); }
/* The note is set with textContent, so the line breaks somebody typed
   survive here rather than through a <br> built from a replace(). */
.trait-note-body { margin: 0; font-size: 0.82rem; line-height: 1.55; white-space: pre-wrap; }
.trait-stamp { font-size: 0.68rem; }
.trait-edit-btn { align-self: flex-start; font-size: 0.74rem; padding: 0.22rem 0.6rem; }
.trait-note-panel textarea { width: 100%; font-family: inherit; font-size: 0.82rem; }

.trait-create-row { display: flex; gap: 0.4rem; align-items: center; margin: 0.5rem 0; }
.trait-create-row input[type="text"] { flex: 1; min-width: 0; }
.trait-create-row input[type="color"] { width: 2.4rem; height: 2.2rem; padding: 0.1rem;
  border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.4rem; cursor: pointer; flex: none; }
.trait-picker-list { display: flex; flex-direction: column; gap: 0.25rem;
  max-height: 16rem; overflow-y: auto; margin-bottom: 0.6rem; }
.trait-picker-row { display: flex; gap: 0.35rem; align-items: center; }
.trait-picker-pick { flex: 1; display: flex; align-items: center; gap: 0.45rem;
  text-align: left; background: none; border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem; padding: 0.4rem 0.6rem; font: inherit; font-size: 0.82rem;
  cursor: pointer; min-width: 0; }
.trait-picker-pick.on { border-color: var(--c-brand, #6366f1); background: var(--c-brand-tint-2, #eef2ff); font-weight: 600; }
.trait-dot { width: 0.6rem; height: 0.6rem; border-radius: 50%; flex: none; }
.trait-count { margin-left: auto; font-size: 0.7rem; }
.trait-delete { background: none; border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.4rem;
  padding: 0.25rem 0.5rem; font: inherit; font-size: 0.72rem; cursor: pointer;
  color: var(--c-danger-deep, #b91c1c); flex: none; }

/* ── Linxi AI: the persona editor ────────────────────────────────────
   Half and half — the box on the left, what it means on the right. The
   right column is not decoration: it carries the "not wired up" notice,
   which is the most important thing on this screen. */
.lai-persona-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.25rem;
  align-items: start;
  margin-top: 0.5rem;
}
.lai-persona-editor { display: flex; flex-direction: column; min-width: 0; }
.lai-persona-editor textarea {
  width: 100%;
  /* Monospace: this is an instruction block where line breaks and
     indentation are part of the meaning, not prose. */
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.8rem;
  line-height: 1.6;
  resize: vertical;
}
.lai-persona-side { display: flex; flex-direction: column; gap: 0.6rem; min-width: 0; }
.lai-persona-side .muted { font-size: 0.78rem; line-height: 1.55; margin: 0; }
.lai-persona-warn {
  margin: 0;
  padding: 0.6rem 0.75rem;
  border-radius: 0.5rem;
  border: 1px solid var(--c-warning-border, #fde68a);
  background: var(--c-warning-tint, #fffbeb);
  color: var(--c-warning-text, #92400e);
  font-size: 0.78rem;
  line-height: 1.5;
}
.lai-persona-meta {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  margin: 0;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
}
.lai-persona-meta > div { display: flex; justify-content: space-between; gap: 0.75rem; }
.lai-persona-meta dt { font-size: 0.72rem; color: var(--c-text-muted, #64748b); margin: 0; }
.lai-persona-meta dd { margin: 0; font-size: 0.78rem; font-weight: 600; text-align: right; }
.lai-persona-cost { font-size: 0.72rem !important; }
#lai-persona-default { align-self: flex-start; font-size: 0.78rem; }
#lai-persona-default:disabled { opacity: 0.5; cursor: default; }

@media (max-width: 900px) {
  /* Stacked, with the notice ABOVE the box — it is the thing to read
     before typing into it. */
  .lai-persona-row { grid-template-columns: 1fr; }
  .lai-persona-side { order: -1; }
}

/* ── Admin → Site (Landing, Features, Pages, Navigation) ─────────────── */
.toggle-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.45rem 0;
  border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9);
  cursor: pointer;
}
.toggle-row:last-child { border-bottom: none; }
.toggle-row input[type="checkbox"] { flex-shrink: 0; margin: 0; }
.panel-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
  margin-top: 1rem;
}

/* The form builder's foot: Save, then what it did, then Delete pushed to
   the far end.

   ⚠️ `margin-left: auto` rather than letting the two sit as a pair. They
   are not alternatives -- one is the thing people press constantly and
   the other destroys the form -- and a destructive button beside a
   frequent one is how the wrong one gets pressed. */
.sfm-delete-btn { margin-left: auto; }

/* The choice editor: one row per choice, each with its own emoji, name
   and description.

   ⚠️ The emoji box is narrow and FIXED, the name grows, the description
   grows more. Sized by what goes in them -- an emoji is one or two
   characters and a box the width of the name box beside it reads as if a
   sentence belongs there. */
.sfm-choice-row {
  display: flex;
  gap: 0.35rem;
  align-items: center;
  margin-bottom: 0.35rem;
}
.sfm-choice-emoji { flex: 0 0 3.25rem; text-align: center; }
/* ⚠️ min-width: 0 on both, or a long description refuses to shrink and
   pushes the row wider than the panel column. */
.sfm-choice-label { flex: 1 1 9rem; min-width: 0; }
.sfm-choice-desc { flex: 2 1 12rem; min-width: 0; }
.sfm-choice-btn { padding: 0.3rem 0.5rem; line-height: 1; flex: 0 0 auto; }
.sfm-choice-actions { display: flex; gap: 0.4rem; flex-wrap: wrap; align-items: flex-start; }
.sfm-choice-bulk { flex: 1 1 100%; }

/* A switch on its own line in a field row: the box and its words on one
   baseline, the whole thing clickable. */
.sfm-inline {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0.35rem 0;
  font-size: 0.88rem;
  cursor: pointer;
}
.sfm-inline input { margin: 0; }
.sfm-max-choices { width: 5rem; }
/* ⚠️⚠️ `.sfm-colcount`, NOT `.sfm-cols`. `.sfm-cols` IS ALREADY TAKEN --
   it is the form builder's own two-column layout (build on the left,
   snippet and preview on the right), declared further down this file.
   A `width: 12rem` landed on it and squished the ENTIRE editor into a
   12rem column; the later rule won on the grid properties it also sets,
   but nothing else declares `width`, so there was nothing to override
   it. Reported as "the UI is all squished together", and it is the same
   family as `#tab-analytics` vs `#tab-admin-analytics`: a name that
   already means something else.

   Its own width so the row does not jump between "One under another" and
   "4 side by side" -- a control that resizes as you use it reads as a
   layout fault. */
.sfm-colcount { width: 12rem; max-width: 100%; }

@media (max-width: 720px) {
  /* Four controls on one line is unusable at this width. The row stacks,
     but the emoji keeps its own width beside the name rather than
     stretching across -- a full-width box for one character is the same
     mistake the other way. */
  .sfm-choice-row { flex-wrap: wrap; }
  .sfm-choice-desc { flex: 1 1 100%; }
}

/* ⚠️ The confirmation is MUTED by default and only takes a colour when
   it has something to say: a permanently coloured line beside a button is
   one people stop seeing, which is the whole failure mode of a status
   that is always on. */
.sfm-saved {
  font-size: 0.8rem;
  color: var(--c-text-muted, #64748b);
  min-height: 1em;
}
.sfm-saved.is-ok { color: var(--c-success-text, #166534); font-weight: 600; }
.sfm-saved.is-error { color: var(--c-danger, #dc2626); font-weight: 600; }

@media (max-width: 560px) {
  /* Wrapped, `margin-left: auto` puts Delete alone on its own line, which
     is if anything better -- but it must not stretch across the row. */
  .sfm-delete-btn { margin-left: 0; }
}
.site-page-row,
.site-nav-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.6rem;
  margin-bottom: 0.5rem;
}
.site-page-main { flex: 1; min-width: 0; }
.site-page-title { display: flex; align-items: center; gap: 0.45rem; flex-wrap: wrap; }
.site-page-title code {
  font-size: 0.72rem;
  color: var(--c-text-muted, #64748b);
  background: var(--c-surface-sunken, #f1f5f9);
  padding: 0.05rem 0.3rem;
  border-radius: 0.3rem;
}
.site-page-badge {
  font-size: 0.62rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 0.1rem 0.4rem;
  border-radius: 2rem;
  background: var(--c-warning-tint-2, #fef3c7);
  color: var(--c-warning-text, #92400e);
}
.site-page-badge.is-live {
  background: var(--c-success-tint-2, #dcfce7);
  color: var(--c-success-text, #166534);
}
/* The "also send to" list.
   ⚠️⚠️ CHECKBOXES, NOT A <select multiple>. A multi-select shows the whole
   list with nothing marked and browsers draw the first row as though it
   were chosen -- so it read as "one of these is already selected" when
   nothing was. It also needs ctrl-click for a second, which nobody
   discovers. A tick is a tick. */
.sfm-target-list {
  box-sizing: border-box;
  max-height: 11rem;
  overflow-y: auto;
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--c-border);
  border-radius: 8px;
  background: var(--c-surface);
}
.sfm-target-list > p { margin: 0.25rem 0; }
/* The whole row is the hit target -- the label wraps the box. */
.sfm-target-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.3rem 0.15rem;
  cursor: pointer;
  /* ⚠️ The panel's label rule is a block with its own weight; these are
     rows in a list, not headings for the control below them. */
  font-weight: 400;
  margin: 0;
}
.sfm-target-row:hover { color: var(--c-brand-deep); }
/* ⚠️ flex: none, or the box is stretched by the row it is in. */
.sfm-target-row input[type="checkbox"] { flex: 0 0 auto; margin: 0; }
/* A heading for the list, where a <label for> would have nothing single
   to point at. */
.sfm-field-label {
  display: block;
  font-weight: 600;
  margin: 0.6rem 0 0.25rem;
}
#sfm-targets-note, #wfm-targets-note { margin: 0.35rem 0 0.6rem; }

/* Which surface built a form, and where its leads land.
   ⚠️ NEITHER IS A STATUS. The badge beside them says whether the form is
   accepting, which is amber or green; these two are facts ABOUT the
   form, so they are neutral -- borrowing either of those colours would
   make "System Linxi" read as a warning or an all-clear. */
.site-page-badge.is-site {
  background: var(--c-brand-tint);
  color: var(--c-brand-deep);
}
.site-page-badge.is-linxi {
  background: var(--c-surface-subtle);
  color: var(--c-text-muted);
  border: 1px solid var(--c-border);
  /* ⚠️ A Linxi's name is typed by its owner and is not upper-cased like
     the status words -- "SYSTEM LINXI" is shouting somebody's own name
     back at them, and a long one in caps wraps the row. */
  text-transform: none;
  letter-spacing: 0;
  font-weight: 600;
}

/* ⚠️ AMBER, NEVER RED. An empty page is a state to notice, not a failure
 * -- a draft with nothing in it yet is the ordinary way one starts. Red
 * would read as "this is broken" on a page somebody is halfway through
 * writing. The wording carries the severity instead: the badge says
 * "live and blank" only when it actually is. */
.site-page-badge.is-empty {
  background: color-mix(in srgb, var(--c-warning) 16%, transparent);
  color: var(--c-warning-text);
  border-color: color-mix(in srgb, var(--c-warning) 40%, transparent);
}
.site-page-summary { margin: 0 0 0.75rem; }

.site-page-actions { display: flex; gap: 0.35rem; flex-shrink: 0; align-items: center; }
.site-page-actions a.secondary { text-decoration: none; }
/* Label grows least, URL most -- the URL is the field people actually read. */
.site-nav-row input[type="text"] { min-width: 0; }
.site-nav-row input[type="text"]:first-child { flex: 1 1 8rem; }
.site-nav-row input[type="text"]:nth-child(2) { flex: 2 1 12rem; }
.site-nav-row select { flex: 0 0 auto; }
.site-nav-row button { flex: 0 0 auto; padding: 0.3rem 0.55rem; }
@media (max-width: 640px) {
  .site-page-row, .site-nav-row { flex-direction: column; align-items: stretch; }
}

/* ── Admin → Site → Domain ───────────────────────────────────────────── */
.dns-option { margin: 0.9rem 0 1.1rem; }
.dns-option h4 {
  margin: 0 0 0.45rem;
  font-size: 0.82rem;
  font-weight: 700;
  color: var(--c-text-2, #334155);
}
.dns-record {
  display: flex;
  align-items: flex-end;
  gap: 0.75rem;
  flex-wrap: wrap;
  padding: 0.55rem 0.75rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.55rem;
  margin-bottom: 0.4rem;
}
.dns-field { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.dns-field-label {
  font-size: 0.62rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--c-text-muted, #64748b);
}
.dns-field-value {
  font-size: 0.8rem;
  background: var(--c-surface-sunken, #f1f5f9);
  padding: 0.2rem 0.45rem;
  border-radius: 0.3rem;
  /* An IP or a hostname must be selectable in one go, and must not wrap
     mid-value where somebody could copy half of it by hand. */
  white-space: nowrap;
  overflow-x: auto;
  user-select: all;
}
.dns-record button { margin-left: auto; flex-shrink: 0; }

.domain-status {
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.6rem;
  padding: 0.3rem 0.85rem;
  margin: 0.5rem 0;
}
.domain-status-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--c-surface-sunken, #f1f5f9);
}
.domain-status-row:last-child { border-bottom: none; }
.domain-status-label {
  flex: 0 0 3rem;
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--c-text-muted, #64748b);
}
.domain-status-value { display: flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; }
/* The emoji carries the same meaning as the colour, so this reads in
   forced-colors and to anyone who cannot distinguish the two. */
.domain-status-value.is-ok { color: var(--c-success-text, #166534); }
.domain-status-value.is-bad { color: var(--c-danger-darkest, #991b1b); }
.domain-status-value.is-wait { color: var(--c-text-muted, #64748b); }
.domain-status-icon { font-style: normal; }
@media (max-width: 640px) {
  .dns-record { flex-direction: column; align-items: stretch; }
  .dns-record button { margin-left: 0; }
}

/* ── Site owner tab ──────────────────────────────────────────────────── */
.callout {
  margin-top: 1.25rem;
  padding: 0.9rem 1.1rem;
  border: 1px solid var(--c-info-border, #bfdbfe);
  background: var(--c-info-tint, #eff6ff);
  border-radius: 0.65rem;
}
.callout h3 { margin: 0 0 0.4rem; font-size: 0.85rem; font-weight: 700; }
.callout p { margin: 0 0 0.5rem; font-size: 0.82rem; }
.callout p:last-child { margin-bottom: 0; }

/* ── Account switching ────────────────────────────────────────────────
   The "operating as" banner, the nav switcher rows, and the account
   group cards. The banner offset is a CLASS on <body>, never an inline
   style set from JS -- an inline paddingTop survives the banner being
   removed and leaves a permanent gap at the top of the page. */

.switch-banner {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 99999;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  padding: 0.4rem 1rem;
  font-size: 0.78rem;
  font-weight: 700;
  background: var(--c-sky, #0ea5e9);
  color: var(--c-on-fill, #ffffff);
  text-align: center;
}
.switch-banner-impersonation { background: var(--c-violet, #7c3aed); }
.switch-banner-btn {
  background: color-mix(in srgb, var(--c-highlight, #ffffff) 22%, transparent);
  border: none;
  color: var(--c-on-accent, #ffffff);
  padding: 0.2rem 0.7rem;
  border-radius: 2rem;
  cursor: pointer;
  font-size: 0.75rem;
  font-weight: 700;
  flex-shrink: 0;
}
.switch-banner-btn:hover { background: color-mix(in srgb, var(--c-highlight, #ffffff) 34%, transparent); }
/* ⚠️ THE MEASURED HEIGHT, with 34px only as the value before the
   script has run. accountSwitcher sets --switch-banner-h from the
   banner's real offsetHeight and keeps it in step on resize. */
body.has-switch-banner { padding-top: var(--switch-banner-h, 34px); }

/* ⚠️⚠️ padding-top MOVES THE PAGE AND NOTHING ELSE. A `position: fixed`
   element is positioned against the viewport, not against the body box,
   so every full-height drawer on this install opened UNDERNEATH the
   banner -- which sits at z-index 99999. In the page builder that hid the
   entire header strip: Save draft, Publish, View page, the version
   picker and the close button, all of them unreachable while operating
   as another account. Reported exactly that way.

   ⚠️ THE HEIGHT IS MEASURED, not guessed. --switch-banner-h is set by
   accountSwitcher from the banner's real offsetHeight, so the body
   padding and every drawer move by exactly what is there -- including
   when the sentence wraps to two lines on a narrow screen, which a
   hard-coded 34px would have been wrong by a whole line.

   ⚠️ AND EVERY FULL-HEIGHT FIXED OVERLAY IS LISTED, not just the builder.
   They all have the same geometry and the same problem; fixing only the
   one that was reported would leave the next person to find it in the
   request drawer, the AI panels or the workspace rail. */
body.has-switch-banner .coms-builder,
body.has-switch-banner .coms-ai-panel,
body.has-switch-banner .campaign-ai-panel,
body.has-switch-banner .cal-ai-drawer,
body.has-switch-banner .request-drawer,
body.has-switch-banner .ws-settings-rail,
body.has-switch-banner #links-section.links-direct-open {
  top: var(--switch-banner-h, 34px);
}
/* ⚠️ `inset: 0` SETS top AS WELL, so the builder needs its bottom kept at
   0 explicitly -- the shorthand is what the rule above is overriding. */
body.has-switch-banner .coms-builder { bottom: 0; }

.user-menu-section { border-top: 1px solid var(--border, var(--c-border, #e2e8f0)); margin-top: 0.25rem; padding-top: 0.25rem; }
.user-menu-section-title {
  padding: 0.25rem 0.65rem;
  font-size: 0.66rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  opacity: 0.6;
}
.user-menu-section-note {
  padding: 0.25rem 0.65rem;
  font-size: 0.72rem;
  opacity: 0.65;
  /* Carries a user-typed display name ("Linked to <name>"). */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.user-menu-account { display: flex; align-items: center; gap: 0.6rem; }
.user-menu-account-icon { flex-shrink: 0; font-size: 0.95rem; }
.user-menu-account-text { display: flex; flex-direction: column; min-width: 0; text-align: left; }
.user-menu-account-name { font-size: 0.82rem; font-weight: 500; }
.user-menu-account-email,
.muted-small { font-size: 0.7rem; opacity: 0.65; }
.user-menu-account-name,
.user-menu-account-email { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.account-link-list { display: flex; flex-direction: column; gap: 0.5rem; margin: 0.75rem 0; }
.account-link-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.65rem 0.75rem;
  border: 1px solid var(--border, var(--c-border, #e2e8f0));
  border-radius: 0.65rem;
}
.account-link-icon { font-size: 1.1rem; flex-shrink: 0; }
.account-link-text { flex: 1; min-width: 0; }
.account-link-name { display: flex; align-items: center; gap: 0.4rem; font-weight: 600; font-size: 0.86rem; }
.account-link-email { font-size: 0.72rem; opacity: 0.65; }
.account-link-remove { flex-shrink: 0; }

.account-group-card {
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.75rem;
  padding: 0.85rem 1rem;
  margin-bottom: 0.65rem;
  background: var(--c-surface, #ffffff);
}
.account-group-head { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.account-group-member {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 0;
  font-size: 0.82rem;
  border-top: 1px solid var(--c-border, #e2e8f0);
  margin-top: 0.4rem;
}
.account-group-member > .muted-small { flex: 1; min-width: 0; overflow-wrap: anywhere; }

/* ⚠️ `.callout-warn` AND `.badge-amber` WERE USED AND DEFINED NOWHERE.
   The first is the "N admin views active right now" banner, which was
   therefore painted by `.callout` alone -- INFO BLUE. Somebody operating
   as a customer right this second is not information, and a neutral box
   is the one thing that notice must not be. The second marks the primary
   account in a group and in the switcher (5 call sites) and was rendering
   as an ordinary badge, so the account that can switch into the others
   looked exactly like the ones it can switch into. */
.callout-warn {
  border-color: var(--c-warning-border, #fde68a);
  background: var(--c-warning-tint, #fffbeb);
  color: var(--c-warning-text, #b45309);
}
.callout-warn strong { display: block; margin-bottom: 0.3rem; }
.callout-warn .muted-small { color: var(--c-warning-text, #b45309); opacity: 0.85; }
.badge-amber {
  background: var(--c-warning, #f59e0b);
  color: var(--c-on-status, #0f172a);
}

.master-range { display: flex; gap: 0.35rem; }
/* ⚠️ `hidden` needs a display override here, because .master-range sets
   `display: flex` and an attribute-driven [hidden] loses to a class rule
   on specificity. The range strip is hidden in the two states where there
   is nothing to range over -- no group, and a group of one -- and a strip
   of dead buttons over an empty state reads as a broken page. */
.master-range.hidden { display: none; }
.btn-impersonate { color: var(--c-violet, #7c3aed); border-color: var(--c-violet-border, #ddd6fe); }

/* ── Master View ────────────────────────────────────────────────────
   Cards stack with the same gap the rail pages use, so this page reads
   like the rest of the app rather than like a report. */
#master-view-content > * + * { margin-top: 1.5rem; }

.mv-explainer-list { list-style: none; margin: 1rem 0 0; padding: 0; display: grid; gap: 0.9rem; }
.mv-explainer-item { display: flex; gap: 0.75rem; align-items: flex-start; }
.mv-explainer-icon { font-size: 1.25rem; line-height: 1.4; flex: none; }
.mv-explainer-text strong { display: block; font-size: 0.95rem; }

/* The direction rule. Tinted rather than plain, because it is the one
   paragraph somebody has to read before accepting an invitation -- and
   BRAND-tinted rather than amber: it is how the feature works, not a
   warning that something is wrong. */
.mv-note {
  margin-top: 1.1rem;
  padding: 0.85rem 1rem;
  border-radius: 10px;
  border: 1px solid var(--c-brand-border, var(--c-border, #e2e8f0));
  background: color-mix(in srgb, var(--c-brand, #6366f1) 7%, transparent);
  font-size: 0.88rem;
  line-height: 1.55;
}
.mv-note strong { display: block; margin-bottom: 0.15rem; }

.mv-roster { display: grid; gap: 0.6rem; margin-top: 1rem; }
.mv-roster-row {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  padding: 0.75rem 0.9rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 10px;
  background: var(--c-surface, #ffffff);
}
.mv-roster-icon { font-size: 1.5rem; line-height: 1; flex: none; width: 2rem; text-align: center; }
/* ⚠️ min-width: 0 or a long email refuses to shrink and pushes the
   actions off the row -- a flex item will not go below its content
   width without it. */
.mv-roster-main { flex: 1 1 auto; min-width: 0; }
.mv-roster-main input { width: 100%; }
.mv-roster-name { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.mv-roster-actions { display: flex; gap: 0.4rem; flex: none; }
.mv-roster-editing { align-items: flex-start; }
.mv-icon-input { width: 3.5rem; flex: none; text-align: center; font-size: 1.15rem; }

.mv-danger { color: var(--c-danger, #dc2626); border-color: var(--c-danger-border, #fecaca); }
.mv-danger-card { margin-top: 1.5rem; }
.mv-error { color: var(--c-danger, #dc2626); }

/* A table of seven columns does not fit a phone. It scrolls inside its
   own container rather than making the PAGE scroll sideways. */
.mv-table-scroll { overflow-x: auto; }

@media (max-width: 640px) {
  .mv-roster-row { flex-wrap: wrap; }
  .mv-roster-main { flex-basis: 100%; order: 2; }
  .mv-roster-actions { order: 3; flex-basis: 100%; }
  .mv-roster-actions button { flex: 1 1 auto; }
}

/* REQ-0031: the admin dropdown carries the longest entries in the app
   ("Admin Settings", "Analytics", "Support") and was sized for the user
   menu's shorter ones, so labels wrapped onto a second row. Widened
   here rather than on .user-menu-dropdown so the user menu keeps its
   own measure -- one shared width would size both for the longest
   label in either. */
#admin-menu-dropdown {
  min-width: 15rem;
  white-space: nowrap;
}
#admin-menu-dropdown .user-menu-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  white-space: nowrap;
}
/* The open-request count sits on the Support entry now that its rail
   button is gone. Pushed to the right so it does not ride against the
   label. */
#admin-menu-dropdown .request-badge { margin-left: auto; }

/* REQ-0029: the reply-method line under the channel picker. Muted and
   small -- it is a confirmation, not a control, and must not compete
   with the buttons above it. */
/* ⚠️ NO BOTTOM MARGIN. It is the last thing in the reply box now that
   the character counter is gone, so anything here is added to the 4px
   the container already leaves -- two rules quietly deciding one gap. */
.reply-channel-note {
  margin: 0.3rem 0 0;
  font-size: 0.72rem;
  opacity: 0.7;
}

/* ── Brand Profile (migration 080) ───────────────────────────────── */

.bp-completion { margin-bottom: 1.25rem; }
.bp-completion-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem; }
.bp-completion-pct { font-weight: 700; font-size: 0.9rem; }

.bp-group { border: 1px solid var(--border, var(--c-border, #e2e8f0)); border-radius: 0.75rem; padding: 1rem 1.15rem; margin: 0 0 1rem; }
.bp-group legend { font-weight: 700; font-size: 0.82rem; padding: 0 0.35rem; }
.bp-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem 1rem; }
.bp-field { display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; }
.bp-field input, .bp-field select, .bp-field textarea { width: 100%; box-sizing: border-box; }
.bp-field textarea { resize: vertical; font-family: inherit; }
@media (max-width: 760px) { .bp-grid { grid-template-columns: 1fr; } }

.bp-tier { margin-top: 0.75rem; }
.bp-tier:empty { display: none; }

.bp-verification { border: 1px solid var(--border, var(--c-border, #e2e8f0)); border-radius: 0.75rem; padding: 1rem 1.15rem; margin-bottom: 1.25rem; }
.bp-steps { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.5rem; margin: 0.65rem 0; }
.bp-step { display: flex; align-items: center; gap: 0.4rem; font-size: 0.76rem; padding: 0.5rem 0.6rem; border: 1px solid var(--border, var(--c-border, #e2e8f0)); border-radius: 0.5rem; }
.bp-step-mark { font-weight: 700; }
.bp-step-done { border-color: var(--c-success-border, #bbf7d0); }
@media (max-width: 760px) { .bp-steps { grid-template-columns: 1fr 1fr; } }

.bp-logo-row { display: flex; align-items: center; gap: 0.85rem; flex-wrap: wrap; }
.bp-logo-frame { width: 72px; height: 72px; border: 2px dashed var(--border, var(--c-border, #e2e8f0)); border-radius: 0.65rem; display: flex; align-items: center; justify-content: center; overflow: hidden; font-size: 0.68rem; opacity: 0.7; flex-shrink: 0; }
.bp-logo-frame img { max-width: 100%; max-height: 100%; object-fit: contain; }

.bp-handle-card, .bp-documents { border: 1px solid var(--border, var(--c-border, #e2e8f0)); border-radius: 0.75rem; padding: 1rem 1.15rem; margin-top: 1.25rem; }
.bp-handle-row { display: flex; align-items: center; gap: 0.4rem; margin: 0.5rem 0; }
.bp-handle-at { font-weight: 700; opacity: 0.6; }
.bp-handle-row input { flex: 1; min-width: 0; }
.bp-handle-result { font-size: 0.78rem; min-height: 1.2em; margin: 0.25rem 0; }
.bp-handle-result.is-free { color: var(--c-success-text, #166534); font-weight: 600; }
.bp-handle-result.is-taken { color: var(--c-danger-deep, #b91c1c); }
.bp-handle-held { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.5rem; font-size: 1.05rem; }

.bp-doc-list { display: flex; flex-direction: column; gap: 0.4rem; margin-top: 0.75rem; }
.bp-doc-row { display: flex; align-items: center; gap: 0.6rem; padding: 0.55rem 0.7rem; border: 1px solid var(--border, var(--c-border, #e2e8f0)); border-radius: 0.55rem; }
.bp-doc-text { flex: 1; min-width: 0; }
.bp-doc-text > div { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Partner dashboard */
.pd-key { display: block; word-break: break-all; padding: 0.75rem; margin: 0.75rem 0;
  border: 1px solid var(--border, var(--c-border, #e2e8f0)); border-radius: 0.5rem; font-size: 0.8rem; }
.checkbox-row { display: flex; align-items: center; gap: 0.45rem; margin: 0.5rem 0; font-size: 0.85rem; }

/* E2E Secure launcher on the Linxis thread row.
   Pushed to the far right with a divider, because it is not a filter on
   this conversation -- the pills to its left narrow what is shown, this
   one opens a different module. (There are up to seven of them now, not
   the four this said when it was written.)

   ⚠️ margin-left:auto claimed the row "still collapses sensibly on
   narrow screens" and it did not: with no free space an auto margin
   resolves to zero and collapses nothing, so on a phone this simply ran
   off the side. The row wraps now, and the auto margin does what the
   sentence always described. */
/* ⚠️ THE GROUP CARRIES THE AUTO MARGIN, NOT THE BUTTONS. Two flex
   children each asking for margin-left:auto SPLIT the free space between
   them, so Direct would have sat in the middle of the row with E2E at
   the end -- not "Direct on the left, E2E on the right" but "Direct
   adrift". One wrapper takes the space; the pair inside it stays a pair.

   ⚠️ AND THE DIVIDER MOVED WITH IT. It marks where the filters stop and
   the things that are not filters begin, so it belongs on the group. On
   the second button it drew a line between two controls that do the same
   KIND of job, and vanished whenever the first one was hidden. */
.thread-row-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-left: auto;
  padding-left: 0.75rem;
  border-left: 1px solid var(--c-border, #e2e8f0);
}

.thread-secure-btn,
.thread-direct-btn {
  border-radius: 0;
  white-space: nowrap;
}

/* ⚠️ THE GROUP MUST NOT BE THE THING THAT WRAPS AWKWARDLY. .channel-toggle
   wraps (see the note on it), and when it does, this pair should move as
   one rather than leaving Direct on one line and E2E on the next. */
.thread-row-actions { flex-shrink: 0; }
.thread-secure-btn .sec-nav-dot { margin-left: 0.35rem; }

/* Under 560px the divider reads as a stray line once the row wraps, and
   a full-width launcher is easier to hit than one pinned to an edge. */
@media (max-width: 560px) {
  .thread-secure-btn {
    margin-left: 0;
    border-left: none;
    padding-left: 0;
    width: 100%;
  }
}

/* "SMS Available" — proof the number does SMS, shown as a pill rather
   than the raw `has_sms_traffic` value the profile used to print.
   Green because it is the one thing in the carrier row that is evidence
   rather than a carrier's guess about the line type. */
.sms-available-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  background: var(--c-success-tint, #f0fdf4);
  color: var(--c-success-text, #166534);
}
.sms-available-dot {
  width: 0.45rem;
  height: 0.45rem;
  border-radius: 50%;
  background: var(--c-success, #16a34a);
  flex-shrink: 0;
}
/* The dot carries no information the label does not, so it is the label
   that survives when colour is unavailable — it is aria-hidden in the
   markup for the same reason. */
@media (forced-colors: active) {
  .sms-available-dot { background: CanvasText; }
}

/* ── Theme editor (Admin → Branding → Theme & Colours) ─────────────── */
.theme-block { border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.75rem;
  padding: 1rem 1.15rem; margin-bottom: 1rem; }

/* ⚠️ AN UNSTYLED <input> IS ABOUT TWENTY CHARACTERS WIDE, whatever the
   container. Nothing sized the Site Branding fields at all, so a 300
   character description was being typed through a box the width of the
   word "communication" -- inside a panel several times wider. Not a
   layout that squeezed them; a width that was never set.

   ⚠️ ITS OWN CLASS, not `.theme-block input`. That block is shared with
   Theme & Colours, whose controls are swatches, radios and colour
   pickers -- stretching those to full width would be a worse bug than
   the one being fixed.

   max-width, because 100% alone puts a 300-character description on one
   ~120-character line on a wide monitor, which is past the measure
   anybody reads comfortably. */
.admin-text-field {
  display: block;
  width: 100%;
  max-width: 44rem;
  padding: 0.45rem 0.6rem;
  font-family: inherit;
  font-size: 0.95rem;
  color: var(--c-text, #0f172a);
  background: var(--c-surface, #ffffff);
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 6px;
  margin-bottom: 0.9rem;
}
textarea.admin-text-field { resize: vertical; line-height: 1.5; }
.admin-text-field:focus { outline: none; border-color: var(--c-brand, #6366f1); }
.theme-mode-row { display: flex; gap: 0.4rem; flex-wrap: wrap; margin: 0.5rem 0; }
.theme-mode-row .active { border-color: var(--c-brand, #6366f1); color: var(--c-brand, #6366f1); }
.theme-preset-row { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.6rem; }
.theme-preset { display: inline-flex; align-items: center; gap: 0.45rem; }
.theme-preset-dot { width: 0.85rem; height: 0.85rem; border-radius: 50%; flex-shrink: 0;
  border: 1px solid color-mix(in srgb, var(--c-shadow, #000000) 15%, transparent); }
.theme-grid { display: flex; flex-direction: column; gap: 0.4rem; margin-top: 0.75rem; }
/* A heading per family. Sticky, because the list is long enough that
   scrolling into the middle of it otherwise leaves you with no idea
   which family you are looking at. */
.theme-group {
  position: sticky;
  top: 0;
  z-index: 1;
  margin: 0.9rem 0 0.1rem;
  padding: 0.3rem 0.1rem;
  background: var(--c-surface, #fff);
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--c-text-muted, #64748b);
}
.theme-group:first-child { margin-top: 0; }
.theme-row { display: flex; align-items: center; gap: 0.6rem;
  padding: 0.4rem 0.55rem; border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.5rem; }
/* A changed token is marked, so "what have I actually altered" is
   answerable at a glance rather than by comparing 34 values by eye. */
.theme-row.is-custom { border-color: var(--c-brand, #6366f1); }
.theme-swatch { width: 2.25rem; height: 2rem; padding: 0; border: none;
  background: none; cursor: pointer; flex-shrink: 0; }
.theme-label { flex: 1; min-width: 0; }
.theme-label code { font-size: 0.68rem; opacity: 0.7; }
.theme-hex { width: 7.5rem; font-family: ui-monospace, monospace; font-size: 0.78rem; }
.theme-reset-one { flex-shrink: 0; }
@media (max-width: 620px) {
  .theme-row { flex-wrap: wrap; }
  .theme-hex { width: 100%; }
}

/* Network / Feed toggle. A segmented control, not two loose buttons:
   they are alternatives to each other, and the shared border says so. */
.view-toggle { display: inline-flex; border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem; overflow: hidden; margin-bottom: 1rem; }
.view-toggle-btn { background: none; color: var(--c-text-muted, #64748b);
  border: none; border-radius: 0; padding: 0.45rem 0.9rem;
  font-size: 0.82rem; font-weight: 600; box-shadow: none; }
.view-toggle-btn + .view-toggle-btn { border-left: 1px solid var(--c-border, #e2e8f0); }
.view-toggle-btn:hover { background: var(--c-surface-subtle, #f8fafc); box-shadow: none; }
/* `.active` is set by initTabs on whichever tab is open, so the
   highlight follows the actual view rather than a second piece of state
   that can drift from it. */
.view-toggle-btn.active { background: var(--c-brand-tint, #f5f3ff);
  color: var(--c-brand, #6366f1); }
/* The feed toolbar puts its sort buttons on the right; the toggle takes
   the left, where the heading used to be. */
.feed-toolbar .view-toggle { margin-bottom: 0; }

/* Calendar scope: which calendar you are looking at. */
.cal-overlay-row { display: flex; align-items: center; gap: 0.4rem;
  padding: 0.35rem 0.6rem; font-size: 0.78rem; cursor: pointer; }

/* The AI's name is editable in place, inside the same <strong> as the
   verified badge.

   ⚠️ A VISIBLE BORDER. This was `border: 1px solid transparent`, appearing
   only on hover, and the comment here used to defend that: "chromeless
   until focused rather than a boxed input". The consequence was that the
   one control which renames the assistant across the whole install read
   as a heading -- it arrived as "the user should be able to edit the site
   ai name here" about a field that already could be. An editable box has
   to look editable; the inverse rule (a read-only value must not render
   as a disabled input) is recorded elsewhere in this file.

   ⚠️⚠️ AND EVERYTHING HERE USES THE ON-DARK FAMILY, NEVER --c-surface.
   These sit inside .lai-profile, a brand gradient card carrying
   `color: var(--c-on-accent)` -- WHITE IN BOTH MODES, like the header and
   the E2E module. Giving the input `background: var(--c-surface)` while it
   inherited that white left white-on-white in LIGHT mode: the name was
   invisible on the one screen that exists to change it. Dark mode hid it,
   because --c-surface is dark there.

   That is this file's own rule inverted -- "--c-surface is never a text
   colour, --c-text is never a background" -- and the general form is that
   a token PAIR has to be used together. A themed surface under fixed
   on-accent text is a contrast bug that only appears in one mode.

   ⚠️ The :focus rule had the same fault BEFORE any of this, and worse:
   `background: var(--c-surface)` with inherited white meant focusing the
   field made it unreadable in light mode. It survived only because the
   field looked like a heading and nobody clicked it.

   --c-highlight holds the same value in both modes, which is what makes
   these tints safe: they cannot flip. .lai-email, .lai-role and
   .lai-verified already build from it. */
.lai-name-input {
  font: inherit;
  color: inherit;
  background: color-mix(in srgb, var(--c-highlight, #ffffff) 16%, transparent);
  border: 1px solid color-mix(in srgb, var(--c-highlight, #ffffff) 45%, transparent);
  border-radius: 6px;
  padding: 2px 6px;
  max-width: 22ch;
}
.lai-name-input::placeholder {
  color: color-mix(in srgb, var(--c-highlight, #ffffff) 60%, transparent);
}
.lai-name-hint {
  display: block;
  font-size: 0.78rem;
  font-weight: 400;
  /* On the card too -- --c-text-muted is a dark grey in light mode and
     would be the same bug one line further down. Matches .lai-email. */
  color: color-mix(in srgb, var(--c-highlight, #ffffff) 75%, transparent);
  margin-top: 0.25rem;
}
.lai-name-input:hover {
  border-color: color-mix(in srgb, var(--c-highlight, #ffffff) 70%, transparent);
}
.lai-name-input:focus {
  /* A real ring rather than `outline: none`: a text field on a busy
     gradient with the outline removed leaves keyboard users nothing. */
  outline: 2px solid color-mix(in srgb, var(--c-highlight, #ffffff) 80%, transparent);
  outline-offset: 1px;
  background: color-mix(in srgb, var(--c-highlight, #ffffff) 26%, transparent);
  border-color: color-mix(in srgb, var(--c-highlight, #ffffff) 85%, transparent);
}

/* The Linxi name and the controls that change it, on one line. Edit used to
   live in the toolbar above and Save/Cancel at the far end of the form, which
   left a greyed-out field with no visible way to change it. */
.ws-name-label {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
}
.ws-name-actions {
  display: inline-flex;
  gap: 0.35rem;
  flex-shrink: 0;
}

/* ── Mobile: the section rails become a slide-out drawer ──────────────────
   Nine rails share .ws-settings-layout. The @media (max-width: 900px) block
   further up flattens .ws-settings-rail into a horizontal strip above the
   panel -- that is what put the tabs at the top of the page. These rules come
   later in the file, so at equal specificity they win, and the flattening is
   replaced with an off-canvas drawer.

   The drawer is position:fixed, so it OVERLAYS: opening it does not reflow
   the page beneath, which is the difference between an overlay and a sidebar
   that shoves the content sideways.

   It stops at 5/6 of the viewport on purpose. The visible strip of page is
   what makes it read as a drawer over the current screen rather than a
   navigation away from it, and it gives an obvious place to tap to dismiss. */

.rail-drawer-toggle,
.rail-drawer-backdrop { display: none; }

@media (max-width: 900px) {
  .rail-drawer-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    margin: 0 0 0.75rem;
    padding: 0.55rem 0.9rem;
    font: inherit;
    font-weight: 600;
    color: var(--c-text, #0f172a);
    background: var(--c-surface, #fff);
    border: 1px solid var(--c-border, #e2e8f0);
    border-radius: 0.6rem;
    cursor: pointer;
  }

  .ws-settings-rail {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    /* 5/6 of the viewport, capped so it does not become absurd on a tablet
       in portrait. The remaining sixth stays visible and tappable. */
    width: 83.3333%;
    max-width: 22rem;
    height: 100dvh;
    /* Undo the row flattening from the earlier breakpoint. */
    flex-direction: column;
    overflow-x: hidden;
    overflow-y: auto;
    /* -webkit-overflow-scrolling for momentum on older iOS, which otherwise
       scrolls a fixed-position drawer in one abrupt jump. */
    -webkit-overflow-scrolling: touch;
    margin: 0;
    border: none;
    border-right: 1px solid var(--c-border, #e2e8f0);
    border-radius: 0;
    padding: 0.75rem 0;
    background: var(--c-surface, #fff);
    box-shadow: 0 0 2rem color-mix(in srgb, var(--c-shadow, #000000) 25%, transparent);
    z-index: 1000;
    transform: translateX(-100%);
    transition: transform 0.22s ease;
    /* Off-canvas means out of the tab order and out of the a11y tree, or a
       swipe/tab lands on links nobody can see. */
    visibility: hidden;
  }

  .ws-settings-layout.rail-open .ws-settings-rail {
    transform: translateX(0);
    visibility: visible;
  }

  .rail-drawer-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    background: color-mix(in srgb, var(--c-scrim, #0f172a) 45%, transparent);
    z-index: 999;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.22s ease;
  }

  .ws-settings-layout.rail-open .rail-drawer-backdrop {
    opacity: 1;
    pointer-events: auto;
  }

  /* Full width inside the drawer, whatever the earlier breakpoint set for the
     horizontal strip. */
  .ws-settings-layout.rail-open .ws-rail-btn,
  .ws-settings-rail .ws-rail-btn { width: 100%; }
}

@media (max-width: 900px) and (prefers-reduced-motion: reduce) {
  .ws-settings-rail,
  .rail-drawer-backdrop { transition: none; }
}

/* Inline rename on the Linxi overview. The input takes the heading's place
   rather than sitting beside it, so the row does not jump when it opens. */
.ws-overview-name-input {
  font-size: 1.25rem;
  font-weight: 700;
  padding: 0.25rem 0.5rem;
  border: 1px solid var(--c-brand, #6366f1);
  border-radius: 0.4rem;
  background: var(--c-surface, #fff);
  color: var(--c-text, #0f172a);
  min-width: 12rem;
  flex: 1 1 auto;
}

/* Reset Linxi Number — the last block on the Linxi Number panel.
   ADD ONLY; nothing above was changed.

   Separated by a rule rather than a card of its own: it belongs to this
   panel (it resets what the panel configures) but it undoes the work
   above it, so it must not read as one more setting in the list.

   Every colour goes through a token. --c-danger-tint is the same wash
   the other destructive surfaces use, so this follows a rebrand and
   follows the light/dark switch instead of freezing at one palette. */
.ws-number-reset {
  margin-top: 2rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--c-border, #e2e8f0);
}

.ws-number-reset h3 {
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--c-danger, #dc2626);
  margin: 0 0 0.5rem;
}

.ws-number-reset > p.muted {
  margin-bottom: 0.75rem;
  max-width: 62ch;
}

/* The three "Not" lines. Tinted rather than plain list items because
   each one corrects an assumption somebody is about to act on, and a
   grey bullet under a grey paragraph is read as more of the paragraph. */
.ws-number-reset-list {
  list-style: none;
  margin: 0 0 1rem;
  padding: 0.75rem 1rem;
  max-width: 62ch;
  border: 1px solid var(--c-danger-border, #fecaca);
  border-left: 3px solid var(--c-danger, #dc2626);
  border-radius: 0.5rem;
  background: var(--c-danger-tint, #fef2f2);
  font-size: 0.85rem;
  line-height: 1.6;
}

.ws-number-reset-list li + li {
  margin-top: 0.5rem;
}

.ws-number-reset-list strong {
  color: var(--c-text, #0f172a);
}

@media (max-width: 640px) {
  .ws-number-reset-list { padding: 0.75rem; }
}

/* Delete Linxi Number — the heavier of the two actions on the reset
   block, shown only when the Linxi actually holds a Linxi-managed
   number. Nested inside .ws-number-reset, indented and rule-marked so it
   reads as a second, separate decision rather than a variant of Reset. */
.ws-number-delete {
  margin-top: 1.25rem;
  padding: 1rem;
  border: 1px solid var(--c-danger-border, #fecaca);
  border-radius: 0.5rem;
  background: var(--c-danger-tint, #fef2f2);
  max-width: 62ch;
}

.ws-number-delete h4 {
  font-size: 0.9rem;
  font-weight: 700;
  color: var(--c-danger, #dc2626);
  margin: 0 0 0.5rem;
}

/* The number itself, so the heading names what is about to go. */
.ws-number-delete-num {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-weight: 600;
  color: var(--c-text, #0f172a);
}

.ws-number-delete > p {
  font-size: 0.85rem;
  line-height: 1.6;
  margin-bottom: 0.75rem;
}

/* The A2P-brand explanation. Given its own emphasis because it is the
   non-obvious half: everyone understands that releasing a number stops
   the number, and almost nobody knows a Sole Proprietor brand is
   registered against the PERSON and outlives it. */
.ws-number-delete-why {
  font-size: 0.85rem;
  line-height: 1.6;
  color: var(--c-text, #0f172a);
  background: var(--c-surface, #fff);
  border-left: 3px solid var(--c-danger, #dc2626);
  border-radius: 0.35rem;
  padding: 0.625rem 0.75rem;
  margin-bottom: 0.875rem;
}

/* Per-step teardown report. Four things across two systems, each with
   its own outcome -- an overall "done" would hide the case this feature
   exists for: number released, brand still registered. */
.ws-number-teardown-report {
  margin-top: 1.25rem;
  padding: 1rem;
  max-width: 62ch;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
  background: var(--c-surface-subtle, #f8fafc);
}

.ws-number-teardown-report h4 {
  font-size: 0.9rem;
  font-weight: 700;
  margin: 0 0 0.75rem;
  color: var(--c-text, #0f172a);
}

.ws-number-teardown-report ul {
  list-style: none;
  margin: 0 0 0.875rem;
  padding: 0;
}

.teardown-step {
  display: grid;
  grid-template-columns: 1.5rem 1fr;
  gap: 0 0.5rem;
  font-size: 0.85rem;
  line-height: 1.55;
  padding: 0.375rem 0;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
}

.teardown-step:last-child { border-bottom: 0; }

.teardown-mark { text-align: center; }

.teardown-failed strong { color: var(--c-danger, #dc2626); }
.teardown-deleted strong { color: var(--c-success, #16a34a); }
.teardown-skipped strong { color: var(--c-text-muted, #64748b); }

.teardown-detail {
  grid-column: 2;
  font-size: 0.8rem;
  color: var(--c-text-muted, #64748b);
}

.teardown-note {
  font-size: 0.85rem;
  line-height: 1.6;
  margin: 0;
  padding: 0.625rem 0.75rem;
  border-radius: 0.35rem;
}

.teardown-note-ok {
  color: var(--c-success-text, #166534);
  background: var(--c-success-tint, #f0fdf4);
  border: 1px solid var(--c-success-border, #bbf7d0);
}

.teardown-note-warn {
  color: var(--c-danger-deep, #991b1b);
  background: var(--c-danger-tint, #fef2f2);
  border: 1px solid var(--c-danger-border, #fecaca);
}

/* "My Contacts" — the This Linxi / All my Linxis scope switch.
   ADD ONLY; the default mode is unchanged. */
.contacts-scope-switch {
  display: inline-flex;
  gap: 0;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
  overflow: hidden;
}

.contacts-scope-switch .log-filter-pill {
  border: 0;
  border-radius: 0;
  margin: 0;
}

.contacts-scope-switch .log-filter-pill + .log-filter-pill {
  border-left: 1px solid var(--c-border, #e2e8f0);
}

#contacts-ws-filter {
  max-width: 16rem;
}

/* The note under the toolbar. It carries either the cap warning or the
   one-row-per-record explanation — both are things somebody has to read
   BEFORE they trust the row count, so it sits above the table. */
.contacts-scope-note {
  margin: 0 0 0.75rem;
  font-size: 0.8rem;
  line-height: 1.55;
  max-width: 78ch;
}

/* The Linxi cell. A chip rather than plain text so a column of repeated
   names reads as a grouping, and the row you are "in" is marked — with a
   border weight and a label, never colour alone. */
.contacts-ws-chip {
  display: inline-block;
  font-size: 0.75rem;
  line-height: 1.4;
  padding: 0.15rem 0.5rem;
  border-radius: 2rem;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc);
  color: var(--c-text-muted, #64748b);
  white-space: nowrap;
  max-width: 14rem;
  overflow: hidden;
  text-overflow: ellipsis;
  vertical-align: middle;
}

.contacts-ws-chip.is-active-ws {
  border-color: var(--c-brand, #6366f1);
  color: var(--c-brand-deep, #4338ca);
  background: var(--c-brand-tint, #eef2ff);
  font-weight: 600;
}

@media (max-width: 640px) {
  .contacts-scope-switch { width: 100%; }
  .contacts-scope-switch .log-filter-pill { flex: 1 1 0; }
  #contacts-ws-filter { max-width: 100%; width: 100%; }
}

/* Reset / Delete Linxi Number, folded into a disclosure.
   The card, the summary row and the ▾ come from .composer-card /
   .directive-card unchanged — this only neutralises the standalone
   spacing .ws-number-reset carried when it WAS the last block on the
   panel, and tints the head so a destructive section does not read as
   one more collapsed setting. */
.ws-number-danger {
  margin-top: 2rem;
  border-color: var(--c-danger-border, #fecaca);
}

.ws-number-danger > summary {
  background: var(--c-danger-tint, #fef2f2);
  border-bottom-color: var(--c-danger-border, #fecaca);
}

.ws-number-danger > summary strong { color: var(--c-danger-deep, #b91c1c); }

/* Its own rule to the container, so the divider is the card edge. */
.ws-number-danger .ws-number-reset {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

/* Step 1's carrier picker: the recommended carrier WIDE, "Other" a
   small square beside it, and Twilio one click behind Other in
   .carrier-other-panel below.

   The width is the message. This step has one obvious answer — Telnyx
   is cheaper on the number AND roughly half the price per message — so
   the recommended card keeps its full-width treatment and everything
   else is a compact door out of it. Two equal boxes, whether both
   rectangles or both squares, put the two carriers back on the same
   footing, which is exactly what this arrangement is for. */
/* ⚠️ .carrier-grid.carrier-grid-lead, not .carrier-grid-lead alone.
   .carrier-grid's own max-width:700px rule sets `grid-template-columns:
   1fr` to stack the two rectangles, and at equal specificity the later
   rule in the file wins -- which is source order deciding a layout, the
   same trap that kept the "Main Workspace" badge permanently visible
   (see the comment in workspaceSettings.js). Two classes beats one at
   any position, so the squares stay side by side wherever this block
   ends up. */
.carrier-grid.carrier-grid-lead {
  /* Telnyx takes the room; Other is only as wide as it needs to be.
     `auto` rather than a second 1fr — two equal columns is what made the
     recommended option look like one of two interchangeable choices. */
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: stretch;
  max-width: 34rem;
}

/* Only "Other" wears this now. A small square beside the wide
   recommended card — sized by its own width rather than by half the
   grid, so it stays compact however wide the row gets. */
.carrier-square {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: 0.15rem;
  width: 7rem;
  padding: 0.75rem 0.5rem;
}

.carrier-square-icon { font-size: 1.6rem; line-height: 1; margin-bottom: 0.15rem; }

.carrier-square-name {
  font-weight: 700;
  font-size: 0.95rem;
  color: var(--c-text, #0f172a);
}

.carrier-square .carrier-option-tag { margin: 0.1rem 0; }

.carrier-square-price {
  font-weight: 700;
  font-size: 0.9rem;
  color: var(--c-text, #0f172a);
}

.carrier-square-rate {
  font-size: 0.72rem;
  line-height: 1.35;
  color: var(--c-text-muted, #64748b);
}

/* "Other" is a disclosure, not a carrier — dashed so it never reads as
   a third thing you could have selected, and it never takes .active. */
.carrier-square-other {
  border-style: dashed;
}

.carrier-square-chevron {
  font-size: 0.75rem;
  color: var(--c-text-dim, #94a3b8);
  transition: transform 0.15s;
  margin-top: 0.15rem;
}

.carrier-square-other.is-open .carrier-square-chevron { transform: rotate(180deg); }

.carrier-other-panel {
  max-width: 30rem;
  margin-top: 0.75rem;
}

@media (max-width: 700px) {
  /* Stays a pair, never stacked: the row is a wide card plus a small
     door, and stacking would give "Other" a full-width box of its own
     and put the two back on the same footing. */
  .carrier-grid.carrier-grid-lead { grid-template-columns: minmax(0, 1fr) auto; }
  .carrier-square { width: 5.75rem; padding: 0.6rem 0.4rem; }
  .carrier-square-icon { font-size: 1.35rem; }
  .carrier-square-name { font-size: 0.82rem; }
}

/* The chosen number, restated at the top of step 2 — the thing being
   bought, on the screen where the tier is picked. Centred and large
   because it is the one fact somebody is checking before they commit to
   a monthly price. */
.mnw-plan-number {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.2rem;
  text-align: center;
  margin: 0.25rem 0 1rem;
  padding: 1rem;
  border: 1px solid var(--c-brand-border, #c7d2fe);
  border-radius: 0.7rem;
  background: var(--c-brand-tint, #eef2ff);
}

.mnw-plan-number-label {
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--c-brand-deep, #4338ca);
}

/* Tabular figures so the digits sit on a fixed pitch — a phone number
   rendered in a proportional face reads as prose, not as an identifier. */
.mnw-plan-number-value {
  font-size: 1.85rem;
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -0.01em;
  color: var(--c-text, #0f172a);
  font-variant-numeric: tabular-nums;
}

.mnw-plan-number-meta {
  font-size: 0.78rem;
  color: var(--c-text-muted, #64748b);
}

/* The same number on the bill, so the total and the line it is for are
   never on screen without each other. */
.mnw-bill-number span:last-child {
  font-weight: 700;
  color: var(--c-text, #0f172a);
  font-variant-numeric: tabular-nums;
}

@media (max-width: 600px) {
  .mnw-plan-number-value { font-size: 1.5rem; }
  .mnw-plan-number { padding: 0.8rem; }
}

/* The header carrier badge's two non-committed states.
   ⚠️ `.carrier-badge` is declared TWICE in this file for two unrelated
   features — the header badge (~9058) and the number-lookup row
   (~10304) — and the later one wins every property both set, so the
   header badge is actually painted by the lookup rule. Left alone here:
   untangling it changes how the lookup badges look, which is a separate
   decision. These modifiers are last in the file so they win over both,
   whichever ends up applying. */

/* Setup in progress: a carrier has been picked but nothing bought. */
.carrier-badge-pending {
  background: var(--c-warning-tint, #fffbeb);
  color: var(--c-warning-text, #92400e);
  border-color: var(--c-warning-border, #fde68a);
}

/* Nothing chosen at all. Muted, because it is the absence of a fact
   rather than a fact — it should not read as loudly as a real carrier. */
.carrier-badge-unset {
  background: var(--c-surface-subtle, #f8fafc);
  color: var(--c-text-muted, #64748b);
  border-color: var(--c-border, #e2e8f0);
  font-weight: 600;
}

/* A2P filings — what was actually submitted to a carrier, above the
   editable Linxi-local list. Read-only, and visually distinct from it so
   the two are never mistaken for one list with one status. */
.a2p-filings { margin-bottom: 1.25rem; }

.a2p-filings-head {
  font-size: 0.85rem;
  font-weight: 700;
  margin: 0 0 0.25rem;
  color: var(--c-text, #0f172a);
}

.a2p-filings-note { font-size: 0.75rem; margin: 0 0 0.6rem; }

.a2p-filing {
  border: 1px solid var(--c-border, #e2e8f0);
  border-left: 3px solid var(--c-text-dim, #94a3b8);
  border-radius: 0.5rem;
  padding: 0.7rem 0.85rem;
  margin-bottom: 0.6rem;
  background: var(--c-surface-subtle, #f8fafc);
  font-size: 0.82rem;
}

.a2p-filing-approved { border-left-color: var(--c-success, #16a34a); }
.a2p-filing-failed { border-left-color: var(--c-danger, #dc2626); }

.a2p-filing-top {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.35rem;
}

.a2p-filing-status { font-weight: 700; color: var(--c-text, #0f172a); }

.a2p-filing-carrier,
.a2p-filing-usecase {
  font-size: 0.7rem;
  padding: 0.1rem 0.45rem;
  border-radius: 2rem;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface, #fff);
  color: var(--c-text-muted, #64748b);
}

.a2p-filing-desc { margin: 0 0 0.35rem; color: var(--c-text-3, #475569); }

/* The rejection reason — the single most useful thing on this panel,
   and previously visible nowhere in the app. */
.a2p-filing-error {
  margin: 0.35rem 0;
  padding: 0.45rem 0.6rem;
  border-radius: 0.35rem;
  background: var(--c-danger-tint, #fef2f2);
  color: var(--c-danger-deep, #b91c1c);
  border: 1px solid var(--c-danger-border, #fecaca);
  font-size: 0.78rem;
  line-height: 1.5;
}

.a2p-filing-samples summary {
  cursor: pointer;
  font-size: 0.76rem;
  color: var(--c-text-muted, #64748b);
  padding: 0.2rem 0;
}

.a2p-filing-samples ul {
  margin: 0.3rem 0 0;
  padding-left: 1.1rem;
  font-size: 0.76rem;
  color: var(--c-text-3, #475569);
}

.a2p-filing-samples li { margin-bottom: 0.3rem; }

.a2p-filing-optin {
  font-size: 0.76rem;
  color: var(--c-text-muted, #64748b);
  margin: 0.4rem 0 0;
}

/* The carrier ids, as a definition list: four of them with names that
   differ per carrier, so a run-on line of values would be unreadable. */
.a2p-filing-ids {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 0.1rem 0.6rem;
  margin: 0.5rem 0 0;
  font-size: 0.68rem;
}

.a2p-filing-ids dt {
  color: var(--c-text-muted, #64748b);
  white-space: nowrap;
}

.a2p-filing-ids dd {
  margin: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--c-text-dim, #94a3b8);
  word-break: break-all;
}

/* Use-case cards: the EIN condition.
   ⚠️ NOT the filled green of .usecase-badge. That one says
   "Recommended" — a recommendation — and "Requires EIN" is a
   CONDITION, in several cases a disqualifying one. Rendering it in the
   same positive pill would read as another endorsement, and on the
   MIXED card the two sit side by side where the difference has to be
   obvious at a glance. Outlined, and amber for the requirement. */
.usecase-badge-ein,
.usecase-badge-noein {
  background: transparent;
  font-weight: 600;
  border: 1px solid;
  /* Beside "Recommended" rather than under it when the card is wide
     enough; each keeps the base rule's own top margin when they wrap. */
  margin-right: 0.3rem;
}

.usecase-badge-ein {
  color: var(--c-warning-text, #92400e);
  border-color: var(--c-warning-border, #fde68a);
  background: var(--c-warning-tint, #fffbeb);
}

/* The inverse, on the one card that must NOT carry an EIN — a sole
   proprietor brand is registered against a person, and sending one
   anyway is what got a real filing rejected as "not well-formed". */
.usecase-badge-noein {
  color: var(--c-text-muted, #64748b);
  border-color: var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc);
}

/* Step 4: whose policy pages? A choice, not a silent default —
   generating a Linxi page publishes the customer's business details at a
   public URL, which is the point and is why it is asked. */
.policy-source {
  display: grid;
  gap: 0.5rem;
  margin: 0.35rem 0 0.75rem;
}

@media (min-width: 720px) {
  .policy-source { grid-template-columns: 1fr 1fr; }
}

.policy-source-opt {
  display: flex;
  align-items: flex-start;
  gap: 0.55rem;
  padding: 0.75rem 0.85rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.6rem;
  background: var(--c-surface, #fff);
  cursor: pointer;
}

.policy-source-opt:hover { border-color: var(--c-border-strong, #cbd5e1); }

/* :has, with a plain fallback: the border is a nicety and the radio
   itself still shows the selection everywhere. */
.policy-source-opt:has(input:checked) {
  border-color: var(--c-brand, #6366f1);
  background: var(--c-brand-tint, #eef2ff);
}

.policy-source-opt input { margin-top: 0.15rem; flex: none; }
.policy-source-name { display: block; font-weight: 700; font-size: 0.85rem; color: var(--c-text, #0f172a); }
.policy-source-desc { display: block; font-size: 0.75rem; line-height: 1.45; color: var(--c-text-muted, #64748b); margin-top: 0.15rem; }

/* What gets published. Amber rather than red: this is a disclosure
   somebody is agreeing to, not a warning that something went wrong. */
.policy-consent {
  border: 1px solid var(--c-warning-border, #fde68a);
  background: var(--c-warning-tint, #fffbeb);
  border-radius: 0.6rem;
  padding: 0.85rem 1rem;
  margin-top: 0.25rem;
}

.policy-consent-head {
  font-weight: 700;
  font-size: 0.8rem;
  margin: 0 0 0.4rem;
  color: var(--c-warning-text, #92400e);
}

.policy-consent-list {
  margin: 0 0 0.6rem;
  padding-left: 1.1rem;
  font-size: 0.78rem;
  line-height: 1.55;
  color: var(--c-text-3, #475569);
}

.policy-consent-list li { margin-bottom: 0.3rem; }
.policy-consent-list strong { color: var(--c-text, #0f172a); }

.policy-consent-note {
  font-size: 0.75rem;
  color: var(--c-text-muted, #64748b);
  margin: 0 0 0.6rem;
}

.policy-consent-tick {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  font-size: 0.8rem;
  line-height: 1.5;
  color: var(--c-text, #0f172a);
  cursor: pointer;
}

.policy-consent-tick input { margin-top: 0.15rem; flex: none; }

/* Use-case explainer. The BOX is .mnw-samples-note, reused unchanged —
   it is the neutral bordered-and-muted note already used one field
   below, in this same panel. No new component: an amber box here would
   read as a warning when this is guidance, and the app has no general
   info/alert component to reach for. */
.mnw-usecase-explainer { margin: 0.6rem 0 0; }

.mnw-usecase-explainer-body { margin: 0; }

/* The limits/price line and the match reminder, quieter than the
   guidance above them. */
.mnw-usecase-explainer-small {
  margin: 0.5rem 0 0;
  padding-top: 0.5rem;
  border-top: 1px solid var(--c-border, #e2e8f0);
  font-size: 0.72rem;
  color: var(--c-text-dim, #94a3b8);
}

/* A2P compliance pre-check. Advisory: no score, no tick, no green
   "passed" state — the clear case is muted, not celebratory, because
   nothing here predicts approval. The result box is .mnw-samples-note
   reused, like the use-case explainers. */
.mnw-check { margin: 1rem 0 0; }
.mnw-check-lede { font-size: 0.72rem; line-height: 1.4; max-width: 44ch; }
.mnw-check-result { margin-top: 0.6rem; }

.mnw-check-summary {
  margin: 0;
  font-weight: 700;
  font-size: 0.82rem;
  color: var(--c-text, #0f172a);
}

/* Blockers red, warnings amber, clear MUTED — deliberately not green.
   Green reads as a pass, and this cannot pass anything. */
.mnw-check-summary.is-blocked { color: var(--c-danger-deep, #b91c1c); }
.mnw-check-summary.is-warn { color: var(--c-warning-text, #92400e); }
.mnw-check-summary.is-clear { color: var(--c-text-muted, #64748b); }

.mnw-check-page-note,
.mnw-check-none {
  margin: 0.4rem 0 0;
  font-size: 0.74rem;
  line-height: 1.5;
  color: var(--c-text-muted, #64748b);
}

.mnw-check-list { list-style: none; margin: 0.7rem 0 0; padding: 0; }

.mnw-check-issue {
  border-left: 3px solid var(--c-border-strong, #cbd5e1);
  padding: 0.45rem 0 0.45rem 0.7rem;
  margin-bottom: 0.7rem;
}

.mnw-check-blocker { border-left-color: var(--c-danger, #dc2626); }
.mnw-check-warning { border-left-color: var(--c-warning-border, #fde68a); }

.mnw-check-issue-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  margin: 0 0 0.2rem;
}

.mnw-check-sev {
  font-size: 0.63rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 0.05rem 0.4rem;
  border-radius: 2rem;
  border: 1px solid var(--c-border, #e2e8f0);
  color: var(--c-text-muted, #64748b);
}

.mnw-check-blocker .mnw-check-sev {
  color: var(--c-danger-deep, #b91c1c);
  border-color: var(--c-danger-border, #fecaca);
  background: var(--c-danger-tint, #fef2f2);
}

.mnw-check-warning .mnw-check-sev {
  color: var(--c-warning-text, #92400e);
  border-color: var(--c-warning-border, #fde68a);
  background: var(--c-warning-tint, #fffbeb);
}

/* A <button>, so it opts out of the app-wide gradient treatment
   explicitly — the same trap the plan cards fell into. */
.mnw-check-jump {
  background: none;
  border: 0;
  box-shadow: none;
  transform: none;
  padding: 0;
  font: inherit;
  font-weight: 700;
  font-size: 0.78rem;
  color: var(--c-brand-deep, #4338ca);
  text-decoration: underline;
  cursor: pointer;
}

.mnw-check-jump:hover:not(:disabled) {
  background: none;
  box-shadow: none;
  transform: none;
  color: var(--c-brand, #6366f1);
}

.mnw-check-field { font-weight: 700; font-size: 0.78rem; color: var(--c-text, #0f172a); }
.mnw-check-rule { font-size: 0.68rem; color: var(--c-text-dim, #94a3b8); }

.mnw-check-problem { margin: 0; font-size: 0.78rem; line-height: 1.5; color: var(--c-text-3, #475569); }
.mnw-check-fix { margin: 0.25rem 0 0; font-size: 0.78rem; line-height: 1.5; color: var(--c-text, #0f172a); }

/* Which required field is still missing. Named rather than left to a
   disabled button with no explanation. */
.mnw-check-need {
  margin: 0.35rem 0 0;
  font-size: 0.74rem;
  line-height: 1.45;
}

/* What the check actually cost, shown with the result. Quiet: it is a
   receipt, not the answer. */
.mnw-check-charge {
  margin: 0.25rem 0 0;
  font-size: 0.72rem;
  color: var(--c-text-dim, #94a3b8);
}

/* Advisory notes under step-4 fields — a bad-looking policy URL, or a
   sample missing a link/phone while both boxes are ticked. Amber, not
   red: neither blocks anything. */
.mnw-field-note {
  margin: 0.3rem 0 0;
  font-size: 0.73rem;
  line-height: 1.45;
  color: var(--c-warning-text, #92400e);
}

/* The campaign status, shown rather than chosen. Styled as a value, not
   as a disabled input — a greyed-out dropdown still reads as something
   you could enable. */
.a2p-status-readonly {
  margin: 0;
  padding: 0.6rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
  background: var(--c-surface-subtle, #f8fafc);
  font-size: 0.825rem;
  color: var(--c-text, #0f172a);
}

/* ── AI compliance-check fee dialogue ──────────────────────────────────
   Its own modal rather than the shared confirmDelete() popover: that one
   is textContent-only (deliberately -- it is used by every delete on the
   Contacts tab and must never render markup), so a price, a balance
   ledger and a caveat could only ever arrive as one grey paragraph.
   Every colour is a token, so it follows the palette and dark mode. */
.fee-modal { max-width: 420px; padding: 0; overflow: hidden; }

.fee-modal-head {
  display: flex; align-items: center; gap: 0.75rem;
  padding: 1.25rem 1.35rem 1rem;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
}
.fee-modal-glyph {
  width: 2.5rem; height: 2.5rem; flex: 0 0 2.5rem;
  display: flex; align-items: center; justify-content: center;
  font-size: 1.25rem; border-radius: 0.7rem;
  background: var(--c-brand-tint, #eef2ff);
}
.fee-modal-head h3 { margin: 0; font-size: 0.98rem; font-weight: 700; }
.fee-modal-head p {
  margin: 0.1rem 0 0; font-size: 0.75rem; color: var(--c-text-muted, #64748b);
}

.fee-modal-body { padding: 1.1rem 1.35rem 0; }

/* The price and the ledger sit on one row: what it costs, and what that
   leaves. Two facts about the same number, so they belong together. */
.fee-modal-price {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; padding: 0.85rem 1rem; border-radius: 0.7rem;
  background: var(--c-surface-subtle, #f8fafc);
  border: 1px solid var(--c-border, #e2e8f0);
}
.fee-modal-amount { font-size: 1.6rem; font-weight: 800; line-height: 1; }
.fee-modal-amount span {
  display: block; font-size: 0.68rem; font-weight: 600; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--c-text-muted, #64748b);
  margin-top: 0.3rem;
}
.fee-modal-ledger { text-align: right; font-size: 0.78rem; }
.fee-modal-ledger div { display: flex; justify-content: space-between; gap: 0.75rem; }
.fee-modal-ledger dt { color: var(--c-text-muted, #64748b); }
.fee-modal-ledger dd { margin: 0; font-weight: 700; font-variant-numeric: tabular-nums; }
.fee-modal-ledger .fee-after dd { color: var(--c-brand, #6366f1); }

.fee-modal-reads { margin: 0.9rem 0 0; padding: 0; list-style: none; }
.fee-modal-reads li {
  display: flex; gap: 0.5rem; align-items: flex-start;
  font-size: 0.8rem; padding: 0.2rem 0;
}
.fee-modal-reads li::before {
  content: '✓'; color: var(--c-success, #10b981); font-weight: 800; line-height: 1.45;
}

/* ⚠️ AMBER, NOT GREEN. This says what the check CANNOT do; a success
   colour on it reads as reassurance, which is the opposite. */
.fee-modal-caveat {
  margin: 0.9rem 0 0; font-size: 0.76rem; line-height: 1.45;
  padding: 0.55rem 0.7rem; border-radius: 0.5rem;
  color: var(--c-warning-text, #92400e);
  background: var(--c-warning-tint, #fffbeb);
  border: 1px solid var(--c-warning-border, #fde68a);
}

.fee-modal-actions {
  display: flex; gap: 0.6rem; padding: 1.1rem 1.35rem 1.35rem;
}
.fee-modal-actions button { flex: 1; }

@media (max-width: 420px) {
  .fee-modal-price { flex-direction: column; align-items: flex-start; }
  .fee-modal-ledger { text-align: left; width: 100%; }
}

/* ── AI compliance-check progress ──────────────────────────────────────
   ⚠️ INDETERMINATE ON PURPOSE. The run is one server call that fetches
   two policy pages and then waits on a model; nothing reports how far
   through it is. A bar creeping to 90% and stopping there is a lie that
   makes a working request look stuck, so the fill sweeps and the honest
   signal is the phase label above it. */
.check-progress {
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.7rem;
  padding: 0.9rem 1rem;
  background: var(--c-surface-subtle, #f8fafc);
}
.check-progress-label {
  margin: 0 0 0.55rem;
  font-size: 0.82rem;
  font-weight: 600;
  display: flex; align-items: center; gap: 0.45rem;
}
.check-progress-label::before {
  content: '🤖';
  font-size: 0.95rem;
}
.check-progress-track {
  height: 6px;
  border-radius: 3px;
  overflow: hidden;
  background: color-mix(in srgb, var(--c-brand, #6366f1) 14%, transparent);
}
.check-progress-bar {
  height: 100%;
  width: 35%;
  border-radius: 3px;
  background: var(--c-brand, #6366f1);
  animation: check-progress-sweep 1.4s ease-in-out infinite;
}
@keyframes check-progress-sweep {
  0%   { margin-left: -35%; }
  100% { margin-left: 100%; }
}
/* A sweeping bar with no way to stop it is exactly what somebody who set
   this preference asked not to see. Static, still clearly "in progress"
   because the label above it says so. */
@media (prefers-reduced-motion: reduce) {
  .check-progress-bar { animation: none; width: 100%; opacity: 0.55; }
}
.check-progress-note {
  margin: 0.5rem 0 0;
  font-size: 0.72rem;
  color: var(--c-text-muted, #64748b);
}

/* ── Public signup form on /@username ──────────────────────────────────
   Rendered for logged-out visitors, so every colour is a token and the
   card stands on its own rather than inheriting the app shell. */
.lead-form-card {
  background: var(--c-surface, #fff);
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.9rem;
  padding: 1.35rem;
  margin-top: 1.5rem;
}
.lead-form-title { margin: 0; font-size: 1rem; font-weight: 700; }
.lead-form-sub {
  margin: 0.25rem 0 1rem;
  font-size: 0.82rem;
  color: var(--c-text-muted, #64748b);
}
.lead-form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.75rem;
}
@media (max-width: 560px) { .lead-form-grid { grid-template-columns: 1fr; } }
.lead-field { display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; }
.lead-field label { font-size: 0.75rem; font-weight: 600; }
.lead-field input {
  width: 100%;
  padding: 0.55rem 0.65rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
  font-size: 0.875rem;
  background: var(--c-surface, #fff);
  color: var(--c-text, #0f172a);
}

.lead-consent-block {
  margin-top: 1rem;
  padding: 0.85rem 0.95rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.6rem;
  background: var(--c-surface-subtle, #f8fafc);
}
.lead-consent-biz { font-size: 0.85rem; font-weight: 700; }
/* The campaign description sits above the boxes, and is the thing a
   person should read before ticking one. */
.lead-consent-desc {
  margin: 0.3rem 0 0.7rem;
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--c-text-muted, #64748b);
}
.lead-consent-row {
  display: flex;
  align-items: flex-start;
  gap: 0.55rem;
  padding: 0.35rem 0;
  cursor: pointer;
}
.lead-consent-row input { margin-top: 0.2rem; flex: 0 0 auto; cursor: pointer; }
/* ⚠️ Not muted and not shrunk further. This is the sentence somebody is
   legally agreeing to; burying it in grey 10px is the pattern this form
   exists to avoid. */
.lead-consent-text { font-size: 0.78rem; line-height: 1.5; }

.lead-form-actions { margin-top: 1.1rem; }
.lead-form-submit {
  width: 100%;
  padding: 0.65rem 1rem;
  border: none;
  border-radius: 0.55rem;
  font-size: 0.9rem;
  font-weight: 600;
  cursor: pointer;
  background: var(--c-brand, #6366f1);
  color: var(--c-on-accent, #fff);
}
.lead-form-submit:disabled { opacity: 0.6; cursor: default; }
.lead-form-result {
  margin: 0.6rem 0 0;
  font-size: 0.8rem;
  text-align: center;
  color: var(--c-text-muted, #64748b);
}
.lead-form-result.is-error { color: var(--c-danger-deep, #991b1b); }

/* The Linxi picker in the Edit Profile modal. */
.lead-ws-list { display: flex; flex-direction: column; gap: 0.2rem; margin-bottom: 0.9rem; }
.lead-ws-row {
  display: flex; align-items: center; gap: 0.5rem;
  font-size: 0.85rem; padding: 0.3rem 0; cursor: pointer;
}

/* ── The signup dock ───────────────────────────────────────────────────
   ⚠️ NOT a card in the thread. It is somebody's profile page; a signup
   form sitting between their posts reads as an ad. Docked bottom-right,
   collapsed until asked for. */
.lead-form-dock {
  position: fixed;
  right: 1.25rem;
  bottom: 1.25rem;
  z-index: 900;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.6rem;
  /* The dock is only as wide as the panel; without this the fixed box
     spans the viewport and swallows clicks on the page behind it.
     760px: the form carries two consent sentences and a campaign
     description, and at 380 they read as a column of wrapped prose. */
  max-width: min(760px, calc(100vw - 2.5rem));
}
.lead-dock-launcher {
  border: none;
  border-radius: 2rem;
  padding: 0.7rem 1.15rem;
  font-size: 0.875rem;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
  background: var(--c-brand, #6366f1);
  color: var(--c-on-accent, #fff);
  box-shadow: 0 6px 24px color-mix(in srgb, var(--c-shadow, #000) 22%, transparent);
}
.lead-dock-panel {
  width: min(760px, calc(100vw - 2.5rem));
  max-height: min(72vh, 640px);
  overflow-y: auto;
  border-radius: 0.9rem;
  box-shadow: 0 12px 40px color-mix(in srgb, var(--c-shadow, #000) 28%, transparent);
}
/* The card supplies its own border and padding; inside the dock it is
   the panel that rounds and scrolls. */
.lead-form-dock .lead-form-card { margin-top: 0; border-radius: 0.9rem; }
.lead-form-head {
  display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
}
.lead-dock-close {
  background: none; border: none; cursor: pointer;
  font-size: 1rem; line-height: 1; padding: 0.2rem;
  color: var(--c-text-muted, #64748b);
}

/* Under 560px it becomes a full-width sheet at the bottom -- a 760px
   panel pinned to one corner of a phone is most of the screen anyway,
   and half-off it at the wrong breakpoint. */
@media (max-width: 560px) {
  .lead-form-dock {
    right: 0.75rem; left: 0.75rem; bottom: 0.75rem;
    max-width: none; align-items: stretch;
  }
  .lead-dock-panel { width: auto; max-height: 70vh; }
  .lead-dock-launcher { width: 100%; }
}

/* ── A2P campaign lock ─────────────────────────────────────────────────
   Amber, not red: a campaign under review is not an error, and not
   green either -- an approved one is locked too, and the note explains
   a restriction rather than congratulating anybody. */
.a2p-locked-note {
  margin-top: 0.5rem;
  padding: 0.55rem 0.75rem;
  border-radius: 0.5rem;
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--c-warning-text, #92400e);
  background: var(--c-warning-tint, #fffbeb);
  border: 1px solid var(--c-warning-border, #fde68a);
}
/* A disabled control still has to be readable -- this is the wording the
   carrier holds, and it is the thing somebody most wants to re-read. */
.settings-section input:disabled,
.settings-section select:disabled,
.settings-section textarea:disabled {
  background: var(--c-surface-subtle, #f8fafc);
  color: var(--c-text, #0f172a);
  opacity: 1;
  cursor: default;
}

/* The number restated inside the step-5 billing estimate. Muted and
   inline: the row's subject is the charge, and the number qualifies it
   rather than competing with the price. */
.mnw-estimate-num {
  font-weight: 600;
  color: var(--c-text-muted, #64748b);
  margin-left: 0.35rem;
  font-variant-numeric: tabular-nums;
}

/* Linxi Numbers on the account page. */
.acct-numbers-summary {
  margin: 0.25rem 0 0.9rem;
  font-size: 0.85rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
/* A released number stays listed -- "where did my number go" is a real
   question -- but reads as history, not as a charge. */
.data-table tr.is-released td {
  color: var(--c-text-muted, #64748b);
  text-decoration: line-through;
  text-decoration-color: color-mix(in srgb, var(--c-text-muted, #64748b) 45%, transparent);
}

/* Resubmit, on a rejected A2P filing. */
.a2p-filing-actions { margin-top: 0.65rem; }
.a2p-filing-resubmit-note {
  margin: 0.45rem 0 0;
  font-size: 0.75rem;
  line-height: 1.45;
  color: var(--c-text-muted, #64748b);
}
.a2p-filing-resubmit-note.is-error { color: var(--c-danger-deep, #991b1b); }

/* Earlier A2P drafts. A <details> so the list stays a list -- five
   rejected campaign descriptions expanded is a wall, and the thing
   somebody scans for is the version and the verdict. */
#a2p-versions-list { margin-top: 1.25rem; }
.a2p-version {
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.6rem;
  padding: 0.6rem 0.8rem;
  margin-bottom: 0.4rem;
  background: var(--c-surface, #fff);
}
.a2p-version > summary {
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-size: 0.82rem;
}
.a2p-version-label { font-weight: 700; }
.a2p-version-status { font-size: 0.75rem; }
.a2p-version-date { margin-left: auto; font-size: 0.72rem; color: var(--c-text-muted, #64748b); }
.a2p-version-reason {
  margin: 0.5rem 0 0;
  font-size: 0.75rem;
  color: var(--c-danger-deep, #991b1b);
}
.a2p-version-field {
  margin: 0.5rem 0 0;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  color: var(--c-text-muted, #64748b);
}
.a2p-version-desc {
  margin: 0.2rem 0 0;
  font-size: 0.78rem;
  line-height: 1.5;
  white-space: pre-wrap;
}

/* The way out of an A2P rejection, on the Linxi Number panel. A
   rejection notice with no next step is a dead end -- the editor that
   fixes it is a different rail entry nobody has reason to open. */
.mnc-a2p-fix {
  margin-top: 0.6rem;
  padding: 0.7rem 0.85rem;
  border-radius: 0.55rem;
  background: var(--c-warning-tint, #fffbeb);
  border: 1px solid var(--c-warning-border, #fde68a);
}
.mnc-a2p-fix-copy {
  margin: 0 0 0.55rem;
  font-size: 0.78rem;
  line-height: 1.5;
  color: var(--c-warning-text, #92400e);
}

/* A field the filing cannot proceed without. Amber rather than red: it
   is not wrong yet, it is required. */
.a2p-needs-mobile {
  padding: 0.7rem 0.8rem;
  border-radius: 0.55rem;
  background: var(--c-warning-tint, #fffbeb);
  border: 1px solid var(--c-warning-border, #fde68a);
}

/* ── Sole-proprietor brand verification ────────────────────────────────
   The carrier texts a one-time code and will not identify the brand
   until it comes back, so this is a STEP, not an error: blue, not
   amber, and never red. */
.mnw-otp, .a2p-otp {
  margin-top: 0.9rem;
  padding: 0.9rem 1rem;
  border-radius: 0.6rem;
  background: var(--c-info-tint, #eff6ff);
  border: 1px solid var(--c-info-border, #bfdbfe);
}
.mnw-otp-title, .a2p-otp-title {
  margin: 0 0 0.35rem;
  font-size: 0.87rem;
  font-weight: 700;
}
.mnw-otp-copy, .a2p-otp-copy {
  margin: 0 0 0.6rem;
  font-size: 0.78rem;
  line-height: 1.5;
  color: var(--c-text-muted, #64748b);
}
.mnw-otp-wait, .a2p-otp-sent {
  margin: 0 0 0.5rem;
  font-size: 0.76rem;
  color: var(--c-text-muted, #64748b);
}
.mnw-otp-row, .a2p-otp-row {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  flex-wrap: wrap;
}
#mnw-otp-code, .a2p-otp-input {
  flex: 1 1 8rem;
  min-width: 0;
  padding: 0.6rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
  /* A one-time code is read off a phone and typed digit by digit. */
  font-size: 1.05rem;
  letter-spacing: 0.18em;
  font-variant-numeric: tabular-nums;
  background: var(--c-surface, #fff);
  color: var(--c-text, #0f172a);
}
.mnw-otp-result, .a2p-otp-result {
  margin: 0.55rem 0 0;
  font-size: 0.78rem;
  color: var(--c-text-muted, #64748b);
}
.mnw-otp-result.is-error, .a2p-otp-result.is-error { color: var(--c-danger-deep, #991b1b); }

/* The A2P brand mobile, beside the account's main phone number.
   ⚠️ NOT a verification -- TCR requires the number on a sole-proprietor
   brand and never checks it. This said "Verified by Telnyx" until the
   live brand was read back as identityStatus: UNVERIFIED. */
.phone-verified {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-top: 0.35rem;
}
.phone-verified-badge {
  font-size: 0.75rem;
  font-weight: 700;
  padding: 0.15rem 0.5rem;
  border-radius: 2rem;
  color: var(--c-success-text, #065f46);
  background: var(--c-success-tint, #f0fdf4);
  border: 1px solid var(--c-success-border, #a7f3d0);
}
.phone-verified-when, .phone-verified-other {
  font-size: 0.73rem;
  color: var(--c-text-muted, #64748b);
}
.phone-verified-use {
  font-size: 0.73rem;
  padding: 0.15rem 0.5rem;
  border-radius: 0.35rem;
  cursor: pointer;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface, #fff);
  color: var(--c-text, #0f172a);
}

/* A managed number covered by the plan's free first line. The fee is
   struck through rather than removed: if it simply read "Free", the
   SECOND number costing $1.50 would look like a price rise instead of
   the second unit costing what the first always did. */
.mnc-price-was {
  color: var(--c-text-muted, #64748b);
  margin-right: 0.4rem;
  text-decoration-color: color-mix(in srgb, var(--c-text-muted, #64748b) 60%, transparent);
}
.mnc-price-free {
  font-weight: 700;
  color: var(--c-success-text, #065f46);
}

/* The verification mobile, on the A2P filing card. Amber only when it is
   MISSING -- a filing that has one is not a problem to look at. */
.a2p-mobile {
  margin-top: 0.7rem;
  padding: 0.75rem 0.85rem;
  border-radius: 0.55rem;
  background: var(--c-surface-subtle, #f8fafc);
  border: 1px solid var(--c-border, #e2e8f0);
}
.a2p-mobile.is-missing {
  background: var(--c-warning-tint, #fffbeb);
  border-color: var(--c-warning-border, #fde68a);
}
.a2p-mobile-title { margin: 0 0 0.3rem; font-size: 0.82rem; font-weight: 700; }
.a2p-mobile-copy {
  margin: 0 0 0.55rem;
  font-size: 0.75rem;
  line-height: 1.5;
  color: var(--c-text-muted, #64748b);
}
.a2p-mobile-row { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
.a2p-mobile-input {
  flex: 1 1 10rem;
  min-width: 0;
  padding: 0.55rem 0.65rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.5rem;
  font-size: 0.85rem;
  background: var(--c-surface, #fff);
  color: var(--c-text, #0f172a);
}
.a2p-mobile-result { margin: 0.45rem 0 0; font-size: 0.75rem; color: var(--c-text-muted, #64748b); }
/* The filed mobile, shown as a value rather than a disabled input --
   once the brand is at the carrier there is nothing here to change.
   Full contrast, not muted: it is the number that identifies the
   account holder on the brand record, and reading it back is the whole
   reason it is still on screen. */
.a2p-mobile-value {
  margin: 0;
  font-size: 0.9rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--c-text, #0f172a);
}
.a2p-mobile-result.is-error { color: var(--c-danger-deep, #991b1b); }

/* The remaining step when a brand exists but its campaign does not.
   Green: the brand went through, this is progress, not a problem. */
.a2p-step {
  margin-top: 0.7rem;
  padding: 0.8rem 0.9rem;
  border-radius: 0.55rem;
  background: var(--c-success-tint, #f0fdf4);
  border: 1px solid var(--c-success-border, #a7f3d0);
}
.a2p-step-title { margin: 0 0 0.35rem; font-size: 0.85rem; font-weight: 700; }
.a2p-step-copy {
  margin: 0 0 0.65rem;
  font-size: 0.77rem;
  line-height: 1.5;
  color: var(--c-text-muted, #64748b);
}
.a2p-step-row { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.a2p-step-result { margin: 0.5rem 0 0; font-size: 0.77rem; color: var(--c-text-muted, #64748b); }
.a2p-step-result.is-error { color: var(--c-danger-deep, #991b1b); }

/* ── Emergency Address (E911) ────────────────────────────────────────
   Reuses the settings-page language already established here: .card for
   the panel, .panel-head for the title row, the .carrier-badge chip
   shape, and .result/.error/.success for the outcome line. No new
   visual pattern is invented for one panel.

   ⚠️ Every colour is a token. Three names that read like tokens --
   --border, --text-muted, --bg-subtle -- are NOT declared anywhere in
   this stylesheet, and a bare var() with an undeclared name is invalid
   at computed-value time and silently does nothing. */

.e911-head h2 { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }

/* ⚠️ Its own class, NOT .carrier-badge. That name is declared TWICE in
   this file for two unrelated features (the header badge ~9058 and the
   number-lookup row ~10304), and the later rule wins every property
   both set -- so borrowing it would make this chip's appearance depend
   on which of those two someone edits next. */
.carrier-chip {
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  padding: 0.15rem 0.55rem;
  border-radius: 2rem;
  background: var(--c-brand-tint, #f5f3ff);
  color: var(--c-brand-strong, #4f46e5);
  border: 1px solid transparent;
}
.carrier-chip-telnyx {
  background: var(--c-success-tint, #f0fdf4);
  color: var(--c-success-text, #166534);
}

/* The gated state. A real empty state rather than a sentence, because
   it is what most workspaces see and its whole job is to send somebody
   somewhere else. */
.e911-empty {
  text-align: center;
  padding: 2rem 1.25rem;
  border: 1px dashed var(--c-border, #e2e8f0);
  border-radius: 0.75rem;
  background: var(--c-surface-subtle, #f8fafc);
}
.e911-empty-icon { font-size: 1.75rem; line-height: 1; margin-bottom: 0.6rem; }
.e911-empty-title {
  margin: 0 0 0.35rem;
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--c-text, #0f172a);
}
.e911-empty-body {
  margin: 0 auto 1rem;
  max-width: 34rem;
  font-size: 0.82rem;
  line-height: 1.55;
  color: var(--c-text-muted, #64748b);
}
.e911-empty-cta { margin: 0; }

/* The form. Grouped fields with their own hint line, rather than a
   stack of bare labels and inputs. */
.e911-form { display: flex; flex-direction: column; gap: 1.1rem; margin-top: 1rem; }
.e911-field { display: flex; flex-direction: column; gap: 0.3rem; min-width: 0; }
.e911-field label { margin: 0; font-size: 0.8rem; font-weight: 600; color: var(--c-text, #0f172a); }
.e911-field input { margin: 0; }
.e911-hint { margin: 0; font-size: 0.72rem; line-height: 1.45; color: var(--c-text-muted, #64748b); }

/* City / State / ZIP on one row, sized by what each holds -- a
   two-character state field the width of a city field reads as a field
   somebody forgot to finish. min-width: 0 or the inputs refuse to
   shrink below their content and push the row wider than the column. */
.e911-row { display: flex; gap: 0.75rem; flex-wrap: wrap; }
.e911-field-city { flex: 3 1 10rem; }
.e911-field-state { flex: 0 1 5.5rem; }
.e911-field-zip { flex: 1 1 7rem; }
@media (max-width: 560px) {
  .e911-row { display: block; }
  .e911-row .e911-field { margin-bottom: 1.1rem; }
}

.e911-actions { display: flex; flex-direction: column; gap: 0.5rem; align-items: flex-start; }

/* The registered state. Success-tinted, but the ADDRESS is full-contrast
   text -- it is the thing being checked, not decoration. */
.e911-registered {
  border: 1px solid var(--c-success-border, #bbf7d0);
  background: var(--c-success-tint, #f0fdf4);
  border-radius: 0.75rem;
  padding: 1rem 1.1rem;
}
.e911-registered-head { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.e911-registered-tick {
  font-weight: 800;
  color: var(--c-success, #16a34a);
}
.e911-registered-title { font-size: 0.9rem; font-weight: 700; color: var(--c-success-text, #166534); }
.e911-registered-status {
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: 0.1rem 0.45rem;
  border-radius: 2rem;
  background: var(--c-surface, #fff);
  color: var(--c-success-text, #166534);
}
.e911-registered-address {
  margin: 0.55rem 0 0;
  font-size: 0.9rem;
  font-weight: 600;
  line-height: 1.5;
  color: var(--c-text, #0f172a);
}
.e911-registered-meta {
  margin: 0.25rem 0 0.9rem;
  font-size: 0.75rem;
  color: var(--c-text-muted, #64748b);
}

/* ── Test buttons on the Linxi Number card ───────────────────────────
   Sits between the detail rows and the A2P status line, so it reuses
   the card's own spacing rather than introducing a section. */
.mnc-tests {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin: 0.85rem 0 0.6rem;
}
.mnc-tests button { margin: 0; }

.mnc-verify-result {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  margin: 0 0 0.85rem;
  padding: 0.7rem 0.8rem;
  border-radius: 0.6rem;
  font-size: 0.8rem;
  line-height: 1.45;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc);
  color: var(--c-text, #0f172a);
}
/* The tone is on the CONTAINER so the border follows the worst line in
   it -- a green box containing a red row reads as a pass. */
.mnc-verify-ok { border-color: var(--c-success-border, #bbf7d0); background: var(--c-success-tint, #f0fdf4); }
.mnc-verify-warn { border-color: var(--c-warning-border, #fde68a); background: var(--c-warning-tint, #fffbeb); }
.mnc-verify-fail { border-color: var(--c-danger-border, #fecaca); background: var(--c-danger-tint, #fef2f2); }

.mnc-verify-line { display: flex; gap: 0.45rem; align-items: flex-start; }
/* The glyph is aria-hidden, so the colour is not the only signal --
   the sentence beside it says what happened. */
.mnc-verify-mark { flex: 0 0 auto; }

/* ── The AI request queue: "not run" and the closed-work note ────────
   ⚠️ `not_run` is the NULL case and it must not borrow the "working on
   it" blue that analyzing/executing use -- that is what made an empty
   column read as "the AI has this in hand". Neutral: nobody has run it. */
.request-ai-chip.request-ai-state-not_run {
  border-color: var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc);
  color: var(--c-text-muted, #64748b);
}
.request-ai-state-not_run { border-left-color: var(--c-border, #e2e8f0); }

.request-ai-queue-done {
  margin: 0 0 0.6rem;
  font-size: 0.75rem;
  line-height: 1.45;
  color: var(--c-text-muted, #64748b);
}

/* ── "Add another number?" ───────────────────────────────────────────
   Reuses .modal-overlay / .modal and the fee-modal's proportions rather
   than inventing a third dialogue shape. */
.addnum-modal { max-width: 30rem; }
.addnum-head { display: flex; gap: 0.85rem; align-items: flex-start; }
.addnum-glyph { font-size: 1.6rem; line-height: 1; }
.addnum-head h3 { margin: 0 0 0.15rem; font-size: 1.05rem; }
.addnum-head p { margin: 0; font-size: 0.8rem; color: var(--c-text-muted, #64748b); }
.addnum-body { margin: 1.1rem 0; display: flex; flex-direction: column; gap: 0.85rem; }
.addnum-current {
  display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem;
  padding: 0.6rem 0.75rem; border-radius: 0.6rem;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc);
}
.addnum-current-label { font-size: 0.75rem; color: var(--c-text-muted, #64748b); }
.addnum-current-value {
  font-size: 0.95rem; font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--c-text, #0f172a);
}
/* Amber, not red: buying a second number is a legitimate thing to do.
   This is the fact people miss, not a danger. */
.addnum-warn {
  margin: 0; padding: 0.7rem 0.8rem; border-radius: 0.6rem;
  font-size: 0.82rem; line-height: 1.5;
  border: 1px solid var(--c-warning-border, #fde68a);
  background: var(--c-warning-tint, #fffbeb);
  color: var(--c-text, #0f172a);
}
.addnum-alt { margin: 0; font-size: 0.78rem; line-height: 1.5; color: var(--c-text-muted, #64748b); }
.addnum-actions { display: flex; gap: 0.6rem; justify-content: flex-end; flex-wrap: wrap; }

/* ── "Publish this page?" ────────────────────────────
   Reuses .modal-overlay / .modal and the addnum dialogue's proportions
   rather than inventing a fourth shape -- the same call .addnum-modal
   made about the fee modal above it. */
.compub-modal { max-width: 30rem; }
.compub-head { display: flex; gap: 0.85rem; align-items: flex-start; }
.compub-glyph { font-size: 1.6rem; line-height: 1; }
.compub-head h3 { margin: 0 0 0.2rem; font-size: 1.05rem; }
/* The page's own title. Not muted: it is the thing being published, and
   it is the one value here the author wrote. */
.compub-head p {
  margin: 0; font-size: 0.85rem; font-weight: 600;
  color: var(--c-text, #0f172a);
  overflow-wrap: anywhere;
}
.compub-body { margin: 1.1rem 0; display: flex; flex-direction: column; gap: 0.9rem; }

/* The address is the thing being agreed to, so it is the loudest element
   in the dialogue. ⚠️ `overflow-wrap: anywhere` -- a nested slug with no
   spaces in it would otherwise push the modal wider than the screen. */
.compub-where {
  display: flex; flex-direction: column; gap: 0.3rem;
  padding: 0.7rem 0.8rem; border-radius: 0.6rem;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc);
}
.compub-where-label {
  font-size: 0.68rem; font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--c-text-dim, #94a3b8);
}
.compub-url {
  font-family: monospace; font-size: 0.95rem; font-weight: 700;
  color: var(--c-text, #0f172a);
  overflow-wrap: anywhere;
}

.compub-facts { margin: 0; padding: 0; list-style: none;
  display: flex; flex-direction: column; gap: 0.45rem; }
/* ⚠️ THE GLYPH IS A COLUMN, not a bullet: a fact that wraps to two
   lines has to keep its second line clear of the mark, or the list reads
   as prose with a tick loose in it. */
.compub-fact {
  display: grid; grid-template-columns: 1rem 1fr; gap: 0.5rem;
  font-size: 0.82rem; line-height: 1.45;
  color: var(--c-text, #0f172a);
}
.compub-mark { font-weight: 700; text-align: center; }
/* ⚠️ COLOUR IS NOT THE MESSAGE -- every sentence says on its own
   whether the thing happens, and the mark is aria-hidden. This only
   makes the list scannable. */
.compub-fact-yes .compub-mark { color: var(--c-brand, #6366f1); }
.compub-fact-no { color: var(--c-text-muted, #64748b); }
.compub-fact-no .compub-mark { color: var(--c-text-dim, #94a3b8); }

.compub-note { margin: 0; font-size: 0.78rem; line-height: 1.5; color: var(--c-text-muted, #64748b); }
.compub-actions { display: flex; gap: 0.6rem; justify-content: flex-end; flex-wrap: wrap; }

/* ── App map (/appmap) ───────────────────────────────────────────────
   Rides on the .legal-page shell, like /about and /privacy, so the header,
   hero, footer and both themes come for free. Every colour is a palette
   token, so a rebrand and a dark/light switch carry it without edits --
   the /info guides each carry ~6KB of inline CSS instead, duplicated
   across twenty files, which is the pattern this deliberately does not
   follow. */
/* ⚠️ .legal-content IS A TWO-COLUMN GRID -- 220px for a .legal-toc sidebar,
   then the document. This page has no TOC, so everything landed in the
   220px column and the wide one sat empty: the whole page crushed against
   the left with the middle blank. Anything else reusing this shell without
   a TOC needs the same override. */
.legal-content.appmap { display: block; }

.appmap-tools {
  margin: 0 0 2.5rem;
  padding: 1.1rem 1.2rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 12px;
  background: var(--c-surface-subtle, #f8fafc);
}
.appmap-search { position: relative; display: block; }
.appmap-search-icon {
  position: absolute;
  top: 50%;
  left: 0.85rem;
  transform: translateY(-50%);
  font-size: 0.9rem;
  opacity: 0.55;
  pointer-events: none;
}
.appmap-search input {
  width: 100%;
  padding: 0.7rem 0.9rem 0.7rem 2.4rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 8px;
  background: var(--c-surface, #ffffff);
  color: var(--c-text, #0f172a);
  font: inherit;
  font-size: 0.92rem;
}
.appmap-search input:focus-visible {
  outline: none;
  border-color: var(--c-brand, #6366f1);
  box-shadow: var(--btn-ring);
}
.appmap-result {
  margin: 0.6rem 0 0;
  min-height: 1.2em;
  font-size: 0.82rem;
  color: var(--c-text-muted, #64748b);
}
.appmap-chips { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.9rem; }
.appmap-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.3rem 0.7rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 2rem;
  background: var(--c-surface, #ffffff);
  color: var(--c-text-muted, #64748b);
  font-size: 0.78rem;
  font-weight: 600;
  text-decoration: none;
  transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
}
.appmap-chip:hover {
  border-color: var(--c-brand, #6366f1);
  color: var(--c-brand, #6366f1);
  background: var(--c-brand-tint, #f5f3ff);
}
/* The count sits in a <b> so it is bold without being a heading. */
.appmap-chip b {
  font-size: 0.7rem;
  font-weight: 700;
  color: var(--c-text-dim, #94a3b8);
}
.appmap-chip:hover b { color: var(--c-brand, #6366f1); }

.appmap-section { margin: 0 0 2.75rem; scroll-margin-top: 1.5rem; }
.appmap-section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
}
.appmap-section-head h2 { margin: 0; display: flex; align-items: center; gap: 0.5rem; }
.appmap-section-icon { font-style: normal; }
.appmap-count {
  flex-shrink: 0;
  font-size: 0.76rem;
  font-weight: 600;
  color: var(--c-text-dim, #94a3b8);
}
/* A section the reader cannot open. It keeps its heading, icon and page
   count and loses only the list: a map with a hole in it reads as a map
   that is wrong, and somebody who cannot open a section still needs to
   know it exists and what would open it. */
.appmap-locked { opacity: 0.92; }

.appmap-lock {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  padding: 0.9rem 1.1rem;
  border: 1px dashed var(--c-border, #e2e8f0);
  border-radius: 0.65rem;
  background: var(--c-surface-subtle, #f8fafc);
}

.appmap-lock-icon { font-size: 1.2rem; line-height: 1.2; }
.appmap-lock-why { margin: 0; font-size: 0.88rem; font-weight: 600; }
.appmap-lock-how { margin: 0.3rem 0 0; font-size: 0.82rem; color: var(--c-text-muted, #64748b); }
.appmap-lock-how a { color: var(--c-brand-deep, #4338ca); font-weight: 600; }

@media (max-width: 640px) {
  .appmap-lock { flex-direction: column; gap: 0.45rem; }
}

.appmap-blurb { margin: 0.65rem 0 1rem; color: var(--c-text-muted, #64748b); }

/* auto-fill with a min() so a single card does not stretch the full width
   on desktop while still going edge to edge on a phone. */
.appmap-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr));
  gap: 0.7rem;
}
.appmap-card {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding: 0.85rem 1rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 10px;
  background: var(--c-surface, #ffffff);
  text-decoration: none;
  transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.12s ease;
}
.appmap-card:hover {
  border-color: var(--c-brand, #6366f1);
  box-shadow: var(--btn-shadow-hover);
  transform: translateY(-1px);
}
.appmap-card:focus-visible {
  outline: none;
  border-color: var(--c-brand, #6366f1);
  box-shadow: var(--btn-ring);
}
.appmap-card code {
  font-family: monospace;
  font-size: 0.84rem;
  font-weight: 600;
  color: var(--c-brand, #6366f1);
  overflow-wrap: anywhere;
}
.appmap-card span {
  font-size: 0.86rem;
  line-height: 1.45;
  color: var(--c-text-muted, #64748b);
}

/* ⚠️ KEPT, THOUGH /nav NO LONGER DRAWS THE LIST. The "Old addresses"
   section is gone; these rules are the only styling for a redirect row
   and deleting them is a decision about markup that may come back, not
   about the page that stopped using it. If nothing emits
   .appmap-redirects a year from now, that is when they go. */
.appmap-redirects { list-style: none; margin: 0; padding: 0; }
.appmap-redirects li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.55rem 0;
  border-top: 1px solid var(--c-border, #e2e8f0);
  font-size: 0.86rem;
}
.appmap-redirects li:first-child { border-top: none; }
.appmap-redirects code { font-family: monospace; font-size: 0.84rem; overflow-wrap: anywhere; }
.appmap-redirects a { text-decoration: none; }
.appmap-redirects a code { color: var(--c-brand, #6366f1); font-weight: 600; }
.appmap-redirects a:hover code { text-decoration: underline; }
.appmap-arrow { color: var(--c-text-dim, #94a3b8); }
.appmap-redirect-note { color: var(--c-text-muted, #64748b); }

/* Two links per box, so a card is a container rather than one link. The
   inner links carry the hit target; the box itself must not, or a click
   anywhere in it would have to pick one of the two destinations. */
.appmap-card h3 {
  margin: 0;
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--c-text, #0f172a);
}
.appmap-card p {
  margin: 0;
  font-size: 0.86rem;
  line-height: 1.45;
  color: var(--c-text-muted, #64748b);
}
.appmap-card-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-top: 0.15rem;
}
.appmap-link {
  display: inline-flex;
  align-items: center;
  padding: 0.2rem 0.55rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 6px;
  text-decoration: none;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.appmap-link code {
  font-family: monospace;
  font-size: 0.76rem;
  font-weight: 600;
  overflow-wrap: anywhere;
}
.appmap-link-app { border-color: var(--c-brand-border, #c7d2fe); background: var(--c-brand-tint, #f5f3ff); }
.appmap-link-app code { color: var(--c-brand, #6366f1); }
.appmap-link-doc code { color: var(--c-text-muted, #64748b); }
.appmap-link:hover { border-color: var(--c-brand, #6366f1); }
.appmap-link:hover code { text-decoration: underline; }
.appmap-link:focus-visible { outline: none; border-color: var(--c-brand, #6366f1); box-shadow: var(--btn-ring); }

/* A card that IS a link again -- the sibling list on an explainer page. */
.appmap-card-link { text-decoration: none; }
.appmap-card-link:hover { border-color: var(--c-brand, #6366f1); box-shadow: var(--btn-shadow-hover); transform: translateY(-1px); }
.appmap-card-link:focus-visible { outline: none; border-color: var(--c-brand, #6366f1); box-shadow: var(--btn-ring); }
.appmap-grid-tight { gap: 0.5rem; }

.appmap-detail .appmap-section:first-of-type { margin-top: 0; }
.appmap-lead { margin: 0.65rem 0 0.9rem; font-size: 1.02rem; color: var(--c-text, #0f172a); }
.appmap-blurb code { font-family: monospace; font-size: 0.86rem; color: var(--c-brand, #6366f1); }

.appmap-actions { display: flex; flex-wrap: wrap; gap: 0.6rem; margin: 0 0 2rem; }
.appmap-cta {
  display: inline-flex;
  align-items: center;
  padding: 0.6rem 1.1rem;
  border-radius: 8px;
  border: 1px solid transparent;
  background: var(--btn-grad);
  color: var(--c-on-accent, #ffffff);
  font-size: 0.9rem;
  font-weight: 600;
  text-decoration: none;
  box-shadow: var(--btn-shadow);
  transition: box-shadow 0.15s ease, transform 0.12s ease;
}
.appmap-cta:hover { box-shadow: var(--btn-shadow-hover); transform: translateY(-1px); }
.appmap-cta:focus-visible { outline: none; box-shadow: var(--btn-shadow), var(--btn-ring); }
.appmap-cta-quiet {
  background: var(--c-surface, #ffffff);
  color: var(--c-text-muted, #64748b);
  border-color: var(--c-border, #e2e8f0);
  box-shadow: none;
}
.appmap-cta-quiet:hover { border-color: var(--c-brand, #6366f1); color: var(--c-brand, #6366f1); box-shadow: none; }

/* Stands in for the screenshot that is not written yet, and says so rather
   than leaving a blank where a picture is obviously missing. */
.appmap-placeholder {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  padding: 2.5rem 1.5rem;
  border: 1px dashed var(--c-border, #e2e8f0);
  border-radius: 12px;
  background: var(--c-surface-subtle, #f8fafc);
  text-align: center;
}
.appmap-placeholder span { font-size: 2rem; line-height: 1; }
.appmap-placeholder p { margin: 0; font-weight: 700; color: var(--c-text, #0f172a); }
.appmap-placeholder code { font-family: monospace; font-size: 0.82rem; color: var(--c-text-muted, #64748b); }

.legal-eyebrow a { color: inherit; }

@media (max-width: 640px) {
  .appmap-section-head { flex-direction: column; align-items: flex-start; gap: 0.2rem; }
}

/* ── Coms: the public feed and reader (/coms, /coms/:slug) ───────────
   On the .legal-page shell like /about and /appmap, so header, footer and
   both themes come for free. Every colour is a palette token. */

/* .legal-content is a 220px+1fr grid for a TOC sidebar. The FEED has no
   rail at all, so it becomes a block; the ARTICLE has two, so it declares
   its own three-column track instead of inheriting the two-column one. */
.legal-content.coms { display: block; }

.coms-lead {
  display: block;
  padding: 2rem 2.25rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 14px;
  background: var(--c-surface, #ffffff);
  text-decoration: none;
  margin-bottom: 1.25rem;
  transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.12s ease;
}
.coms-lead:hover, .coms-card:hover {
  border-color: var(--c-brand, #6366f1);
  box-shadow: var(--btn-shadow-hover);
  transform: translateY(-1px);
}
.coms-lead:focus-visible, .coms-card:focus-visible {
  outline: none; border-color: var(--c-brand, #6366f1); box-shadow: var(--btn-ring);
}
.coms-lead h2 { margin: 0.5rem 0 0.4rem; font-size: 1.75rem; line-height: 1.2; color: var(--c-text, #0f172a); }
.coms-lead p { margin: 0 0 0.75rem; font-size: 1.02rem; color: var(--c-text-muted, #64748b); }

.coms-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 21rem), 1fr));
  gap: 1.25rem;
}
.coms-card {
  display: flex; flex-direction: column; gap: 0.35rem;
  padding: 1.25rem 1.35rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 12px;
  background: var(--c-surface, #ffffff);
  text-decoration: none;
  transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.12s ease;
}
.coms-card h2 { margin: 0.15rem 0 0.2rem; font-size: 1.1rem; line-height: 1.3; color: var(--c-text, #0f172a); }
.coms-card p { margin: 0; font-size: 0.9rem; line-height: 1.5; color: var(--c-text-muted, #64748b); }
/* aspect-ratio with the width/height attributes the renderer emits: the box
   is known before the bytes arrive, so nothing below it jumps. */
.coms-card-cover {
  width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover;
  border-radius: 8px; margin-bottom: 0.5rem;
}
.coms-tag {
  align-self: flex-start;
  font-size: 0.68rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--c-brand, #6366f1);
}
.coms-meta { margin-top: 0.35rem; font-size: 0.78rem; color: var(--c-text-dim, #94a3b8); }
.coms-empty {
  padding: 4rem 2rem; text-align: center;
  border: 1px dashed var(--c-border, #e2e8f0); border-radius: 14px;
  background: var(--c-surface-subtle, #f8fafc);
}
.coms-empty h2 { margin: 0 0 0.35rem; color: var(--c-text, #0f172a); }
.coms-empty p { margin: 0; color: var(--c-text-muted, #64748b); }

/* The reader. The brief called 1fr 3fr 1fr a "60-30-10 split"; it is
   20-60-20, and 20% of 1100px is too narrow for a usable contents rail --
   so the tracks are named explicitly instead of inherited from a slogan. */
.legal-content.coms-article {
  display: grid;
  grid-template-columns: 15rem minmax(0, 1fr) 14rem;
  gap: 2.5rem;
  align-items: start;
  max-width: 1240px;
}
.coms-rail { position: sticky; top: 1.5rem; }
.coms-toc-title {
  margin: 0 0 0.6rem; font-size: 0.7rem; font-weight: 700;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--c-text-dim, #94a3b8);
}
.coms-toc-link {
  display: block; padding: 0.3rem 0 0.3rem 0.75rem;
  border-left: 2px solid var(--c-border, #e2e8f0);
  font-size: 0.84rem; line-height: 1.35;
  color: var(--c-text-muted, #64748b); text-decoration: none;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.coms-toc-h3 { padding-left: 1.5rem; font-size: 0.8rem; }
.coms-toc-link:hover { color: var(--c-text, #0f172a); }
.coms-toc-link.active { color: var(--c-brand, #6366f1); border-left-color: var(--c-brand, #6366f1); }

/* ⚠️ 66ch, not a pixel width. The brief asks for 60-75 characters, which is
   a measure of the TEXT -- a px value only lands there at one font size. */
/* ── The custom landing canvas ────────────────────────────────────────
 *
 * ⚠️⚠️ A CUSTOM LANDING PAGE USED TO RENDER INSIDE .coms-body, AND THAT IS
 * WHY THEY ALL LOOKED WRONG.
 *
 * renderSiteShell puts the body in `<main class="site-page coms-body">`,
 * and .coms-body is ARTICLE typography: max-width 66ch, 1.06rem at
 * line-height 1.75, h2 at 1.5rem with 2.5rem of margin. Correct for
 * /:slug, which really is a written page. Fatally wrong for the site
 * root, where it squeezed every hero, every feature grid and every
 * full-bleed section into a ~600px reading column and sized the headings
 * like a blog post. No prompt could have fixed that: the model can write
 * a perfect landing page and the container still renders it as prose.
 *
 * .site-page and .site-page-body carry no rules at all -- they are hooks
 * -- so this class is the only thing between the page and the viewport.
 *
 * ⚠️ IT IS KEYED ON layout === 'bare', the same flag that already decides
 * whether the site header and footer are drawn, rather than on a second
 * one. Those two questions have one answer: is this the whole page, or is
 * it a document inside the site's chrome.
 */
/* ⚠️⚠️ THE PUBLIC SHELL REUSES THE APP'S STYLESHEET, AND THAT STYLESHEET
 * STYLES BARE ELEMENTS.
 *
 *   main   { max-width: 1280px; margin: 0 auto; padding: 1.5rem;
 *            display: flex; flex-direction: column; gap: 1.5rem }
 *   header { background: <dark gradient>; color: #fff; padding: 1rem 1.5rem }
 *
 * Those are the SIGNED-IN INTERFACE -- a centred column of cards, and the
 * dark app bar above it. A public page's <main> IS the page, and its
 * <header> is the one the admin wrote. Inheriting the app's rules gave
 * the landing page a 1.5rem inset on all four sides -- read as "a margin
 * between the content and the header and footer" -- and a 1.5rem flex gap
 * between every top-level section. The 1280px cap was the one part
 * already handled: .site-page-canvas reset max-width and nothing else,
 * which is why only some of it showed.
 *
 * ⚠️ SCOPED TO .site-page-body, WHICH ONLY THE PUBLIC SHELL SETS -- the
 * app's own <body> carries no class at all. So nothing in the signed-in
 * interface can be reached by either rule.
 */
.site-page {
  padding: 0;
  display: block;
  gap: normal;
}

/* ⚠️ :where() CONTRIBUTES NOTHING TO SPECIFICITY, so this weighs (0,1,0)
 * -- enough to beat the bare `header {}` at (0,0,1), and NOT enough to
 * beat the admin's own `.sc-header {}`, which is also (0,1,0) and is
 * served later in /theme.css. Written as `.site-page-body header` it
 * would be (0,1,1) and would strip the background off the very header it
 * is meant to protect.
 *
 * ⚠️ DESCENDANT, not `>`. A semantic <header> or <footer> INSIDE a
 * section is legitimate markup, and without this it renders as the dark
 * app bar in the middle of somebody's page. The page's own CSS is a
 * <style> block after both stylesheets, so a class of its own still wins.
 */
.site-page-body :where(header, footer) {
  padding: 0;
  color: inherit;
  background: none;
  border-bottom: 0;
}

.site-page-canvas {
  max-width: none;
  font-size: 1rem;
  line-height: 1.6;
  color: var(--c-text);
}

/* ⚠️ LOOSE PROSE KEEPS A MEASURE. Sections, grids and heroes want the
 * whole viewport, but a bare paragraph dropped straight into the canvas
 * would run the full width of a desktop monitor at 1rem, which is
 * unreadable. Only DIRECT children -- anything the page nests inside its
 * own section is that section's business, not ours. */
.site-page-canvas > p,
.site-page-canvas > ul,
.site-page-canvas > ol,
.site-page-canvas > blockquote {
  max-width: 66ch;
  margin-left: auto;
  margin-right: auto;
}

.site-page-canvas img { max-width: 100%; height: auto; }

/* The shell has no page padding of its own, so a page that writes plain
 * content with no wrapper would touch both edges on a phone. Sections
 * that want full bleed override it, which is the ordinary direction. */
@media (max-width: 620px) {
  .site-page-canvas > p,
  .site-page-canvas > ul,
  .site-page-canvas > ol,
  .site-page-canvas > h1,
  .site-page-canvas > h2,
  .site-page-canvas > h3 { padding-left: 1rem; padding-right: 1rem; }
}

.coms-body {
  max-width: 66ch;
  font-size: 1.06rem;
  line-height: 1.75;
  color: var(--c-text, #0f172a);
}
.coms-body h2 { margin: 2.5rem 0 0.75rem; font-size: 1.5rem; line-height: 1.25; scroll-margin-top: 1.5rem; }
.coms-body h3 { margin: 2rem 0 0.6rem; font-size: 1.2rem; line-height: 1.3; scroll-margin-top: 1.5rem; }
.coms-body h4 { margin: 1.5rem 0 0.5rem; font-size: 1.02rem; scroll-margin-top: 1.5rem; }
.coms-body p { margin: 0 0 1.15rem; }
/* ⚠️ An embedded form carries its own typography, and this column's
   paragraph margin was reaching into it: .coms-body p is (0,1,1) and
   beats the snippet's own .lxf-help at (0,1,0), so every note under an
   input sat on 1.15rem it never asked for. Fixed HERE rather than in
   snippetCss because the snippet is COPIED TEXT -- a form already
   pasted onto a page keeps the CSS it was pasted with, so a service
   change cannot reach it and this can.
   ⚠️ margin-bottom only: the top margins are what separate a note from
   the input above it. */
.coms-body .lxf p,
.coms-body .lxf-wrap p { margin-bottom: 0; }
.coms-body ul, .coms-body ol { margin: 0 0 1.15rem; padding-left: 1.35rem; }
.coms-body li { margin: 0.3rem 0; }
.coms-body a { color: var(--c-brand, #6366f1); }
.coms-body img { max-width: 100%; height: auto; border-radius: 10px; margin: 1.25rem 0; }
.coms-body blockquote {
  margin: 1.5rem 0; padding: 0.15rem 0 0.15rem 1.15rem;
  border-left: 3px solid var(--c-brand, #6366f1);
  color: var(--c-text-muted, #64748b); font-size: 1.1rem; font-style: italic;
}
.coms-body code {
  font-family: monospace; font-size: 0.88em;
  padding: 0.1rem 0.35rem; border-radius: 4px;
  background: var(--c-surface-sunken, #f1f5f9); color: var(--c-brand-deep, #4338ca);
}
.coms-body pre {
  margin: 1.35rem 0; padding: 1rem 1.15rem; overflow-x: auto;
  border: 1px solid var(--c-border, #e2e8f0); border-radius: 10px;
  background: var(--c-surface-sunken, #f1f5f9);
}
.coms-body pre code { padding: 0; background: none; color: var(--c-text, #0f172a); }
.coms-body hr { margin: 2rem 0; border: none; border-top: 1px solid var(--c-border, #e2e8f0); }
.coms-article-cover { width: 100%; height: auto; border-radius: 12px; margin-bottom: 1.75rem; }

.coms-aside {
  padding: 1.15rem 1.25rem;
  border: 1px solid var(--c-border, #e2e8f0); border-radius: 12px;
  background: var(--c-surface, #ffffff);
}
.coms-aside-label {
  margin: 0.85rem 0 0.1rem; font-size: 0.68rem; font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--c-text-dim, #94a3b8);
}
.coms-aside-label:first-child { margin-top: 0; }
.coms-aside-value { margin: 0; font-size: 0.9rem; color: var(--c-text, #0f172a); }
.coms-aside-cta {
  display: block; margin-top: 1.1rem; padding: 0.5rem 0.75rem;
  border: 1px solid var(--c-border, #e2e8f0); border-radius: 8px;
  text-align: center; font-size: 0.84rem; font-weight: 600;
  color: var(--c-text-muted, #64748b); text-decoration: none;
}
.coms-aside-cta:hover { border-color: var(--c-brand, #6366f1); color: var(--c-brand, #6366f1); }

/* transform on one element rather than a width change: a compositor frame,
   not a layout, on every scroll tick. */
.coms-progress {
  position: fixed; top: 0; left: 0; right: 0; height: 3px;
  background: transparent; z-index: 40; pointer-events: none;
}
.coms-progress > span {
  display: block; height: 100%; width: 100%;
  background: var(--btn-grad);
  transform: scaleX(0); transform-origin: 0 50%;
}

@media (max-width: 1080px) {
  /* The rails go, the measure stays. A contents list squeezed into a
     phone-width column is worse than no contents list. */
  .legal-content.coms-article { grid-template-columns: minmax(0, 1fr); }
  .coms-rail { display: none; }
  .coms-body { max-width: 66ch; margin: 0 auto; }
}

/* ── Coms portal (#tab-coms) and its builder drawer ──────────────────── */
.coms-portal-stats { display: flex; flex-wrap: wrap; gap: 0.75rem; margin: 1.25rem 0; }
.coms-stat {
  display: flex; flex-direction: column; gap: 0.1rem;
  padding: 0.7rem 1.1rem; min-width: 6rem;
  border: 1px solid var(--c-border, #e2e8f0); border-radius: 10px;
  background: var(--c-surface, #ffffff);
}
.coms-stat-value { font-size: 1.35rem; font-weight: 700; color: var(--c-text, #0f172a); }
.coms-stat-label { font-size: 0.72rem; font-weight: 600; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--c-text-dim, #94a3b8); }

.coms-portal-list { display: flex; flex-direction: column; gap: 0.6rem; }
.coms-row {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 1.25rem;
  padding: 1rem 1.15rem;
  border: 1px solid var(--c-border, #e2e8f0); border-radius: 10px;
  background: var(--c-surface, #ffffff);
}
.coms-row-main { min-width: 0; }
/* A real <button>, so keyboard focus and Enter work with no extra wiring --
   the reasoning .ws-rail-btn already records. Chromeless so it reads as the
   heading it replaced. */
.coms-row-title {
  display: block; padding: 0; margin: 0 0 0.2rem;
  background: none; border: none; box-shadow: none;
  font-family: inherit; font-size: 1rem; font-weight: 700; text-align: left;
  color: var(--c-text, #0f172a); cursor: pointer;
}
.coms-row-title:hover { color: var(--c-brand, #6366f1); box-shadow: none; transform: none; background: none; }
.coms-row-summary { margin: 0 0 0.4rem; font-size: 0.88rem; color: var(--c-text-muted, #64748b); }
.coms-row-meta { display: flex; flex-wrap: wrap; gap: 0.75rem; font-size: 0.76rem; color: var(--c-text-dim, #94a3b8); }
.coms-row-meta code { font-family: monospace; color: var(--c-brand, #6366f1); }
.coms-row-side { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
.coms-row-btn { padding: 0.35rem 0.7rem; font-size: 0.8rem; }
.coms-row-link { font-size: 0.8rem; font-weight: 600; color: var(--c-text-muted, #64748b); text-decoration: none; }
.coms-row-link:hover { color: var(--c-brand, #6366f1); }
.coms-status {
  padding: 0.2rem 0.6rem; border-radius: 2rem;
  font-size: 0.7rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase;
}
.coms-status-published { background: var(--c-success-tint, #dcfce7); color: var(--c-success-deep, #166534); }
.coms-status-draft { background: var(--c-surface-sunken, #f1f5f9); color: var(--c-text-muted, #64748b); }
.coms-status-archived { background: var(--c-warning-tint, #fef3c7); color: var(--c-warning-deep, #92400e); }
.coms-portal-empty {
  padding: 3rem 2rem; text-align: center;
  border: 1px dashed var(--c-border, #e2e8f0); border-radius: 12px;
  background: var(--c-surface-subtle, #f8fafc);
}

/* The drawer. Fixed, full height, sliding from the right -- it is opened
   over the list it was launched from, so it must cover it rather than push
   it. transform, not `right`, so the slide is a compositor animation. */
/* ⚠️ FULL WIDTH, not a 1200px panel. It is a three-pane workspace --
   sidebar, editor, preview -- and at 1200px on a wide screen the editor and
   the preview were each about 500px, which is narrower than the article
   they are previewing. `inset: 0` rather than `inset: 0 0 0 auto`: the
   latter is what pinned it to the right. */
.coms-builder {
  position: fixed; inset: 0;
  width: 100vw;
  display: flex; flex-direction: column;
  background: var(--c-surface-sunken, #f1f5f9);
  border-left: 1px solid var(--c-border, #e2e8f0);
  box-shadow: -8px 0 32px color-mix(in srgb, var(--c-shadow, #0f172a) 18%, transparent);
  z-index: 60;
  transform: translateX(0);
  transition: transform 0.22s ease;
}
.coms-builder.hidden { display: flex; transform: translateX(101%); pointer-events: none; }
@media (prefers-reduced-motion: reduce) { .coms-builder { transition: none; } }
.coms-builder-open { overflow: hidden; }

.coms-builder-head {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: 0.85rem 1.15rem;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface, #ffffff);
}
.coms-builder-head h3 { margin: 0; font-size: 1rem; }
.coms-builder-actions { display: flex; align-items: center; gap: 0.5rem; }
.coms-builder-status { font-size: 0.8rem; color: var(--c-text-muted, #64748b); }

/* Sidebar, editor, preview. Under 1100px the preview goes: a live preview
   squeezed to a third of a laptop screen tells you less than the words do.
   ⚠️ THIS BLOCK IS SHARED WITH THE EVENTS BUILDER, which still has three
   panes. The Coms builder has two -- its editor IS its preview -- and says
   so with `.coms-builder-body-2` rather than by changing the default out
   from under a drawer that was not part of that change. */
.coms-builder-body {
  flex: 1; min-height: 0;
  /* REQ-0048: 22rem, not 17. Fourteen controls live in this column --
     the kind radio, title, address, summary, category, tags, schema,
     chrome, cover, more images and two meta fields -- and at 272px the
     slug row, the tag chips and the cover thumbnail were all fighting for
     the same line. */
  display: grid; grid-template-columns: 22rem minmax(0, 1fr) minmax(0, 1fr);
}
/* The Coms builder: the Visual tab renders the real document in the real
   container, so a third column showing the same render beside it would be
   the same page twice, each half the width it needs. */
.coms-builder-body-2 { grid-template-columns: 22rem minmax(0, 1fr); }
.coms-builder-side {
  overflow-y: auto; padding: 1rem 1.1rem;
  border-right: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface, #ffffff);
}
.coms-builder-side label { display: block; margin: 0.9rem 0 0.25rem; font-size: 0.76rem;
  font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--c-text-dim, #94a3b8); }
.coms-builder-side label:first-child { margin-top: 0; }
.coms-builder-side input[type="text"],
.coms-builder-side textarea,
.coms-builder-side select { width: 100%; font-family: inherit; font-size: 0.88rem; }
.coms-slug-row { display: flex; align-items: center; gap: 0.25rem; }
.coms-slug-row span { font-family: monospace; font-size: 0.8rem; color: var(--c-text-dim, #94a3b8); }
.coms-cover-preview { margin-top: 0.5rem; }
.coms-cover-preview img { width: 100%; height: auto; border-radius: 8px; margin-bottom: 0.4rem; }
.coms-seo { margin-top: 1.25rem; }
.coms-seo summary { cursor: pointer; font-size: 0.8rem; font-weight: 600; color: var(--c-text-muted, #64748b); }

.coms-builder-pane { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
.coms-pane-head {
  display: flex; justify-content: space-between; align-items: center;
  padding: 0.45rem 0.9rem;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--c-text-dim, #94a3b8);
  background: var(--c-surface-subtle, #f8fafc);
}
.coms-pane-count { font-weight: 600; letter-spacing: 0; text-transform: none; }
/* ⚠️⚠️ THE FULL-PANE TEXTAREA IS REACHED STRUCTURALLY, NEVER BY ID.

   This rule used to read `#coms-markdown`, and that box was renamed to
   `coms-html` when the Coms editor became a WYSIWYG (177). The rule went
   dead in the same commit: nothing matched it, so the HTML tab fell back
   to the browser's default `rows="2"` -- a two-line box in a full-screen
   drawer, with the pane it was meant to fill empty beneath it.

   ⚠️ AND `#ev-markdown` HAS NEVER BEEN STYLED AT ALL. The Events builder
   was copied from this pane, and what it could not copy was a rule hung
   off an id it does not carry -- so its markdown box has been two lines
   since the day it shipped, and nobody reported it because the defect is
   only visible once you open the tab. One rename and one copy, the same
   hole twice, from the same cause.

   A DIRECT CHILD OF THE PANE is what "fills this pane" actually means, so
   a rename cannot silently take it away and a third builder gets it for
   nothing. `>` rather than a descendant: `#coms-css` lives inside the
   <details> at the foot and is a GRANDCHILD, which is what keeps the CSS
   box its own eight rows.

   ⚠️ `min-height: 0`, like `.coms-ve-wrap` beside it. A flex item's
   min-height defaults to `auto`, which for a textarea is its intrinsic
   row count -- so without this the box refuses to shrink below its rows
   and pushes the CSS box off the bottom of a short drawer. */
.coms-builder-pane > textarea {
  flex: 1; width: 100%; min-height: 0;
  resize: none; border: none; border-radius: 0;
  padding: 1.15rem 1.25rem;
  font-family: monospace; font-size: 0.9rem; line-height: 1.7;
  background: var(--c-surface, #ffffff); color: var(--c-text, #0f172a);
}
.coms-builder-pane > textarea:focus-visible { outline: none; box-shadow: none; }
.coms-builder-preview { border-left: 1px solid var(--c-border, #e2e8f0); }
.coms-preview-body {
  flex: 1; overflow-y: auto; padding: 1.5rem 1.75rem;
  background: var(--c-surface, #ffffff);
  max-width: none;
}

/* ⚠️ THE EDITOR FILLS THE PANE; IT IS NOT A BOXED PANEL. `.ve-toolbar`
   and `.ve-frame-wrap` are written for the site editors, where the frame
   sits inside a card at 60vh in a scrolling column. Here the pane IS the
   column, so the borders, the radius and the viewport height all have to
   go -- left alone the frame would be a 60vh box with dead space under it
   at one height and clipped at another. */
.coms-ve-toolbar {
  border: none;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0;
}
.coms-ve-wrap {
  flex: 1; min-height: 0;
  border: none; border-radius: 0;
}
.coms-ve-wrap iframe { height: 100%; min-height: 0; }

/* The CSS box sits at the FOOT of the editor and is collapsed. Open, it
   takes room from the frame above it rather than scrolling the pane --
   which is what somebody wants while they watch a rule take effect. */
.coms-css-box {
  border-top: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc);
}
.coms-css-box > summary {
  display: flex; justify-content: space-between; align-items: center; gap: 0.5rem;
  padding: 0.45rem 0.9rem; cursor: pointer;
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--c-text-dim, #94a3b8);
}
.coms-css-count { font-weight: 600; letter-spacing: 0; text-transform: none; }
#coms-css {
  display: block; width: 100%; resize: vertical;
  border: none; border-top: 1px solid var(--c-border, #e2e8f0); border-radius: 0;
  padding: 0.75rem 0.9rem;
  font-family: monospace; font-size: 0.82rem; line-height: 1.6;
  background: var(--c-surface, #ffffff); color: var(--c-text, #0f172a);
}
#coms-css:focus-visible { outline: none; box-shadow: none; }
.coms-css-box .coms-hint { padding: 0.5rem 0.9rem 0.7rem; }

@media (max-width: 1100px) {
  .coms-builder-body { grid-template-columns: 20rem minmax(0, 1fr); }
  .coms-builder-preview { display: none; }
}
@media (max-width: 780px) {
  .coms-builder-body { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto 1fr; }
  .coms-builder-side { border-right: none; border-bottom: 1px solid var(--c-border, #e2e8f0); max-height: 40vh; }
}

/* Policy links under each Linxi's consent boxes on the /@username form.
   Small and quiet -- they are a reference, not a call to action -- but
   underlined, because a visitor being asked to consent has to be able to
   see there is something to read. */
.lead-consent-links { display: flex; flex-wrap: wrap; gap: 0.9rem; margin-top: 0.5rem; }
.lead-consent-link {
  font-size: 0.72rem; font-weight: 600;
  color: var(--c-text-muted, #64748b);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.lead-consent-link:hover { color: var(--c-brand, #6366f1); }

/* ── AI in the Coms builder ──────────────────────────────────────────
   The launcher lives in the sidebar; the work happens in a panel fixed to
   the right edge, the same shape as the composer's #campaign-ai-panel. */
.coms-ai-open {
  width: 100%;
  margin-bottom: 1.1rem;
  background: var(--btn-grad);
  color: var(--c-on-accent, #ffffff);
}

.coms-ai-panel {
  position: fixed;
  top: 0; right: 0; bottom: 0;
  width: min(460px, 100vw);
  /* ⚠️ ABOVE .coms-builder (60), or it opens behind the drawer that
     launched it and reads as a button that does nothing. */
  z-index: 910;
  display: flex;
  flex-direction: column;
  background: var(--c-surface, #ffffff);
  border-left: 1px solid var(--c-border, #e2e8f0);
  box-shadow: -12px 0 32px color-mix(in srgb, var(--c-shadow, #0f172a) 16%, transparent);
  transform: translateX(0);
  transition: transform 0.22s ease;
}
/* Slides rather than disappearing, and `display` stays flex so the
   transition has something to animate. */
.coms-ai-panel.hidden { display: flex; transform: translateX(101%); pointer-events: none; }
@media (prefers-reduced-motion: reduce) { .coms-ai-panel { transition: none; } }

.coms-ai-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem;
  padding: 0.9rem 1.05rem;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
  flex-shrink: 0;
}
.coms-ai-who { display: flex; align-items: flex-start; gap: 0.6rem; }
.coms-ai-avatar {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2rem; height: 2rem; flex-shrink: 0;
  border-radius: 50%;
  background: var(--c-brand-tint, #f5f3ff);
  font-size: 1rem;
}
.coms-ai-who strong { display: block; font-size: 0.95rem; color: var(--c-text, #0f172a); }
.coms-ai-tagline { display: block; font-size: 0.76rem; line-height: 1.4; }
.coms-ai-x {
  background: none; border: none; box-shadow: none; padding: 0.2rem 0.4rem;
  color: var(--c-text-dim, #94a3b8); font-size: 0.9rem; cursor: pointer;
}
.coms-ai-x:hover { background: none; box-shadow: none; transform: none; color: var(--c-text, #0f172a); }

.coms-ai-scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 1.05rem; }
.coms-ai-action {
  padding: 1rem 1.05rem; margin-bottom: 0.85rem;
  border: 1px solid var(--c-border, #e2e8f0); border-radius: 12px;
  background: var(--c-surface-subtle, #f8fafc);
}
.coms-ai-action-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem; }
.coms-ai-action-head h4 { margin: 0; font-size: 0.92rem; color: var(--c-text, #0f172a); }
/* The price rides on the card, so it is read before the button is reached. */
.coms-ai-tag {
  flex-shrink: 0;
  padding: 0.15rem 0.5rem; border-radius: 2rem;
  background: var(--c-brand-tint, #f5f3ff); color: var(--c-brand-deep, #4338ca);
  font-size: 0.72rem; font-weight: 700;
}
.coms-ai-help { margin: 0.4rem 0 0.7rem; font-size: 0.79rem; line-height: 1.5; color: var(--c-text-muted, #64748b); }
.coms-ai-action textarea { width: 100%; font-family: inherit; font-size: 0.86rem; margin-bottom: 0.6rem; }
/* ⚠️ THREE QUARTERS, NOT FULL WIDTH. A button that runs the whole width
   of its column reads as the panel's edge rather than as a control, and
   the wider the panel gets on a desktop the more a 100% button looks like
   a banner. It stays left-aligned with the fields above it, so the column
   still has one left edge.
   ⚠️ min-width so it does not shrink to a stub inside the retracted panel
   or on a phone, where three quarters of a narrow column is not a button
   somebody can read. */
/* ⚠️ THREE QUARTERS AND CENTRED. A full-width button reads as the panel's
   edge rather than a control; left-aligned at 75% it left a ragged gap on
   the right that nothing else in the section shared. `margin-inline` is
   what centres a block-level button -- justify-content only centres its
   own label. */
.coms-ai-go {
  display: block;
  width: 75%;
  min-width: 13rem;
  max-width: 100%;
  margin: 0.25rem auto 0;
  text-align: center;
  font-size: 0.85rem;
}

.coms-ai-log { display: flex; flex-direction: column; gap: 0.5rem; }
.coms-ai-msg {
  padding: 0.7rem 0.85rem; border-radius: 10px;
  font-size: 0.82rem; line-height: 1.5;
  background: var(--c-surface-subtle, #f8fafc);
  color: var(--c-text-muted, #64748b);
  border: 1px solid var(--c-border, #e2e8f0);
}
.coms-ai-msg-ok { border-color: var(--c-success-border, #bbf7d0); background: var(--c-success-tint, #dcfce7); color: var(--c-success-deep, #166534); }
.coms-ai-msg-error { border-color: var(--c-danger-border, #fecaca); background: var(--c-danger-tint, #fee2e2); color: var(--c-danger-deep, #b91c1c); }
.coms-ai-msg-cost { display: block; margin-top: 0.3rem; font-size: 0.72rem; opacity: 0.85; }

.coms-ai-foot {
  flex-shrink: 0; margin: 0;
  padding: 0.7rem 1.05rem;
  border-top: 1px solid var(--c-border, #e2e8f0);
  font-size: 0.72rem; color: var(--c-text-dim, #94a3b8);
}

/* ── Retract ─────────────────────────────────────────────────────────
 *
 * ⚠️ RETRACTED IS NOT HIDDEN, and the two are independent classes on
 * purpose. `.hidden` means the panel is not in play at all; retracted
 * means it is running, holding a conversation, possibly mid-build, and
 * merely out of the way. Closing while retracted has to take BOTH the
 * panel and its handle off screen, which is why the handle carries its
 * own `.hidden` rather than being tied to the panel's state.
 *
 * The same translate the close animation uses, so there is one movement
 * to reason about and reduced-motion is already handled above.
 */
.coms-ai-panel.is-retracted {
  transform: translateX(101%);
  pointer-events: none;
}

.coms-ai-head-actions { display: flex; align-items: center; gap: 0.1rem; flex-shrink: 0; }

/* The way back. Fixed to the right edge, vertically centred -- the panel
 * it restores is full height, so no single vertical position is "where it
 * went", and the middle is the findable one. */
.coms-ai-handle {
  position: fixed;
  top: 50%;
  right: 0;
  transform: translateY(-50%);
  /* Level with the panel: they are never both on screen, so there is
     nothing for them to fight over. */
  z-index: 910;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.3rem;
  width: 2.4rem;
  padding: 0.85rem 0;
  border: 1px solid var(--c-border);
  border-right: none;
  border-radius: 10px 0 0 10px;
  background: var(--c-surface);
  color: var(--c-text);
  font-size: 1rem;
  cursor: pointer;
  box-shadow: -6px 0 18px color-mix(in srgb, var(--c-shadow) 14%, transparent);
}
.coms-ai-handle:hover {
  background: var(--c-brand-tint);
  box-shadow: -6px 0 18px color-mix(in srgb, var(--c-shadow) 22%, transparent);
  transform: translateY(-50%);
}
/* ⚠️ The hover transform RESTATES translateY(-50%). The base rule uses
 * transform for centring, and `button:hover` elsewhere in this file sets
 * its own -- without this the handle jumps to the wrong place on hover,
 * which is the same trap .mnw-plan-card hit with its background. */
.coms-ai-handle:focus-visible {
  outline: 2px solid var(--c-brand);
  outline-offset: -2px;
}
.coms-ai-handle-glyph { display: block; line-height: 1; }

/* Lit only while a run is in flight. A permanent dot is a light people
 * stop seeing. */
.coms-ai-handle-dot {
  position: absolute;
  top: 0.4rem; right: 0.4rem;
  width: 0.45rem; height: 0.45rem;
  border-radius: 50%;
  background: var(--c-brand);
  animation: comsAiHandlePulse 1.4s ease-in-out infinite;
}
@keyframes comsAiHandlePulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.25; }
}
@media (prefers-reduced-motion: reduce) {
  .coms-ai-handle-dot { animation: none; }
}

@media (max-width: 620px) { .coms-ai-panel { width: 100vw; } }

/* Visually hidden but read by a screen reader. Standard clip pattern --
   display:none and visibility:hidden both remove it from the accessibility
   tree, which defeats the point of having a label at all. */
/* The directory's access-profile filter, beside the search box. */
.users-profile-filter { flex: 0 0 auto; }
.users-profile-filter select { min-width: 12rem; }

/* The access profile on a directory card. Deliberately not the role
   palette: they are different axes and a shared colour would read as the
   same fact twice. */
.users-profile-badge {
  background: var(--c-surface-subtle, #f1f5f9);
  color: var(--c-text-muted, #64748b);
  border: 1px solid var(--c-border, #e2e8f0);
}

/* What a system account's row actually holds, under the address the card
   shows. Only rendered when the two differ. */
.user-card-stored {
  font-size: 0.7rem;
  color: var(--c-text-muted, #64748b);
  margin-top: 0.1rem;
}

.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ── Landing hero: copy on the left, number picker on the right ────── */
/* ⚠️ .landing-hero-inner IS CAPPED AT 780px, which is right for one centred
   column and far too narrow for two.
   ⚠️ TWO CLASSES, NOT ONE. `.landing-hero-split` alone is (0,1,0) -- the
   same specificity as the rule it has to beat -- so which one wins would
   depend on source order, which is what made the "Main Workspace" badge
   permanently visible and what the carrier-square grid rule had to be fixed
   for. `.landing-hero-inner.landing-hero-split` is (0,2,0) and wins
   wherever either block ends up in the file. */
.landing-hero-inner.landing-hero-split {
  max-width: 1160px;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
  gap: 3rem;
  align-items: center;
}
/* The copy column keeps the hero's original centred-block measure rather
   than stretching to whatever the grid gives it. */
.landing-hero-copy { max-width: 34rem; }
/* ⚠️ The copy column is a THIRD now, so the 2.75rem headline that fit a
   780px centred block has ~360px to work in. clamp() keeps the original
   size on a wide screen and scales it down rather than letting it wrap
   three ways -- scoped to the split, so the un-split hero is untouched.
   Two classes for the same specificity reason as the width above:
   .landing-hero-inner h1 is (0,1,1) and this has to beat it. */
.landing-hero-inner.landing-hero-split h1 {
  font-size: clamp(2rem, 3.4vw, 2.75rem);
}

.landing-picker {
  /* 15px all round, and nothing inside adds its own outer margin -- the
     head, the fields and the foot each space themselves downward only, so
     the card's edge is this padding and only this padding. */
  padding: 15px;
  border-radius: 16px;
  /* The hero is dark in both themes, so this card states its own surface
     from the chrome tokens rather than the page ones -- .legal-hero and the
     header do the same. Using --c-surface here would make it white in light
     mode and invisible against the hero in dark. */
  background: color-mix(in srgb, var(--c-highlight, #ffffff) 7%, transparent);
  border: 1px solid color-mix(in srgb, var(--c-highlight, #ffffff) 14%, transparent);
  backdrop-filter: blur(6px);
  text-align: left;
}
/* ⚠️ numberPicker.js hides the whole card when the search answers 503 --
   a deployment with no Telnyx master key cannot sell numbers at all. The
   rule is written out because `hidden` is UA-sheet `display: none` and
   loses to any author rule that sets display on this class; nothing does
   today, and this makes adding one safe. */
.landing-picker[hidden] { display: none; }
.landing-picker-head h2 {
  margin: 0 0 0.2rem;
  font-size: 1.15rem;
  color: var(--c-on-accent, #ffffff);
}
.landing-picker-head p { margin: 0 0 0.9rem; font-size: 0.86rem; color: var(--c-chrome-text, #cbd5e1); }

/* Two by two, so four filters fit without making the card tall. */
.landing-picker-fields {
  display: grid;
  /* Four across at two-thirds width; auto-fit rather than a fixed 4, so it
     folds to 2 and then 1 on its own instead of needing a breakpoint per
     step. */
  grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
  gap: 0.6rem 0.7rem;
  margin-bottom: 0.75rem;
}
.landing-picker-field { min-width: 0; }
.landing-picker-field label {
  display: block; margin-bottom: 0.2rem;
  font-size: 0.68rem; font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--c-chrome-text, #cbd5e1);
}
/* A disabled prefix field must LOOK unavailable, or the rule reads as the
   field being broken. */
.landing-picker-field input:disabled { opacity: 0.4; cursor: not-allowed; }
.landing-picker-form { display: block; }
.landing-picker-form > button { width: 100%; justify-content: center; }
.landing-picker-form input {
  width: 100%; min-width: 0;
  padding: 0.55rem 0.75rem;
  border-radius: 9px;
  border: 1px solid color-mix(in srgb, var(--c-highlight, #ffffff) 20%, transparent);
  background: color-mix(in srgb, var(--c-scrim, #0f172a) 35%, transparent);
  color: var(--c-on-accent, #ffffff);
  font: inherit; font-size: 0.92rem;
}
.landing-picker-form input::placeholder { color: var(--c-chrome-text, #cbd5e1); opacity: 0.7; }
.landing-picker-form input:focus-visible { outline: none; border-color: var(--c-brand, #6366f1); box-shadow: var(--btn-ring); }
.landing-picker-form button { padding: 0.65rem 1.1rem; }

.landing-picker-note { margin: 0.7rem 0 0; font-size: 0.8rem; line-height: 1.45; color: var(--c-chrome-text, #cbd5e1); }
.landing-picker-note.is-error { color: var(--c-danger-tint, #fee2e2); }

/* A scrolling list rather than a growing card: the hero must not change
   height when six results arrive, or everything below it jumps. */
.landing-picker-results {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 0.35rem;
  /* Still capped and still scrolling: two columns of six is three rows, but
     the hero must not change height when results arrive or everything below
     it jumps. */
  max-height: 15rem; overflow-y: auto;
  margin-top: 0.7rem;
}
.landing-picker-results:empty { margin-top: 0; }

.landing-picker-num {
  display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem;
  width: 100%; text-align: left;
  padding: 0.55rem 0.75rem;
  border-radius: 9px;
  border: 1px solid color-mix(in srgb, var(--c-highlight, #ffffff) 12%, transparent);
  background: color-mix(in srgb, var(--c-scrim, #0f172a) 25%, transparent);
  box-shadow: none;
  color: var(--c-on-accent, #ffffff);
  font-family: inherit; font-size: 0.92rem; font-weight: 600;
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.landing-picker-num:hover {
  background: color-mix(in srgb, var(--c-brand, #6366f1) 28%, transparent);
  border-color: var(--c-brand, #6366f1);
  box-shadow: none; transform: none;
}
.landing-picker-num span {
  font-size: 0.74rem; font-weight: 500;
  color: var(--c-chrome-text, #cbd5e1);
  text-transform: capitalize;
}

.landing-picker-foot {
  margin: 0.9rem 0 0; padding-top: 0.8rem;
  border-top: 1px solid color-mix(in srgb, var(--c-highlight, #ffffff) 12%, transparent);
  font-size: 0.76rem; line-height: 1.5;
  color: var(--c-chrome-text, #cbd5e1);
}

@media (max-width: 900px) {
  /* Stacked, and the picker goes BELOW the copy -- somebody arriving on a
     phone should read what this is before being asked to pick a number.

     ⚠️ TWO CLASSES HERE TOO, AND THIS BLOCK GOT IT WRONG. A media query
     adds NOTHING to specificity, so `.landing-hero-split` at (0,1,0) never
     beat the two-column `.landing-hero-inner.landing-hero-split` at (0,2,0)
     -- the hero stayed side-by-side at every width, and the copy column was
     a third of a phone screen. Which is exactly what the comment above that
     base rule warns about, in this same file, eight lines up. */
  .landing-hero-inner.landing-hero-split { grid-template-columns: minmax(0, 1fr); gap: 2rem; }
  .landing-hero-copy { max-width: none; }
}

/* A link the assistant wrote. Underlined and in the brand colour, because
   an answer that is a link has to look like one -- the whole point is that
   it is followable rather than a path to read out. */
.ai-nav-link {
  color: var(--c-brand, #6366f1);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.ai-nav-link:hover { color: var(--c-brand-deep, #4338ca); }

/* ── "More from Linxi" — the cross-links at the foot of a page ────────
   A <nav> outside <main>, so a screen reader meets it as navigation about
   the site rather than as part of the document it just finished reading. */
.page-links {
  border-top: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc);
}
.page-links-inner { max-width: 1000px; margin: 0 auto; padding: 2.25rem 1.5rem 2.5rem; }
.page-links h2 {
  margin: 0 0 1rem;
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--c-text-dim, #94a3b8);
}
.page-links ul {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 0.7rem;
}
.page-links a {
  display: flex; flex-direction: column; gap: 0.25rem; height: 100%;
  padding: 0.9rem 1rem;
  border: 1px solid var(--c-border, #e2e8f0); border-radius: 10px;
  background: var(--c-surface, #ffffff);
  text-decoration: none;
  transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.12s ease;
}
.page-links a:hover {
  border-color: var(--c-brand, #6366f1);
  box-shadow: var(--btn-shadow-hover);
  transform: translateY(-1px);
}
.page-links a:focus-visible { outline: none; border-color: var(--c-brand, #6366f1); box-shadow: var(--btn-ring); }
.page-links strong { font-size: 0.92rem; color: var(--c-text, #0f172a); }
.page-links span { font-size: 0.82rem; line-height: 1.45; color: var(--c-text-muted, #64748b); }

/* ── Register: three columns on a wide screen ─────────────────────────
   The card was capped at 760px in a two-column grid (brand+alt+footer on
   the left, the whole form on the right), which left the form itself as one
   long single-file column. Wide now, with the FORM's own fields in two
   columns beside the brand rail -- so the page reads as three.

   ⚠️ min-width, like the 820px rule above it. This is an enhancement on top
   of an already-correct mobile-first stack, not a narrow-screen fallback
   off a desktop default -- the one deliberate exception to this file's
   max-width convention, and the reason register.html duplicates no markup
   for the two breakpoints. */
@media (min-width: 1080px) {
  .register-card {
    /* Overrides .login-card's 340px AND the 820px rule's 760px. Two classes
       for the same reason the hero split needed them: .register-card alone
       ties with .login-card at (0,1,0) and would be decided by source
       order. */
    max-width: 1080px;
  }
  .login-card.register-card {
    max-width: 1080px;
    grid-template-columns: minmax(0, 17rem) minmax(0, 1fr);
    gap: 1rem 3rem;
  }

  /* The form's own fields become the second and third columns. */
  #register-form {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    /* Row gap of 0: the vertical rhythm comes from .reg-field's own
       bottom margin, so it is the same in one column and in two. */
    gap: 0 1.25rem;
    align-items: start;
  }
  /* One box per field, so a label and its input, hint and error stay
     together in a cell. Without the wrappers added in register.html they
     are loose siblings and a grid deals them out one per cell. */
  .reg-field { min-width: 0; }

  /* ⚠️ Everything that is NOT a field spans both columns. A submit button
     or a consent sentence sitting in one half reads as a layout fault, and
     the account-type grid is nine options wide on its own. */
  #register-form > :not(.reg-field) { grid-column: 1 / -1; }

  /* ⚠️ Nine options in two columns was right in a 340px card. Here the
     Advanced Setup block spans the whole form, so two columns means nine
     very wide cells stacked five deep -- auto-fit uses the width instead
     and folds back on its own. This is a consequence of the three-column
     layout, not a separate design choice. */
  #account-type-grid {
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  }

  /* ⚠️ The name row spans BOTH columns, and that is what makes Display Name
     and Last name half each. In a single column its two inputs would be a
     quarter of the form apiece, which is narrower than the placeholder they
     carry. Everything else sits one per column:

       [ Display name | Last name ]
       [ Username     | Email     ]
       [ Password     | Confirm   ]

     Username used to span. It does not need to -- the hint under it wraps
     to two lines in half the width and reads fine, and a full-width field
     between two half-width ones is the thing that looked wrong. */
  .reg-field:has(.reg-name-row) { grid-column: 1 / -1; }
}

/* ── Register form: a tight, consistent vertical rhythm ───────────────
   ⚠️ THE LOOSE GAPS WERE THE BROWSER'S, NOT THIS FILE'S. There is no global
   `p` rule in this stylesheet, so the supporting lines inside the form --
   <p class="muted">You can change this anytime…</p> and friends -- were
   getting the UA default `margin: 1em 0`, i.e. 16px above AND below each
   one. .reg-username-hint and .field-error already declared their own tight
   margins; the un-classed ones did not, which is why the spacing looked
   inconsistent from field to field rather than merely large.

   Scoped to .reg-field so it covers every supporting line a field carries
   -- hint, error, and anything added later -- without naming them. */
#register-form .reg-field > p,
#register-form .reg-field > small {
  margin: 0.15rem 0 0;
  line-height: 1.35;
}

/* (1,0,2), so it beats #register-form label at (1,0,1) on SPECIFICITY
   rather than on where these two blocks happen to sit in the file -- the
   same care the hero and card width overrides needed. The space above a
   label now comes from the field's own bottom margin, so it is one gap
   between fields instead of two stacked ones. */
#register-form .reg-field label {
  margin-top: 0;
  margin-bottom: 0.2rem;
}

/* ⚠️ .reg-field CARRIES NO BOTTOM MARGIN, DELIBERATELY, and the fields
   therefore sit flush: the desktop grid's row-gap is 0 (see `gap: 0 1.25rem`
   in the 1080px block -- column gap only), and there is no margin below a
   field on any breakpoint either. The separation you see is the label's
   own weight and the input's border, nothing more.

   If a gap is ever wanted back, change the grid's ROW-GAP rather than
   adding a margin here: a margin applies on mobile too, where there is no
   grid, and the two would then have to be kept in step by hand. */
#register-form .reg-field .reg-name-row { margin-bottom: 0; }

/* 15px above the whole form, so the first label is not flush against the
   divider once its own top margin is gone. */
#register-form { margin-top: 15px; }

/* ⚠️ 15px MORE top and bottom than the shared login card, on the register
   card only. Written as calc so the "+15px" is legible as an intent rather
   than as two numbers somebody has to diff against .login-card to
   understand -- and the base values are named here because .login-card is
   shared with login.html, which is deliberately NOT being made taller.

   ⚠️ (0,2,0) IS REQUIRED HERE, NOT TIDINESS. There are THREE rules with a
   claim on this card's padding and all three are (0,1,0), so among them it
   is decided by source order:

     .card            padding: 1.25rem 1.5rem
     .login-card      padding-top: 1rem; padding-bottom: 1.1rem
     .card            padding: 1rem            (inside the mobile query,
                                                LATER in the file, so it
                                                already overrides
                                                .login-card's longhands on a
                                                narrow screen)

   .login-card.register-card beats all three on specificity, at every width,
   wherever this block ends up. Written as `.register-card` alone it would
   tie with them and land back in the same argument. */
.login-card.register-card {
  padding-top: calc(1rem + 15px);
  padding-bottom: calc(1.1rem + 15px);
}

/* ── Brand name colour (Admin → Branding → Site Branding) ─────────── */
.brand-ink-block { margin-top: 1.75rem; padding-top: 1.5rem; border-top: 1px solid var(--c-border, #e2e8f0); }
.brand-ink-block h3 { margin: 0 0 0.2rem; font-size: 1rem; }
.bi-toggle { display: flex; align-items: center; gap: 0.5rem; margin: 0.9rem 0; font-size: 0.9rem; }
.bi-toggle input { width: auto; }

.bi-controls { padding: 1rem; border: 1px solid var(--c-border, #e2e8f0); border-radius: 10px; background: var(--c-surface-subtle, #f8fafc); }
.bi-kind { display: flex; gap: 1.25rem; margin-bottom: 0.9rem; font-size: 0.88rem; }
.bi-kind label { display: flex; align-items: center; gap: 0.4rem; font-weight: 500; }
.bi-kind input { width: auto; }

.bi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: 0.9rem; margin-bottom: 1rem; }
.bi-field { min-width: 0; }
.bi-swatch { display: flex; align-items: center; gap: 0.5rem; }
/* A native colour input is a tiny box by default; give it a real target. */
.bi-swatch input[type="color"] {
  width: 2.6rem; height: 2.2rem; flex-shrink: 0; padding: 2px;
  border: 1px solid var(--c-border, #e2e8f0); border-radius: 6px;
  background: var(--c-surface, #ffffff); cursor: pointer;
}
.bi-swatch input[type="text"] { min-width: 0; font-family: monospace; font-size: 0.85rem; }
.bi-swatch input[type="range"] { flex: 1; min-width: 0; }
.bi-angle-value { flex-shrink: 0; font-size: 0.82rem; font-variant-numeric: tabular-nums; color: var(--c-text-muted, #64748b); }

.bi-presets { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1rem; }
/* The swatch IS the gradient, so a preset shows what it does rather than
   naming a colour somebody has to imagine. */
.bi-preset {
  padding: 0.45rem 0.8rem; border: 1px solid var(--c-border, #e2e8f0); border-radius: 8px;
  box-shadow: none; cursor: pointer;
}
.bi-preset span {
  font-size: 0.78rem; font-weight: 700; color: var(--c-on-accent, #ffffff);
  text-shadow: 0 1px 2px color-mix(in srgb, var(--c-scrim, #0f172a) 55%, transparent);
}
.bi-preset:hover { border-color: var(--c-brand, #6366f1); box-shadow: none; transform: translateY(-1px); }

.bi-preview {
  padding: 1.25rem; border: 1px dashed var(--c-border, #e2e8f0); border-radius: 10px;
  background: var(--c-surface, #ffffff); text-align: center;
}
#bi-preview-text { font-size: 1.75rem; font-weight: 800; letter-spacing: -0.02em; }

/* ── Admin panel inputs ───────────────────────────────────────────────
   ⚠️ THERE IS NO GLOBAL `input` RULE IN THIS STYLESHEET. The only thing
   touching a bare input anywhere is the mobile `font-size: 16px !important`
   that stops iOS zooming on focus. So every text field in every admin panel
   has been rendering with pure browser defaults -- a 2px inset border, the
   UA's own control font rather than the page's, and whatever padding the
   platform felt like. Next to the buttons and cards around them they read
   as unfinished rather than as plain.

   Scoped to [data-admin-panel-id], so this is the admin surface and nothing
   else -- the register form, the login card, the contact sheet and the
   composer all have their own field styling already, and none of them is
   touched.

   ⚠️ NO `width`. Several admin panels lay their fields out themselves --
   .admin-email-field .input-actions input is a flex child, and the brand
   colour hex boxes sit beside a swatch -- and a blanket width: 100% here
   would break each of them. Appearance only.

   ⚠️ The type exclusions matter: a colour input styled with padding and a
   border becomes a grey box with a smear in it, a range loses its track,
   and a checkbox becomes a large empty square. They carry their own chrome
   and are left alone. */
/* ⚠️⚠️ #tab-forms IS IN EVERY SELECTOR BELOW, AND HAS TO BE.
 *
 * The Forms builder exists twice: as an admin panel, and as the per-Linxi
 * tab. This block is what gives a panel's inputs their background, text
 * colour, border and placeholder — and it is scoped to
 * [data-admin-panel-id], which the tab does not have.
 *
 * Without it the tab's inputs fall back to the BROWSER's defaults, which
 * are white with black text and do not follow the theme at all. In light
 * mode that is merely slightly wrong; in dark mode it is white boxes on a
 * dark card, which is how this was reported.
 *
 * ⚠️ The attribute is NOT added to the tab's card instead. Its value is
 * how showAdminPanel finds a panel to open, and a second element carrying
 * `site-forms` would make that ambiguous.
 *
 * ⚠️ A rule added to this block later must gain #tab-forms too --
 * t-site-forms.js walks every selector here and fails if one covers only
 * half of the same UI. */
[data-admin-panel-id] input:not([type="color"]):not([type="range"]):not([type="checkbox"]):not([type="radio"]):not([type="file"]),
#tab-forms input:not([type="color"]):not([type="range"]):not([type="checkbox"]):not([type="radio"]):not([type="file"]),
[data-admin-panel-id] textarea,
#tab-forms textarea {
  /* font: inherit is the load-bearing one -- inputs do NOT inherit the
     page font, the same reason .ws-rail-btn states it. */
  font: inherit;
  font-size: 0.9rem;
  padding: 0.5rem 0.7rem;
  border: 1px solid var(--c-border-strong, #cbd5e1);
  border-radius: 8px;
  /* ⚠️ THE COLOURS COME FROM THE BASE RULE NOW (see "Text-entry
     controls, everywhere" in the reset). This block keeps the padding
     and the size, which are its own; a second answer to what a field is
     coloured is the fork where one of the two gets updated. */
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
[data-admin-panel-id] textarea,
#tab-forms textarea { line-height: 1.5; resize: vertical; }
[data-admin-panel-id] input:not([type="color"]):not([type="range"]):not([type="checkbox"]):not([type="radio"]):not([type="file"])::placeholder,
#tab-forms input:not([type="color"]):not([type="range"]):not([type="checkbox"]):not([type="radio"]):not([type="file"])::placeholder,
[data-admin-panel-id] textarea::placeholder,
#tab-forms textarea::placeholder {
  color: var(--c-text-dim, #94a3b8);
}
/* ⚠️⚠️ THE FOCUS RULE THAT STOOD HERE WAS CORRUPT AND HAD NEVER
   APPLIED. Its comment had been duplicated and interleaved by an earlier
   edit, leaving a stray comment TERMINATOR inside the selector list:

       /* :focus-visible,
       /* :focus-visible,
       not :focus -- ... ,
       and
          ... distinction. * /
       [data-admin-panel-id] input...:focus-visible,
       and
          ... distinction. * /          <- parse error, in a selector
       #tab-forms input...:focus-visible, ...

   A malformed selector invalidates the WHOLE rule, so neither the
   `outline: none` nor the themed ring ever reached an admin field --
   they kept the browser's default outline, which is why nothing looked
   broken enough to report. Found by a sweep for a terminator outside a
   comment;
   it was the only one in 22,000 lines.

   ⚠️ DELETED RATHER THAN REPAIRED. Every selector it named is covered by
   the base rule in the reset -- [data-admin-panel-id] textarea is a
   textarea, and its inputs are the named text types -- so repairing it
   would restore a second copy of an answer the base already gives. */
[data-admin-panel-id] input:disabled,
#tab-forms input:disabled,
[data-admin-panel-id] textarea:disabled,
#tab-forms textarea:disabled {
  background: var(--c-surface-sunken, #f1f5f9);
  color: var(--c-text-dim, #94a3b8);
  cursor: not-allowed;
}
/* A monospace field holding a hex value should look like one. */


[data-admin-panel-id] .bi-swatch input[type="text"] {
  font-family: monospace;
  letter-spacing: 0.02em;
  text-transform: lowercase;
}

/* ── Coms layout blocks ────────────────────────────────────────────────
   The divs `::: callout brand` and friends render into. See the header of
   src/services/comsMarkdown.js for why the source carries a TONE NAME and
   never a colour.

   ⚠️ EVERY COLOUR HERE IS A PALETTE TOKEN. That is the whole point: an
   article written today follows a rebrand made next year, and follows dark
   mode, because it never stated a colour of its own. A hex in this block
   would freeze one post's callouts to whatever the palette was on the day
   it was written.

   ⚠️ The tone tints are color-mix against --c-surface, not fixed pastels.
   Mixed against the SURFACE they stay legible in both modes without a
   second set of dark values -- the same technique the AI smart buttons and
   the E2E notices already use. */
.coms-body .coms-callout,
.coms-body .coms-card-block,
.coms-body .coms-cta,
.coms-body .coms-stat,
.coms-body .coms-block {
  margin: 1.6rem 0;
  padding: 1.05rem 1.25rem;
  border-radius: 12px;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc);
}
/* The last child's trailing margin pushes the container's own padding out
   of shape -- a callout ending in a paragraph looked bottom-heavy against
   one ending in a list. */
.coms-body .coms-callout > :last-child,
.coms-body .coms-card-block > :last-child,
.coms-body .coms-cta > :last-child,
.coms-body .coms-stat > :last-child,
.coms-body .coms-lead-block > :last-child,
.coms-body .coms-col > :last-child,
.coms-body .coms-figure > :last-child { margin-bottom: 0; }

.coms-body .coms-callout { border-left-width: 3px; }
.coms-body .coms-callout > :first-child,
.coms-body .coms-card-block > :first-child { margin-top: 0; }

/* Tones. Only the four properties that carry meaning move -- the border,
   its left edge, the wash and the heading ink -- so a tone reads as the
   same component in a different key rather than as a different component. */
.coms-body .coms-tone-brand   { border-color: var(--c-brand, #6366f1);   background: color-mix(in srgb, var(--c-brand, #6366f1) 8%, var(--c-surface, #ffffff)); }
.coms-body .coms-tone-info    { border-color: var(--c-info, #0ea5e9);    background: color-mix(in srgb, var(--c-info, #0ea5e9) 8%, var(--c-surface, #ffffff)); }
.coms-body .coms-tone-success { border-color: var(--c-success, #16a34a); background: color-mix(in srgb, var(--c-success, #16a34a) 8%, var(--c-surface, #ffffff)); }
.coms-body .coms-tone-warning { border-color: var(--c-warning, #f59e0b); background: color-mix(in srgb, var(--c-warning, #f59e0b) 10%, var(--c-surface, #ffffff)); }
.coms-body .coms-tone-danger  { border-color: var(--c-danger, #dc2626);  background: color-mix(in srgb, var(--c-danger, #dc2626) 8%, var(--c-surface, #ffffff)); }
.coms-body .coms-tone-neutral { border-color: var(--c-border, #e2e8f0);  background: var(--c-surface-subtle, #f8fafc); }

.coms-body .coms-tone-brand   h2, .coms-body .coms-tone-brand   h3, .coms-body .coms-tone-brand   h4 { color: var(--c-brand-deep, #4338ca); }
/* ⚠️ THE TOKEN NAMES ARE CHECKED AGAINST THE STYLESHEET, not guessed from
   the pattern. There is no --c-info-deep, --c-success-deep or
   --c-warning-deep on this install -- those three families spell it *-text,
   while brand and danger spell it *-deep. A bare var() naming a token that
   does not exist is invalid at computed-value time and does NOTHING, with
   no parse error and nothing logged: the 51 dead declarations CLAUDE.md
   records under the Linxi overview panel. */
.coms-body .coms-tone-info    h2, .coms-body .coms-tone-info    h3, .coms-body .coms-tone-info    h4 { color: var(--c-info-text, #0369a1); }
.coms-body .coms-tone-success h2, .coms-body .coms-tone-success h3, .coms-body .coms-tone-success h4 { color: var(--c-success-text, #15803d); }
.coms-body .coms-tone-warning h2, .coms-body .coms-tone-warning h3, .coms-body .coms-tone-warning h4 { color: var(--c-warning-text, #b45309); }
.coms-body .coms-tone-danger  h2, .coms-body .coms-tone-danger  h3, .coms-body .coms-tone-danger  h4 { color: var(--c-danger-deep, #991b1b); }

/* A heading inside a block is a label, not a section, so it does not carry
   the 2.5rem the article's own headings do. */
.coms-body .coms-callout h3, .coms-body .coms-card-block h3,
.coms-body .coms-cta h3, .coms-body .coms-stat h3 { margin: 0 0 0.4rem; font-size: 1.05rem; }
.coms-body .coms-callout h4, .coms-body .coms-card-block h4,
.coms-body .coms-cta h4, .coms-body .coms-stat h4 { margin: 0 0 0.3rem; font-size: 0.95rem; }

.coms-body .coms-cta { text-align: center; padding: 1.5rem 1.25rem; }
.coms-body .coms-cta a {
  display: inline-block; margin-top: 0.35rem; padding: 0.6rem 1.35rem;
  border-radius: 999px; font-weight: 600; text-decoration: none;
  background: var(--c-brand, #6366f1); color: var(--c-on-accent, #ffffff);
}
.coms-body .coms-stat { text-align: center; }
.coms-body .coms-stat h2 { margin: 0; font-size: 2.1rem; line-height: 1.1; }
.coms-body .coms-lead-block { margin: 0 0 1.5rem; font-size: 1.18rem; color: var(--c-text-muted, #64748b); }

/* ⚠️ minmax(0, 1fr), not 1fr. A grid child refuses to shrink below its
   content, so a long unbroken line -- a URL, a code span -- pushes the
   column wider than the article and the whole page scrolls sideways. */
.coms-body .coms-columns,
.coms-body .coms-grid-blocks {
  display: grid; gap: 1rem; margin: 1.6rem 0;
  grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
}
.coms-body .coms-columns > *, .coms-body .coms-grid-blocks > * { min-width: 0; }
.coms-body .coms-col { margin: 0; }
.coms-body .coms-figure { margin: 1.6rem 0; }
.coms-body .coms-figure img { margin: 0 0 0.5rem; }
.coms-body .coms-figure p:last-child { font-size: 0.88rem; color: var(--c-text-muted, #64748b); }

/* ⚠️ A raw `::: html` block gets the article's own measure and nothing
   else. It is the author's markup and styling it further would fight
   whatever they wrote; overflow-x is the one guard, because a wide table
   pasted in here would otherwise scroll the PAGE sideways on a phone. */
.coms-body .coms-block { overflow-x: auto; }
.coms-body table { width: 100%; border-collapse: collapse; margin: 1.5rem 0; font-size: 0.95rem; }
.coms-body th, .coms-body td { padding: 0.5rem 0.7rem; border: 1px solid var(--c-border, #e2e8f0); text-align: left; }
.coms-body th { background: var(--c-surface-subtle, #f8fafc); font-weight: 600; }

/* ── Coms tags ──────────────────────────────────────────────────────── */
.coms-tags { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0.75rem 0 0; }
.coms-tag-chip {
  padding: 0.2rem 0.6rem; border-radius: 999px; text-decoration: none;
  font-size: 0.74rem; font-weight: 600; letter-spacing: 0.01em;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc);
  color: var(--c-text-muted, #64748b);
}
.coms-tag-chip:hover { border-color: var(--c-brand, #6366f1); color: var(--c-brand, #6366f1); }

/* ── The builder's tag bar and Insert bar ──────────────────────────── */

/* One field made of chips plus an input. The chips are INSIDE the border
   so it reads as a single control -- chips above a separate box read as a
   list of something else with an unrelated text field under it. */
.coms-tagbar {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem;
  padding: 0.35rem 0.45rem; cursor: text;
  border: 1px solid var(--c-border, #e2e8f0); border-radius: 8px;
  background: var(--c-surface, #ffffff);
}
.coms-tagbar:focus-within { border-color: var(--c-brand, #6366f1); }
/* ⚠️ THE BAR OWNS THE FOCUS, NOT THE INPUT INSIDE IT. The field is
   borderless and transparent by design -- it is a caret sitting among
   the tag pills -- so the base rule's ring would draw a second, inner
   box around it while :focus-within was already outlining the bar. */
.coms-tagbar input:focus-visible { box-shadow: none; border-color: transparent; }
.coms-tagbar-chips { display: contents; }
/* ⚠️ display: contents on the chip host. The chips and the input have to
   flow as siblings in ONE wrapping row, and a real box around the chips
   would make them a single flex item that wraps as a block -- the input
   then sits under the whole group instead of after the last chip. */
.coms-tagbar input {
  flex: 1 1 8rem; min-width: 6rem;
  border: none; outline: none; background: none; padding: 0.2rem 0.15rem;
  font: inherit; color: var(--c-text, #0f172a);
}
.coms-tag-input-chip {
  display: inline-flex; align-items: center; gap: 0.3rem;
  padding: 0.15rem 0.2rem 0.15rem 0.55rem; border-radius: 999px;
  font-size: 0.74rem; font-weight: 600;
  background: color-mix(in srgb, var(--c-brand, #6366f1) 12%, var(--c-surface, #ffffff));
  color: var(--c-brand-deep, #4338ca);
}
.coms-tag-x {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.05rem; height: 1.05rem; padding: 0; border: none; border-radius: 999px;
  background: none; color: inherit; font-size: 0.9rem; line-height: 1; cursor: pointer;
}
.coms-tag-x:hover { background: color-mix(in srgb, var(--c-brand, #6366f1) 25%, transparent); }
.coms-hint { margin: 0.3rem 0 0; font-size: 0.7rem; color: var(--c-text-dim, #94a3b8); }

/* A checkbox in the builder's sidebar. The label is the hit target, so
   there is no for=/id pair to keep in step beyond the one the markup
   already has -- and the text sits on the box's own line rather than
   under it, which is what the column's other labels do. */
.coms-check {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  margin-top: 0.75rem;
  font-size: 0.78rem;
  font-weight: 600;
  cursor: pointer;
}
.coms-check input { margin: 0; }

/* A checkbox with a ? beside it. The spacing moves to the ROW, and the
   label gives it up -- leaving margin-top on both would double the gap
   above every row that has one. The label still takes the whole width it
   can, so the ? stays pinned at the end rather than floating beside the
   text. Same shape as .coms-kind-row above, and the note it reveals is
   the same .coms-kind-note. */
.coms-check-row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.75rem;
}
.coms-check-row .coms-check { margin-top: 0; flex: 1 1 auto; min-width: 0; }
.coms-hint-full { color: var(--c-warning-text, #b45309); }

.coms-insert-bar {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem;
  padding: 0.4rem 0.6rem;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc);
}
.coms-insert-label {
  font-size: 0.68rem; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--c-text-dim, #94a3b8);
  margin-right: 0.2rem;
}
.coms-insert-btn {
  padding: 0.2rem 0.55rem; border-radius: 6px; cursor: pointer;
  font-size: 0.73rem; font-weight: 600;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface, #ffffff);
  color: var(--c-text-muted, #64748b);
}
.coms-insert-btn:hover {
  border-color: var(--c-brand, #6366f1);
  color: var(--c-brand, #6366f1);
  background: var(--c-surface, #ffffff);
}
/* ⚠️ The raw-HTML button is marked out because it is the one control here
   that leaves the safe subset. It is not disabled or hidden -- a
   super_admin's own markup is the point of it -- but it should not look
   like inserting a callout. */
.coms-insert-btn-raw { border-style: dashed; color: var(--c-warning-text, #b45309); }
.coms-insert-btn-raw:hover { border-color: var(--c-warning, #f59e0b); color: var(--c-warning-text, #b45309); }

/* ── Admin → Analytics → Traffic ───────────────────────────────────── */
.an-live-dot {
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.02em;
  padding: 0.15rem 0.55rem; border-radius: 999px;
  color: var(--c-success-text, #15803d);
  background: color-mix(in srgb, var(--c-success, #16a34a) 14%, var(--c-surface, #ffffff));
}
.an-live-dot:empty { display: none; }

.an-active-row {
  display: flex; align-items: flex-start; gap: 0.6rem;
  padding: 0.45rem 0; border-bottom: 1px solid var(--c-border, #e2e8f0);
}
.an-active-row:last-child { border-bottom: none; }
/* ⚠️ aria-hidden in the markup, and the same facts are written as text
   beside it. Colour alone is never the message -- the SMS Available
   pill's rule. */
.an-active-dot {
  flex: 0 0 auto; width: 8px; height: 8px; margin-top: 0.42rem;
  border-radius: 999px; background: var(--c-success, #16a34a);
}
.an-active-main { font-size: 0.85rem; font-weight: 600; color: var(--c-text, #0f172a); }

/* The map. An inline SVG plot, not a tile layer — see anRenderMap for why
   there is no mapping library. */
.an-map-svg { width: 100%; height: auto; border-radius: 10px; display: block; }
.an-map-bg { fill: var(--c-surface-sunken, #f1f5f9); }
.an-map-dot {
  fill: var(--c-brand, #6366f1);
  fill-opacity: 0.55;
  stroke: var(--c-brand, #6366f1);
  stroke-width: 0.6;
}
@media (prefers-reduced-motion: no-preference) {
  .an-map-dot { transition: fill-opacity 0.15s ease; }
}
.an-map-dot:hover { fill-opacity: 0.9; }

/* ── The Coms builder's kind toggle ────────────────────────────────── */
/* ⚠️ NO GAP AND NO BORDERS. These are two radio options, not two cards --
   a bordered box reads as a button, and the pair belongs together as one
   choice, so they sit directly under each other. */
.coms-kind { display: block; margin-bottom: 0.2rem; }
.coms-kind-row { display: flex; align-items: center; gap: 0.4rem; }
.coms-kind-opt {
  display: flex; align-items: center; gap: 0.45rem;
  flex: 1 1 auto; min-width: 0; cursor: pointer; padding: 0.12rem 0;
  /* The sidebar's own `label` rule is uppercase small-caps for field
     labels. These are option names, not field labels. */
  text-transform: none; letter-spacing: 0; margin: 0;
}
.coms-kind-name { font-size: 0.82rem; font-weight: 600; color: var(--c-text, #0f172a); }
.coms-kind-opt input:checked ~ .coms-kind-name { color: var(--c-brand-deep, #4338ca); }

/* The ? sits at the end of the row. Small, quiet, and a real button --
   it is keyboard-reachable and announces its state. */
.coms-kind-help {
  flex: 0 0 auto; width: 1.15rem; height: 1.15rem; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 999px; cursor: pointer;
  font-size: 0.68rem; font-weight: 700; line-height: 1;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface, #ffffff);
  color: var(--c-text-dim, #94a3b8);
}
.coms-kind-help:hover, .coms-kind-help[aria-expanded="true"] {
  border-color: var(--c-brand, #6366f1);
  color: var(--c-brand, #6366f1);
  background: var(--c-surface, #ffffff);
}
/* Revealed by the ?, so it takes its own space only when asked for --
   `hidden` in the markup, never an inline display. */
.coms-kind-note {
  margin: 0.15rem 0 0.35rem 1.55rem;
  font-size: 0.7rem; line-height: 1.4;
  color: var(--c-text-dim, #94a3b8);
}

.coms-kind-badge {
  padding: 0.1rem 0.45rem; border-radius: 999px;
  font-size: 0.66rem; font-weight: 700; letter-spacing: 0.03em; text-transform: uppercase;
  background: color-mix(in srgb, var(--c-brand, #6366f1) 14%, var(--c-surface, #ffffff));
  color: var(--c-brand-deep, #4338ca);
}
.coms-kind-badge-static {
  background: color-mix(in srgb, var(--c-violet, #8b5cf6) 14%, var(--c-surface, #ffffff));
  color: var(--c-violet-text, #6d28d9);
}

/* A static page's own body. It carries .coms-body too, so the layout
   blocks (::: callout and friends) look the same wherever they are
   written — one renderer, one stylesheet. */
/* ⚠⚠ FULL WIDTH, AND IT USED TO BE 900px. A page with the site header
   and footer was locked to a centred 900px column, so a full-bleed hero
   or a wide grid was impossible without switching to `bare` and losing
   the header -- one flag decided both, and nobody wanted that trade.

   ⚠️⚠️ BUT THE MEASURE IS THE DEFAULT, AND BLEEDING IS OPT-IN. The main
   carries .coms-body, whose whole block vocabulary -- callouts, cards,
   stats, figures, lead blocks, column grids -- was written for a 66ch
   parent and sets no width of its own. Uncapping the main without
   restoring a measure on the children does not give those blocks a
   choice; it stretches every one of them to the window, on pages that
   already exist, which is not a wider page but a broken one.

   So every direct child keeps the reading measure, and a child says it
   is a BAND by being a <section>/<header>/<footer> or by carrying
   .page-bleed. A section at the top level of a page is a band -- that is
   what the tag means here -- and a <div> is content. Both existing
   shapes then land correctly with nothing rewritten: a page built from
   sections bleeds, a page built from editor blocks stays readable.

   ⚠️ AND THE PADDING IS ON THE CHILDREN, not the main: padding on the
   main insets a band by 1.5rem and it stops being a band.

   ⚠️ overflow-x: clip, NOT hidden. Pages written against the old 900px
   column cancelled its padding with negative side margins, which now
   reach past the viewport and would scroll the whole page sideways.
   `clip` contains that without creating a scroll container, so
   position: sticky inside a band still sticks to the viewport; `hidden`
   on one axis forces the other to `auto` and would break it. */
/* ⚠️ THE WIDE MEASURE, IN ONE PLACE. 1100px was chosen when it was
   the first thing wider than the old 900px column, and on a 1920px
   monitor it leaves 410px of empty gutter down each side -- which reads
   as the page sitting in a narrow strip rather than as a wide page.
   
   min() rather than a flat number: 1500px stops a very large monitor
   stretching a two-column split past the point where the eye can track
   from the end of one line to the start of the next, and 94vw keeps a
   real margin at every width below that instead of the content touching
   the window edge. Change this one value to move every wrap, the hero
   and the column helpers together. */
.site-page { --page-measure: min(94vw, 1500px); }
.site-page-landing .site-page {
  /* ⚠️ NO BOTTOM PADDING. The 3rem here was the last gap between a
     page and its footer, and it is the same mistake the footer's own
     margin-top was: a strip of PAGE background below the final section.
     It is invisible while a page ends in body copy on the page colour,
     and obvious the moment one ends in a full-bleed band -- the band
     stops, 3rem of nothing, then the footer's own background starts.
     A section that wants room at the bottom has its own padding; the
     footer opens with 3rem of its own. Neither needed this. */
  max-width: none; margin: 0; padding: 0;
  overflow-x: clip;
}
.site-page-landing .site-page > * {
  max-width: 66ch; margin-left: auto; margin-right: auto;
  padding-left: 1.5rem; padding-right: 1.5rem;
}
/* The bands. */
.site-page-landing .site-page > section,
.site-page-landing .site-page > header,
.site-page-landing .site-page > footer,
.site-page-landing .site-page > .page-bleed {
  max-width: none; margin-left: 0; margin-right: 0;
  padding-left: 0; padding-right: 0;
}
/* Wider measures, still centred. Specificity beats `> *` on its own, so
   these hold whatever order the file ends up in. */
.site-page-landing .site-page > .page-wrap,
.site-page-landing .site-page > .page-cols,
.site-page-landing .site-page > .page-cols-2 { max-width: var(--page-measure); }
/* ⚠️ A BUILT FORM IS WIDE CONTENT, NOT A READING COLUMN. The form
   builder emits one `<div class="lxf-wrap">` at the top level of the
   page, so the 66ch measure above caught it -- and .lxf-* sizes itself
   with CONTAINER QUERIES (34rem / 26rem / 20rem). A form squeezed into
   66ch is not merely narrow: it drops under its own breakpoints and
   renders the phone layout on a desktop, one field per row, for a
   two-hundred-field application. Both Sensorium signup pages were doing
   that.
   ⚠️ BUT IT DOES NOT FOLLOW --page-measure, and it used to share a rule
   with the wraps above. A page got wider; a form did not want to. At
   1500px a text input is a 1400px-long line to read back what you typed,
   and the half-width rows that pair fields sensibly at 1100 become two
   very long boxes. Wide content and a wide FORM are different things.
   ⚠️ AND 820, NOT 1100 EITHER. That number was the fix for a form
   crushed into a 66ch reading column and it over-corrected in the same
   direction, just less far: a form is a column of things to read back
   what you typed, and half-width pairs stop reading as a pair once the
   card is wide enough. 820 keeps two half rows well clear of their 24rem
   container query while giving the card a shape rather than a stripe. */
.site-page-landing .site-page > .lxf-wrap { max-width: 820px; }
.site-page-landing .site-page > .page-wrap-narrow { max-width: 760px; }
/* ⚠️ THE FIRST THING NEEDS ROOM AT THE TOP, which the main's own padding
   used to give it. A band brings its own; a loose heading does not. */
.site-page-landing .site-page > :first-child { margin-top: 2.5rem; }
.site-page-landing .site-page > section:first-child,
.site-page-landing .site-page > header:first-child,
.site-page-landing .site-page > .page-bleed:first-child { margin-top: 0; }
@media (max-width: 700px) {
  .site-page-landing .site-page > * {
    padding-left: 1rem; padding-right: 1rem;
  }
  .site-page-landing .site-page > section,
  .site-page-landing .site-page > header,
  .site-page-landing .site-page > footer,
  .site-page-landing .site-page > .page-bleed {
    padding-left: 0; padding-right: 0;
  }
}

/* ⚠️ AN EVENT PAGE BRINGS ITS OWN LAYOUT, and the landing measure
   above must not reach into it.
   
   The rules above give a generic page a reading measure and let a
   <section>/<header>/<footer> opt out as a full-bleed band. That is right
   for a page somebody wrote in the builder, and wrong for one this
   codebase designed: eventPages.js renders
   
     <main class="site-page ev-page">
       <header class="ev-head">    -- a <header>, so: a BAND
       <div class="ev-layout">     -- a <div>, so: capped to 66ch
   
   and .ev-layout is `grid-template-columns: minmax(0, 1fr) 360px` with a
   3rem gap. Inside 66ch the 360px sidebar and the gap leave the
   description column a couple of hundred pixels wide -- the event body,
   the cover image and the schedule all crushed into a gutter beside a
   sidebar that kept its full width. Meanwhile the title, being a
   <header>, lost its padding and ran to the window edge.
   
   Neither is a page that wants a measure applied to its children. It
   wants a container, which is what it had before it became collateral
   damage of uncapping .site-page.
   
   ⚠️ 1100px, NOT THE 900px IT USED TO BE. The old number was chosen
   when .site-page itself was 900px and there was nothing wider on the
   site; 1100px is now the established wide measure (.page-wrap, .page-cols
   above), and at 900px a 360px sidebar left the description 444px. This
   is the same layout the page has always had, given the room it was
   always short of. */
.site-page-landing .site-page.ev-page {
  max-width: 1100px;
  margin: 0 auto;
  padding: 2.5rem 1.5rem 3rem;
}
/* ⚠️ THREE CLASSES BEATS TWO. `.site-page-landing .site-page > *` is
   two classes, and the band rule that follows it is two classes plus a
   type selector -- both lose to this, whatever order the file ends up in,
   which is the same reasoning the .page-wrap overrides above rely on. */
.site-page-landing .site-page.ev-page > * {
  max-width: none;
  margin-left: 0;
  margin-right: 0;
  padding-left: 0;
  padding-right: 0;
}
/* The container supplies the top padding again, so the header must not
   also take the 2.5rem the loose-first-child rule above would give it. */
.site-page-landing .site-page.ev-page > :first-child { margin-top: 0; }

@media (max-width: 700px) {
  .site-page-landing .site-page.ev-page {
    padding: 1.75rem 1rem 2.5rem;
  }
  /* ⚠️ RE-STATED INSIDE THE QUERY. The landing block's own 700px rule
     puts 1rem of side padding back on every child; without this the event
     page would get it on top of the container's, indenting the layout
     twice on exactly the screens with the least room. */
  .site-page-landing .site-page.ev-page > * {
    padding-left: 0;
    padding-right: 0;
  }
}

/* Containers an author can wrap a section in, and a columns helper.
   ⚠️ NAMED IN THE SYSTEM PROMPT (198/199) -- a utility the page builder
   does not know about is a utility nobody uses. */
.site-page .page-wrap {
  max-width: var(--page-measure); margin-left: auto; margin-right: auto;
  padding-left: 1.5rem; padding-right: 1.5rem;
}
.site-page .page-wrap-narrow {
  max-width: 760px; margin-left: auto; margin-right: auto;
  padding-left: 1.5rem; padding-right: 1.5rem;
}
/* ⚠️ auto-fit WITH A MINIMUM, not a fixed column count: the same markup
   then works on a phone without the author writing a media query, which
   is the thing a generated page most often gets wrong. `min(100%, ...)`
   so a single column never overflows a narrow screen. */
.site-page .page-cols {
  display: grid; gap: 1.5rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
}
.site-page .page-cols > * { min-width: 0; }
.site-page .page-cols-2 {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 24rem), 1fr));
}

/* ── Events: the builder's extra controls ──────────────────────────── */
.ev-online-toggle {
  display: flex !important; align-items: center; gap: 0.45rem;
  margin: 0.8rem 0 0.2rem; font-size: 0.78rem; font-weight: 600;
  color: var(--c-text-muted, #64748b); text-transform: none; letter-spacing: 0;
}
.ev-type-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.4rem; }
.ev-type-row {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.45rem 0; border-bottom: 1px solid var(--c-border, #e2e8f0);
}
.ev-type-row:last-child { border-bottom: none; }
.ev-type-main { flex: 1 1 auto; min-width: 0; }
.ev-type-name { display: block; font-size: 0.82rem; font-weight: 600; color: var(--c-text, #0f172a); }
.ev-type-sub { display: block; font-size: 0.7rem; color: var(--c-text-dim, #94a3b8); }
.ev-type-code {
  font-size: 0.72rem; padding: 0.1rem 0.35rem; border-radius: 4px;
  background: var(--c-surface-sunken, #f1f5f9); color: var(--c-brand-deep, #4338ca);
}
.ev-checkin-result { margin: 0.35rem 0 0; font-size: 0.75rem; min-height: 1em; }
.ev-checkin-result.ok { color: var(--c-success-text, #15803d); }
.ev-checkin-result.warn { color: var(--c-warning-text, #b45309); }

/* ── The public event page ─────────────────────────────────────────── */

/* ⚠️⚠️ IT WAS CAPPED AT 900px AND SAID 1040. `.site-page-landing
   .site-page` is (0,2,0) and `.ev-page` was (0,1,0), so the narrower rule
   won on SPECIFICITY whatever the order -- and this is the one page with a
   sidebar, so the body column was left about 580px on a desktop. Two
   classes now, and it sits later in the file than the rule it has to beat,
   which is the belt to that brace. */
.site-page-landing .ev-page { max-width: 1180px; }

/* The title, full width, FIRST. See eventPages.js's own note: the cover
   image used to sit above it. */
.ev-head { margin-bottom: 1.75rem; }
.ev-page h1 { margin: 0 0 0.6rem; font-size: clamp(1.8rem, 4.2vw, 2.75rem); line-height: 1.12; letter-spacing: -0.015em; }
.ev-summary { margin: 0; font-size: clamp(1.05rem, 1.6vw, 1.2rem); line-height: 1.5; color: var(--c-text-muted, #64748b); max-width: 62ch; }
.ev-cancelled {
  margin: 0 0 1rem; padding: 0.6rem 0.9rem; border-radius: 8px; font-weight: 600;
  border: 1px solid var(--c-danger, #dc2626);
  background: color-mix(in srgb, var(--c-danger, #dc2626) 10%, var(--c-surface, #ffffff));
  color: var(--c-danger-deep, #991b1b);
}

/* ⚠️ minmax(0, …) on the content column, or a long unbroken line in the
   body pushes the whole grid wider than the page and it scrolls sideways. */
.ev-layout { display: grid; grid-template-columns: minmax(0, 1fr) 360px; gap: 3rem; align-items: start; }
.ev-main { min-width: 0; }

/* ⚠️ A COVER IS CROPPED, NEVER LET LOOSE. `height: auto` on a portrait
   photograph at this width is most of a laptop screen before a word of the
   description -- the same shape as the bug the title move fixes, one
   element down. A fixed ratio also means the page does not jump as it
   loads. */
.ev-cover {
  width: 100%; aspect-ratio: 16 / 9; max-height: 440px; object-fit: cover;
  border-radius: 14px; margin: 0 0 1.75rem; display: block;
  background: var(--c-surface-subtle, #f8fafc);
}

/* ⚠️ THE WHOLE ASIDE IS STICKY, not just the ticket box. The facts moved
   in beside it, and a sticky child inside a static parent scrolls its
   sibling away -- the answers to "when" and "where" would leave the screen
   exactly as somebody reached the form. */
.ev-side { position: sticky; top: 1.5rem; display: grid; gap: 1rem; min-width: 0; }
.ev-card {
  padding: 1.25rem; border-radius: 14px;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface, #ffffff);
}

.ev-facts { display: grid; gap: 1rem; }
.ev-fact { display: flex; gap: 0.75rem; align-items: flex-start; }
.ev-fact-icon { font-size: 1.1rem; line-height: 1.35; flex: none; }
.ev-fact-body { min-width: 0; display: flex; flex-direction: column; gap: 0.1rem; }
.ev-fact-label {
  font-size: 0.66rem; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--c-text-dim, #94a3b8);
}
.ev-fact-value { font-size: 1rem; font-weight: 650; color: var(--c-text, #0f172a); line-height: 1.35; }
.ev-fact-sub { font-size: 0.85rem; color: var(--c-text-muted, #64748b); line-height: 1.4; overflow-wrap: anywhere; }
.ev-fact-link { color: var(--c-brand, #6366f1); text-decoration: none; }
.ev-fact-link:hover { text-decoration: underline; }
/* The ↗ marks the address as a way out to Google Maps. Dimmed so it reads
   as an affordance beside the address rather than as part of it, and
   aria-hidden in the markup -- the accessible name carries the real
   sentence, where an arrow glyph would be announced as "north east
   arrow" in the middle of a street name. */
.ev-fact-map { opacity: 0.6; }

@media (max-width: 980px) {
  /* ⚠️ THE ASIDE GOES FIRST on a narrow screen -- the rule the ticket box
     already had, now covering the facts too. Somebody who followed a link
     to find out when it is, or to buy, must not scroll past the whole
     description to reach either. The title still leads: it is outside this
     grid. */
  .ev-layout { grid-template-columns: 1fr; gap: 1.75rem; }
  .ev-side { order: -1; position: static; }
}
.ev-tickets h2 { margin: 0 0 0.75rem; font-size: 1.1rem; }
.ev-ticket {
  display: flex; align-items: center; gap: 0.6rem; cursor: pointer;
  padding: 0.6rem 0.7rem; margin-bottom: 0.5rem; border-radius: 8px;
  border: 1px solid var(--c-border, #e2e8f0);
}
.ev-ticket:has(input:checked) {
  border-color: var(--c-brand, #6366f1);
  background: color-mix(in srgb, var(--c-brand, #6366f1) 8%, var(--c-surface, #ffffff));
}
.ev-ticket-out { opacity: 0.55; cursor: not-allowed; }
.ev-ticket-main { flex: 1 1 auto; min-width: 0; }
.ev-ticket-name { display: block; font-size: 0.88rem; font-weight: 600; }
.ev-ticket-desc { display: block; font-size: 0.72rem; color: var(--c-text-dim, #94a3b8); }
.ev-ticket-price { font-size: 0.88rem; font-weight: 700; color: var(--c-brand-deep, #4338ca); }
.ev-tickets label { display: block; margin: 0.7rem 0 0.2rem; font-size: 0.72rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.05em; color: var(--c-text-dim, #94a3b8); }
.ev-tickets .ev-ticket { text-transform: none; letter-spacing: 0; }
.ev-tickets input[type="text"], .ev-tickets input[type="email"],
.ev-tickets input[type="tel"], .ev-tickets input[type="number"] {
  width: 100%; padding: 0.5rem 0.6rem; font: inherit;
  border: 1px solid var(--c-border, #e2e8f0); border-radius: 8px;
  background: var(--c-surface, #ffffff); color: var(--c-text, #0f172a);
}
.ev-tickets button[type="submit"] { width: 100%; margin-top: 0.9rem; }
.ev-note { margin: 0.5rem 0 0; font-size: 0.75rem; min-height: 1em; color: var(--c-text-muted, #64748b); }
.ev-note-error { color: var(--c-danger-deep, #991b1b); }
.ev-done { text-align: center; padding: 0.5rem 0; }
.ev-done h3 { margin: 0 0 0.35rem; }
.ev-code-label { margin: 0.9rem 0 0.2rem; font-size: 0.7rem; text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--c-text-dim, #94a3b8); }
/* Big and monospaced: it gets read aloud at a door and typed off a phone. */
.ev-code {
  margin: 0; font-family: monospace; font-size: 1.3rem; font-weight: 700; letter-spacing: 0.08em;
  color: var(--c-brand-deep, #4338ca);
}

/* ── Who pays for a Linxi ──────────────────────────────────────────── */
/* Above the figures, because every limit and every charge on that panel
   is against the OWNER's plan and the OWNER's balance. */
.ws-bill-owner {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.4rem;
  padding: 0.5rem 0.7rem; margin-bottom: 0.75rem; border-radius: 8px;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc);
}
.ws-bill-owner-label {
  font-size: 0.66rem; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--c-text-dim, #94a3b8);
}
.ws-bill-owner-name { font-size: 0.86rem; font-weight: 600; color: var(--c-text, #0f172a); }
.ws-bill-owner-you {
  padding: 0.08rem 0.4rem; border-radius: 999px;
  font-size: 0.66rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
  background: color-mix(in srgb, var(--c-brand, #6366f1) 14%, var(--c-surface, #ffffff));
  color: var(--c-brand-deep, #4338ca);
}

/* ── A Linxi's usage panel ─────────────────────────────────────────── */
.ws-usage-meters { display: grid; gap: 0.9rem; margin: 0.9rem 0 1.25rem; }
.ws-usage-meter-head {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 0.75rem; font-size: 0.82rem; margin-bottom: 0.3rem;
}
.ws-usage-meter-head span:first-child { color: var(--c-text, #0f172a); font-weight: 600; }
.ws-usage-meter-value { color: var(--c-text-muted, #64748b); font-variant-numeric: tabular-nums; }
.ws-usage-track {
  height: 7px; border-radius: 999px; overflow: hidden;
  background: var(--c-surface-sunken, #f1f5f9);
}
/* ⚠️ The tone is a class, never an inline colour, so a meter follows the
   admin's palette and dark mode. Only the width is inline — that is data. */
.ws-usage-fill { height: 100%; border-radius: 999px; background: var(--c-brand, #6366f1); }
.ws-usage-warn { background: var(--c-warning, #f59e0b); }
.ws-usage-over { background: var(--c-danger, #dc2626); }

.ws-usage-stats {
  display: grid; gap: 0.75rem;
  grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr));
}
.ws-usage-stat {
  padding: 0.7rem 0.8rem; border-radius: 10px;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc);
}
.ws-usage-stat strong {
  display: block; font-size: 1.25rem; line-height: 1.15;
  color: var(--c-text, #0f172a); font-variant-numeric: tabular-nums;
}
.ws-usage-stat span {
  display: block; font-size: 0.72rem; color: var(--c-text-muted, #64748b); margin-top: 0.15rem;
}
.ws-usage-stat-sub { color: var(--c-text-dim, #94a3b8) !important; font-size: 0.66rem !important; }

/* REQ-0043 — the Help glyph in the user menu.
   ⚠️ The GLYPH is coloured, not the row. The label beside it is menu text
   and has to keep reading like the rows above it; a purple "Help" would
   read as a different kind of entry. */
.menu-glyph-accent { color: var(--c-violet, #8b5cf6); }

/* REQ-0044 — the event date rows. The Clear button sits beside the field
   rather than under it: a datetime-local has no native way to empty
   itself, so "optional" is only true while it has never been filled in. */
.ev-date-row { display: flex; gap: 0.4rem; align-items: center; }
.ev-date-row input { flex: 1 1 auto; min-width: 0; }
.ev-date-clear { flex: 0 0 auto; padding: 0.35rem 0.6rem; font-size: 0.72rem; }

/* REQ-0044 — the image shelf on a post, page or event. */
.post-gallery { display: grid; gap: 0.45rem; margin-top: 0.4rem; }
.post-gallery-row {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.4rem; border-radius: 8px;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface, #ffffff);
}
.post-gallery-thumb {
  flex: 0 0 auto; width: 42px; height: 42px; object-fit: cover; border-radius: 6px;
  background: var(--c-surface-sunken, #f1f5f9);
}
/* ⚠️ min-width: 0 — without it the <code> URL refuses to shrink and pushes
   the actions off the end of a narrow sidebar. */
.post-gallery-main { flex: 1 1 auto; min-width: 0; }
.post-gallery-url {
  display: block; font-size: 0.66rem; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; color: var(--c-brand-deep, #4338ca);
}
.post-gallery-alt {
  width: 100%; margin-top: 0.2rem; padding: 0.2rem 0.35rem;
  font-size: 0.72rem; border-radius: 5px;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface, #ffffff); color: var(--c-text, #0f172a);
}
.post-gallery-actions { flex: 0 0 auto; display: flex; flex-direction: column; gap: 0.2rem; }
.post-gallery-actions button { padding: 0.15rem 0.4rem; font-size: 0.66rem; }

/* ── Deliverability / Domain Health ─────────────────────────────────
   One stylesheet block for both surfaces -- Account → Email and Linxi
   Settings → Domain Health render through the same functions, so a
   second set of classes would be a second place for a tone to drift.

   ⚠️ EVERY COLOUR IS A TOKEN. The four reputation tones map onto the
   palette's own status families, so an admin recolouring the theme
   recolours these with it and dark mode follows for free. */
.pm-good { color: var(--c-success-text, var(--c-success, #15803d)); }
.pm-warn { color: var(--c-warning-text, var(--c-warning, #a16207)); }
.pm-poor { color: var(--c-orange, var(--c-warning, #c2410c)); }
.pm-bad  { color: var(--c-danger-text, var(--c-danger, #b91c1c)); }
.pm-none { color: var(--c-text-muted, #64748b); }

.pm-domain-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap; margin-top: 0.75rem;
}
.pm-domain { font-size: 1.05rem; font-weight: 700; }
.pm-window { font-size: 0.8rem; color: var(--c-text-muted, #64748b); }

/* auto-fit so it is two across on a phone and four on a desktop with no
   breakpoint per step. */
.pm-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 0.5rem 1.25rem;
  margin: 0.75rem 0;
}
.pm-row {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 0.75rem; padding: 0.45rem 0;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
  min-width: 0;
}
.pm-row-label {
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.05em;
  text-transform: uppercase; color: var(--c-text-muted, #64748b);
}
.pm-row-value { font-weight: 600; display: flex; align-items: baseline; gap: 0.35rem; min-width: 0; }
/* The dot is aria-hidden and decorative -- the LABEL beside it carries
   the meaning, so this never becomes colour-as-the-only-signal. */
.pm-dot { font-size: 0.85em; }

.pm-block { margin-top: 1.1rem; }
.pm-list { list-style: none; margin: 0.35rem 0 0; padding: 0; display: grid; gap: 0.3rem; }
.pm-list li { font-size: 0.9rem; }

.pm-overall {
  display: flex; align-items: center; gap: 0.5rem;
  margin: 0.9rem 0; padding: 0.7rem 0.9rem;
  border-radius: 10px;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc);
}
.pm-strong { font-weight: 700; }

.pm-state {
  margin-top: 0.9rem; padding: 1rem 1.1rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 10px;
  background: var(--c-surface-subtle, #f8fafc);
}
.pm-state p { margin: 0 0 0.6rem; }
/* ⚠️ "Not enough data" is NEUTRAL, never a warning tint. Nothing is
   wrong -- Google simply has not measured anything yet, and an amber box
   would send somebody looking for a fault that does not exist. */
.pm-state-neutral { border-style: dashed; }
.pm-state-error { border-color: var(--c-danger-border, #fecaca); }
.pm-actions { display: flex; gap: 0.6rem; align-items: center; flex-wrap: wrap; margin-top: 0.4rem; }
.pm-connect { display: inline-block; margin: 0.3rem 0 0.7rem; }

.pm-external { font-weight: 600; text-decoration: underline; text-underline-offset: 2px; }

.pm-spark-wrap { margin-top: 1.1rem; }
.pm-spark {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 1.4rem; line-height: 1.1; letter-spacing: 0.08em;
  color: var(--c-brand, #6366f1);
  overflow-x: auto;
}

.pm-picker { display: flex; align-items: center; gap: 0.6rem; margin: 0.75rem 0; flex-wrap: wrap; }
.pm-foot {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
  margin-top: 1.1rem; padding-top: 0.7rem;
  border-top: 1px solid var(--c-border, #e2e8f0);
}
.pm-explain { margin-top: 1.1rem; }
.pm-explain summary { cursor: pointer; font-weight: 600; font-size: 0.9rem; }
.pm-explain-item { margin-top: 0.6rem; }

/* ── Landing page editor ────────────────────────────────────────────
   Sections with their own heading, so a long form reads as four short
   ones. Every colour is a token, so an admin recolouring the theme
   recolours this and dark mode follows. */
.sl-section {
  margin-top: 1.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--c-border, #e2e8f0);
}
.sl-section-title { margin: 0 0 0.35rem; font-size: 0.95rem; }
.sl-section-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
}
.sl-master-toggle { margin-top: 0.5rem; }
.sl-off-note {
  margin: 0.4rem 0 0;
  padding: 0.6rem 0.8rem;
  border-radius: 8px;
  border: 1px dashed var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc);
}

/* Two across on desktop, one on a phone. Paired by what is filled in
   together: a button's label with the link it points at. */
.sl-cta-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 0.6rem 1rem;
  margin-top: 0.6rem;
}
/* ⚠️ min-width: 0, or a long placeholder refuses to shrink and pushes
   the grid wider than the panel. */
.sl-field { min-width: 0; }
.sl-field label {
  display: block; margin-bottom: 0.2rem;
  font-size: 0.7rem; font-weight: 700; letter-spacing: 0.05em;
  text-transform: uppercase; color: var(--c-text-muted, #64748b);
}
.sl-field input { width: 100%; }

.sl-dirty {
  font-size: 0.75rem; font-weight: 700;
  color: var(--c-warning-text, var(--c-warning, #a16207));
}

.sl-actions { position: sticky; bottom: 0; }

.sl-preview-wrap {
  margin-top: 1.25rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 10px;
  overflow: hidden;
}
.sl-preview-bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
  padding: 0.5rem 0.75rem;
  background: var(--c-surface-subtle, #f8fafc);
  border-bottom: 1px solid var(--c-border, #e2e8f0);
}
.sl-preview-bar > div { display: flex; gap: 0.4rem; }
/* Tall enough to judge a hero, and it scrolls inside itself rather than
   making the admin page taller than the screen.

   ⚠️⚠️ ON THE WRAP'S CLASS, NOT ON AN ID. This was #sl-preview-frame, so
   the Header and Footer previews -- same markup, same wrap class, their
   own ids -- got NOTHING and fell back to the browser's default iframe
   box: 300x150, sitting to the left inside a full-width panel.

   That is the second time in this file that copying a panel picked up
   the class-based rules and missed an id-based one; the textarea width
   was the first. Every other rule in this block is already a class, and
   the wrap is what the three previews genuinely share -- so an iframe
   inside one is the honest selector and a fourth preview cannot miss it. */
.sl-preview-wrap iframe {
  display: block; width: 100%; height: 70vh; min-height: 400px;
  border: 0; background: var(--c-surface, #ffffff);
}

/* ── header / footer style picker ─────────────────────────────────────
   One card per row, full width: these are previews of a full-width bar,
   and three of them side by side gave each about 260px -- which is a
   PHONE viewport, so the "desktop" preview showed the mobile layout of
   every template. The thing being chosen between is how the bar uses the
   whole width, so the preview has to have the whole width. */
.sc-tpl { margin-top: 1.1rem; }

.sc-tpl-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}

.sc-tpl-head h4 { margin: 0; font-size: 0.95rem; }

/* Width and appearance sit together on the right -- three children under
   space-between would push the two apart with the heading. */
.sc-tpl-tools { display: flex; align-items: center; gap: 0.9rem; flex-wrap: wrap; }

.sc-tpl-widths { display: flex; gap: 0.35rem; }
.sc-tpl-widths button { padding: 0.25rem 0.7rem; font-size: 0.78rem; }
/* ⚠️ BOTH CLASS NAMES. The width buttons mark themselves `.is-on` and
   the appearance buttons `.active` -- the latter is the name the big
   preview's toggle already uses, and those same buttons are reused here.
   A rule for one only leaves the selected appearance looking unselected. */
.sc-tpl-widths button.is-on,
.sc-tpl-widths button.active {
  background: var(--c-brand, #6366f1);
  color: var(--c-on-accent, #ffffff);
  border-color: var(--c-brand, #6366f1);
}

/* ── the ⚙️ beside an AI button ────────────────────────────────────
   Only ever in the DOM for a super_admin -- adminAISpots.js adds it and
   the server refuses the routes behind it regardless, so a hidden button
   somebody un-hides saves nothing.

   ⚠️ SUBTLE ON PURPOSE. It sits next to the control people actually came
   for, and a second bright button beside "Write it with AI" competes with
   it for the click. */
/* A video parked in the visual editor.

   ⚠️ IT LOOKS LIKE SOMETHING, DELIBERATELY. The iframe is not in this
   document -- it cannot be, the editing frame is not sandboxed -- so
   without a visible stand-in the admin sees a gap where their video is
   and concludes it was eaten, which is exactly what used to happen. It
   is contenteditable="false" in the markup, so the caret cannot get
   inside it; this is only what that looks like. */
.ve-embed-park {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 8rem;
  padding: 1rem;
  border: 2px dashed var(--c-border-strong, #cbd5e1);
  border-radius: 0.6rem;
  background: var(--c-surface-subtle, #f8fafc);
  color: var(--c-text-muted, #64748b);
  font-size: 0.85rem;
  text-align: center;
  user-select: none;
}

/* The cog now sits INSIDE the AI panel's header, alongside retract and
   close, so it matches those rather than the page behind it. */
.ai-spot-cog {
  border: 1px solid transparent;
  background: transparent;
  color: var(--c-text-dim, #94a3b8);
  border-radius: 0.5rem;
  padding: 0.25rem 0.4rem;
  margin-right: 0.15rem;
  font-size: 0.95rem;
  line-height: 1;
  cursor: pointer;
  vertical-align: middle;
  transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
}
.ai-spot-cog:hover {
  border-color: var(--c-border, #e2e8f0);
  background: var(--c-surface-2, #f8fafc);
  color: var(--c-brand, #6366f1);
}

/* A price tag over something with a choice of depth behind it. The dotted
   underline is the same affordance the "?" hints use: there is more here
   if you hover, and nothing is hidden if you do not. */
.coms-ai-tag-tiered {
  text-decoration: underline dotted;
  text-underline-offset: 0.2em;
  cursor: help;
}

/* ── the assistant's settings dialogue ──────────────────────────────
   Head and foot are pinned; only the middle scrolls. The old version
   appended everything onto .modal, so the title scrolled away and Save
   sat below a screen and a half of controls. */
.ai-spot-modal {
  display: flex;
  flex-direction: column;
  width: min(46rem, 100%);
  /* ⚠️⚠️ THIS LINE IS WHY THE DIALOGUE WAS NARROW, AND IT WAS MISSING.
     `.modal` sets `max-width: 400px` and this rule only ever set `width`
     -- so `min(46rem, 100%)` was computed and then clamped to 400px, and
     the seven sections, the fourteen-row textarea and the nine-row
     pricing table have been living in a 400px column on every screen
     since the dialogue was written. Widening it at a breakpoint without
     this does nothing at all. */
  max-width: min(46rem, 100%);
  max-height: min(88vh, 54rem);
  padding: 0;
  overflow: hidden;
}
.ai-spot-modal:focus { outline: none; }

.ai-spot-head {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  padding: 1.1rem 1.25rem 0.9rem;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
  flex: 0 0 auto;
}
.ai-spot-head h3 { margin: 0 0 0.2rem; font-size: 1.05rem; }
.ai-spot-head p { margin: 0; }
.ai-spot-head > div:first-child { min-width: 0; }

.ai-spot-x {
  margin-left: auto;
  border: none;
  background: transparent;
  color: var(--c-text-dim, #94a3b8);
  font-size: 1.05rem;
  line-height: 1;
  padding: 0.35rem 0.45rem;
  border-radius: 0.5rem;
  cursor: pointer;
  flex: 0 0 auto;
}
.ai-spot-x:hover { background: var(--c-surface-2, #f8fafc); color: var(--c-text, #0f172a); }

/* The only part that moves. */
.ai-spot-body {
  flex: 1 1 auto;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 0.25rem 1.25rem 1rem;
}

.ai-spot-sec { padding: 1rem 0 0.35rem; }
.ai-spot-sec + .ai-spot-sec { border-top: 1px solid var(--c-border, #e2e8f0); }
.ai-spot-sec > h4 {
  margin: 0 0 0.15rem;
  font-size: 0.82rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--c-text-dim, #94a3b8);
}
.ai-spot-sec-hint { margin: 0 0 0.6rem; }
.ai-spot-sec label { display: block; margin-top: 0.7rem; font-weight: 600; font-size: 0.9rem; }
.ai-spot-sec label.coms-check { font-weight: 400; display: flex; }
.ai-spot-sec p.muted-small { margin: 0.15rem 0 0.4rem; }
.ai-spot-sec textarea,
.ai-spot-sec select,
.ai-spot-sec input[type="number"] { width: 100%; max-width: 100%; }
.ai-spot-sec textarea { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.82rem; }

.ai-spot-foot {
  flex: 0 0 auto;
  border-top: 1px solid var(--c-border, #e2e8f0);
  padding: 0.8rem 1.25rem;
  background: var(--c-surface, #ffffff);
}
.ai-spot-foot .panel-actions { margin: 0.55rem 0 0; display: flex; gap: 0.5rem; justify-content: flex-end; }
.ai-spot-foot .result { margin: 0.5rem 0 0; }
.ai-spot-active { font-weight: 600; }

/* ⚠️ A PHONE GETS A FULL-HEIGHT SHEET, not a small box inside a scroll.
   The prompt box alone is taller than a handset viewport, so a centred
   dialogue put the controls somewhere you could not reach without
   scrolling the page behind it. */
@media (max-width: 640px) {
  .ai-spot-modal {
    width: 100%;
    max-width: 100%;
    max-height: 100%;
    height: 100dvh;
    border-radius: 0;
  }
  .ai-spot-head { padding: 0.9rem 1rem 0.75rem; }
  .ai-spot-body { padding: 0.25rem 1rem 1rem; }
  .ai-spot-foot { padding: 0.7rem 1rem calc(0.7rem + env(safe-area-inset-bottom)); }
  /* Full width, and big enough to hit. */
  .ai-spot-foot .panel-actions { flex-direction: column-reverse; }
  .ai-spot-foot .panel-actions button { width: 100%; padding-block: 0.7rem; }
}
.ai-spot-cog:focus-visible {
  outline: 2px solid var(--c-brand, #6366f1);
  outline-offset: 2px;
}

/* A tier in the depth selector: the radio, then a name and a line under
   it. align-items: flex-start because the second line makes the row tall
   and a centred radio floats in the middle of it. */
.as-tier-row {
  align-items: flex-start;
  gap: 0.6rem;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.6rem;
  margin-top: 0.5rem;
  font-weight: 500;
}
.as-tier-row input { margin-top: 0.2rem; }
.as-tier-row:has(input:checked) {
  border-color: var(--c-brand, #6366f1);
  background: var(--c-brand-tint, #f5f3ff);
}
/* ⚠️ A TIER THEY CANNOT AFFORD READS AS UNAVAILABLE, not as broken. It is
   disabled in the markup too -- this is only what that looks like. */
.as-tier-row:has(input:disabled) { opacity: 0.55; cursor: not-allowed; }

/* The heading over the bigger-model group. */
.as-tier-group {
  margin: 1rem 0 0.25rem;
  font-weight: 700;
  letter-spacing: 0.02em;
}

/* ⚠️ A LOCKED TIER IS VISIBLE, NOT HIDDEN -- somebody choosing between
   tiers should be able to see the ceiling even when they cannot buy it.
   Dashed, so it reads as "not yet" rather than "you cannot afford this",
   which is what the plain disabled style above means. */
.as-tier-row.as-tier-locked {
  border-style: dashed;
  background: var(--c-surface-subtle, #f8fafc);
}

/* ⚠️ ADMIN-ONLY COST LINE. Rendered only when the SERVER said this
   session is a super_admin; this just makes it look like an aside rather
   than part of the offer. */
.as-tier-admin {
  margin-top: 0.2rem;
  font-variant-numeric: tabular-nums;
  opacity: 0.8;
}

.sc-tpl-row {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  margin-top: 0.8rem;
}

/* The style strip: which template is on show.

   ⚠️ THE PICKER USED TO STACK ALL THREE, each with a live iframe preview
   -- three rendered pages down the panel, taller than the editor it
   belongs to, with the HTML and CSS boxes pushed off the end of the
   screen. One card shows at a time now and this chooses it. */
.sc-tpl-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 3px;
  margin-top: 0.8rem;
  padding: 3px;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 999px;
  background: var(--c-surface-subtle, #f8fafc);
  width: fit-content;
  max-width: 100%;
}
.sc-tpl-pick {
  border: 0;
  background: none;
  cursor: pointer;
  padding: 0.35rem 0.9rem;
  border-radius: 999px;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--c-text-muted, #64748b);
  white-space: nowrap;
}
.sc-tpl-pick.is-on {
  background: var(--c-surface, #ffffff);
  color: var(--c-text, #0f172a);
  box-shadow: 0 1px 2px color-mix(in srgb, var(--c-shadow, #000000) 10%, transparent);
}
.sc-tpl-pick:hover {
  background: var(--btn-surface-hover, rgba(0, 0, 0, 0.04));
  color: var(--c-text, #0f172a);
  box-shadow: none;
  transform: none;
}
.sc-tpl-pick.is-on:hover { background: var(--c-surface, #ffffff); color: var(--c-text, #0f172a); }
.sc-tpl-pick:focus-visible { outline: 2px solid var(--c-brand, #6366f1); outline-offset: 2px; }

.sc-tpl-card {
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 0.7rem;
  overflow: hidden;
  background: var(--c-surface, #ffffff);
}
/* ⚠️ A CLASS RULE, NOT THE BARE [hidden] ATTRIBUTE. .sc-tpl-card carries
   no display of its own today, so [hidden] would work -- but this file
   already records `display: flex` beating an attribute-driven [hidden]
   more than once, and a card that gains a display later would silently
   start showing all three again. */
.sc-tpl-card[hidden] { display: none; }

/* ⚠️⚠️ THE FRAME IS A REAL 1200px VIEWPORT, SCALED DOWN -- it is never
   sized to the card. An iframe at card width renders whatever layout
   that width triggers, so a "desktop" preview inside an 800px card
   would show the phone bar on any template that breaks at 720.

   The scale cannot be written here: it depends on how wide the panel
   happens to be. sfrTplFit() measures the card and sets --tpl-scale and
   --tpl-h; sfrTplWatch() re-runs it through a ResizeObserver on the row,
   which is what catches the row FIRST getting a width -- clientWidth is
   0 while any ancestor is hidden, and the single measurement that used
   to sit here read zero and never ran again. The fallbacks below are
   what renders in the instant before it runs. */
.sc-tpl-shot {
  position: relative;
  overflow: hidden;
  height: var(--tpl-h, 160px);
  border-bottom: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc);
}

.sc-tpl-shot iframe {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--tpl-w, 1200px);
  height: var(--tpl-vh, 420px);
  border: 0;
  transform: scale(var(--tpl-scale, 0.6));
  transform-origin: top left;
}

/* Phone: a real 390px viewport at 1:1, centred in the card. Scaling a
   phone UP to card width would show a phone nobody owns. */
.sc-tpl-row.is-phone .sc-tpl-shot iframe {
  left: 50%;
  margin-left: calc(var(--tpl-w, 390px) / -2);
}

.sc-tpl-body {
  padding: 0.75rem 0.95rem 0.9rem;
  display: flex;
  align-items: center;
  gap: 0.9rem;
  flex-wrap: wrap;
}
.sc-tpl-body strong { font-size: 0.95rem; }
.sc-tpl-body p { margin: 0; flex: 1 1 16rem; }
.sc-tpl-body button { margin-left: auto; }

@media (max-width: 640px) {
  .sc-tpl-head { align-items: flex-start; flex-direction: column; gap: 0.5rem; }
  .sc-tpl-body { align-items: flex-start; flex-direction: column; gap: 0.4rem; }
  .sc-tpl-body button { margin-left: 0; }
}

.sl-ai-actions {
  display: flex; align-items: center; gap: 0.75rem;
  flex-wrap: wrap; margin-top: 0.6rem;
}

/* ⚠️ THE ADMIN RULE DELIBERATELY SETS NO WIDTH, so a textarea falls back
   to the browser's default `cols` — about 20 characters — and the HTML
   and CSS boxes rendered as narrow columns in a full-width card. That
   omission is correct where it is: the blanket rule also covers inputs,
   and a width: 100% there would break the colour hex boxes that sit
   beside a swatch. Its own comment says so.

   So this is scoped to the code-editor panels rather than widened
   upstream.

   ⚠️⚠️ AND IT LISTS EVERY ONE OF THEM. It named the landing panel alone,
   so the Header and Footer panels -- added later, with the same markup
   and the same two code boxes -- fell straight back to the 20-character
   default and rendered as narrow columns in a full-width card. Exactly
   the bug the comment above describes, reintroduced by copying the panel
   without the rule that makes it usable.
   Adding a panel here means adding it to this selector. There is a check
   that reads the admin panel ids out of index.html and fails if a panel
   with a code textarea is missing from it.

   ⚠️ box-sizing: border-box, or the 0.7rem horizontal padding from that
   same rule is ADDED to 100% and the box overflows the card it is in. */
[data-admin-panel-id="site-landing"] textarea,
[data-admin-panel-id="site-header"] textarea,
/* ⚠️ BOTH MOUNTS. The Forms panel exists twice -- once on the admin
 * rail and once as the per-Linxi tab -- and they are the same UI, so
 * a rule that covered only one would make the same box readable in
 * one place and a 20-character column in the other. */
#tab-forms textarea,
[data-admin-panel-id="site-forms"] textarea,
[data-admin-panel-id="site-footer"] textarea {
  width: 100%;
  box-sizing: border-box;
  /* Markup and styles are read as code, and proportional type makes
     indentation meaningless. The AI brief above is prose and keeps the
     page font. */
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.82rem;
  line-height: 1.55;
  tab-size: 2;
}
/* The brief is a sentence, not code. */
#sl-ai-brief {
  font-family: inherit;
  font-size: 0.9rem;
}

/* The planning chat, INSIDE the shared AI drawer (#coms-ai-panel) — the
   same one the Coms and Events builders open. Scoped to the panel rather
   than to the landing card, because the panel is where it now lives and a
   second target could turn it on tomorrow. */
.coms-ai-chat {
  max-height: 18rem; overflow-y: auto;
  margin: 0 0 0.7rem;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 10px;
  background: var(--c-surface-subtle, #f8fafc);
  display: grid; gap: 0.6rem;
}
.coms-ai-chat.hidden { display: none; }
.coms-ai-chat-empty { margin: 0; font-size: 0.82rem; color: var(--c-text-muted, #64748b); }
.coms-ai-msg { display: grid; gap: 0.12rem; font-size: 0.86rem; line-height: 1.5; }
.coms-ai-who {
  font-size: 0.65rem; font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--c-text-muted, #64748b);
}
.coms-ai-bot .coms-ai-who { color: var(--c-brand, #6366f1); }
/* ⚠️ pre-wrap, never a <br> built from replace(). The assistant writes
   short paragraphs and the admin's own line breaks are theirs. */
.coms-ai-msg-text { white-space: pre-wrap; }

/* ── Social Channels (admin) and Social Accounts (per Linxi) ────────
   One block for both, because they render through the same functions. */
.soc-card {
  margin-top: 1.25rem;
  padding: 1rem 1.1rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 10px;
  background: var(--c-surface-subtle, #f8fafc);
}
.soc-card-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
}
.soc-card-title { margin: 0 0 0.35rem; font-size: 0.95rem; }
.soc-card-body.hidden { display: none; }

.soc-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: 0.6rem 1rem;
  margin: 0.75rem 0;
}
/* min-width: 0, or a long token refuses to shrink and pushes the grid
   wider than the card. */
.soc-field { min-width: 0; position: relative; }
.soc-field label {
  display: block; margin-bottom: 0.2rem;
  font-size: 0.68rem; font-weight: 700; letter-spacing: 0.05em;
  text-transform: uppercase; color: var(--c-text-muted, #64748b);
}
.soc-field input { width: 100%; box-sizing: border-box; }
.soc-field input[type="password"], .soc-field input[type="text"] { padding-right: 4.2rem; }
.soc-reveal {
  position: absolute; right: 0.3rem; bottom: 0.3rem;
  padding: 0.2rem 0.5rem; font-size: 0.72rem;
}

.soc-statuses { display: flex; gap: 1.25rem; flex-wrap: wrap; margin: 0.5rem 0; font-size: 0.85rem; }
.soc-status { display: inline-flex; align-items: center; gap: 0.35rem; }
/* The dot is decoration — the label beside it carries the meaning, so
   colour is never the only signal. */
.soc-dot { font-size: 0.85em; }

.soc-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.6rem; }
.soc-test { margin-top: 1rem; padding-top: 0.8rem; border-top: 1px solid var(--c-border, #e2e8f0); }

.soc-copy-row {
  display: flex; align-items: center; gap: 0.6rem;
  flex-wrap: wrap; margin: 0.5rem 0;
}
.soc-copy-label {
  font-size: 0.68rem; font-weight: 700; letter-spacing: 0.05em;
  text-transform: uppercase; color: var(--c-text-muted, #64748b);
  flex: none; min-width: 7rem;
}
/* A URL and a token are both long and both need to be readable in full —
   they scroll inside their own box rather than widening the panel. */
.soc-copy-value {
  flex: 1 1 12rem; min-width: 0; overflow-x: auto; white-space: nowrap;
  padding: 0.3rem 0.5rem; border-radius: 6px;
  background: var(--c-surface, #ffffff);
  border: 1px solid var(--c-border, #e2e8f0);
  font-size: 0.8rem;
}
.soc-copy-btn { flex: none; }

.soc-steps { margin-top: 0.9rem; }
.soc-steps summary { cursor: pointer; font-weight: 600; font-size: 0.88rem; }
.soc-steps ol { margin: 0.6rem 0 0; padding-left: 1.2rem; font-size: 0.85rem; line-height: 1.6; }
.soc-steps li { margin-bottom: 0.4rem; }

.soc-account {
  display: flex; align-items: center; gap: 0.8rem;
  padding: 0.7rem 0; flex-wrap: wrap;
}
.soc-avatar { width: 2.5rem; height: 2.5rem; border-radius: 50%; flex: none; object-fit: cover; }
.soc-account-main { flex: 1 1 auto; min-width: 0; }
.soc-account-name { display: flex; align-items: center; gap: 0.3rem; }

@media (max-width: 640px) {
  .soc-actions button { flex: 1 1 auto; }
  .soc-copy-label { min-width: 0; flex-basis: 100%; }
}


/* ── WhatsApp template card (campaign composer) ─────────────────────────
   The approved wording, shown but not editable: Meta holds it, and a box
   somebody can type into would discard whatever they wrote. Rendered as a
   quoted block rather than a disabled textarea for the reason the A2P
   status field records -- a greyed-out input still reads as something you
   could enable. */
.whatsapp-template-preview {
  margin: 0.75rem 0;
  padding: 0.75rem 0.9rem;
  border-left: 3px solid var(--c-success, #16a34a);
  background: var(--c-surface-subtle, #f8fafc);
  border-radius: 0 6px 6px 0;
  color: var(--c-text, #0f172a);
  white-space: pre-wrap;
  font-size: 0.92rem;
}

.whatsapp-template-vars { display: flex; flex-direction: column; gap: 0.6rem; }

.whatsapp-var-row { display: flex; flex-direction: column; gap: 0.25rem; }

.whatsapp-var-row > span {
  font-size: 0.82rem;
  font-weight: 500;
  color: var(--c-text-muted, #64748b);
}

/* min-width: 0 so a long placeholder cannot push the card wider than its
   column -- the same reason the composer's two desktop rows carry it. */
.whatsapp-var-row > input { width: 100%; min-width: 0; box-sizing: border-box; }


/* ── Contact sheet: Channels ────────────────────────────────────────────
   One row per door. The state chip is the thing being read, so it keeps
   its own colour while the handle beside it stays muted. */
.cs-channels { display: flex; flex-direction: column; gap: 0.5rem; }

.cs-channel-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 8px;
  background: var(--c-surface-subtle, #f8fafc);
}

/* min-width: 0 so a long handle ellipsises instead of pushing the state
   chip off the end of the drawer. */
.cs-channel-main { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; flex: 1 1 auto; }

.cs-channel-name { font-weight: 500; font-size: 0.9rem; }

.cs-channel-handle {
  font-size: 0.82rem;
  color: var(--c-text-muted, #64748b);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── campaign email body editor ────────────────────────────────────────
   public/js/composeEmailEditor.js. The surface is fixed and a direct
   child of <body>: position:fixed is defeated by a transformed ancestor
   and the composer column has several.

   Every colour is a token, so this follows the palette and dark mode.
   ------------------------------------------------------------------- */

#email-body-host { display: block; }

.email-body-notes { display: flex; flex-direction: column; gap: 0.5rem; margin-top: 0.5rem; }

.email-body-note {
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--c-warning-border, var(--c-border));
  border-left-width: 3px;
  border-radius: 6px;
  background: var(--c-warning-tint, var(--c-surface-subtle));
  color: var(--c-text);
  font-size: 0.82rem;
  line-height: 1.45;
}
.email-body-note strong { font-weight: 600; }
.email-body-note span { color: var(--c-text-muted); }

.email-body-note-tags { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-top: 0.45rem; }
.email-body-note-tags code {
  padding: 0.1rem 0.4rem;
  border-radius: 4px;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.76rem;
  /* A tag can be a whole sentence -- the request that prompted this
     carries {{Suggested Category & Tier — e.g. "Category B · Small
     Brand"}}. Let it wrap rather than push the row sideways. */
  overflow-wrap: anywhere;
}

.email-ed {
  position: fixed;
  inset: 0;
  z-index: 1200;
  display: flex;
  flex-direction: column;
  background: var(--c-surface);
}
.email-ed[hidden] { display: none; }
/* Stop the page behind it scrolling under the editor on a trackpad. */
body.email-ed-locked { overflow: hidden; }

.email-ed-head {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
  padding: 0.7rem 1rem;
  border-bottom: 1px solid var(--c-border);
  background: var(--c-surface-subtle);
}
.email-ed-title { display: flex; align-items: center; gap: 0.45rem; }
.email-ed-fmt {
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
  background: var(--c-brand-tint);
  color: var(--c-brand-deep);
  font-size: 0.72rem;
  font-weight: 600;
}
.email-ed-views { display: inline-flex; gap: 0.25rem; margin-left: auto; }
.email-ed-view-btn {
  padding: 0.3rem 0.7rem;
  border: 1px solid var(--c-border);
  border-radius: 6px;
  background: var(--c-surface);
  color: var(--c-text-muted);
  font-size: 0.8rem;
  font-weight: 500;
  cursor: pointer;
}
/* .email-ed-view-btn is a <button>, and button:hover:not(:disabled)
   sets the indigo gradient -- which behind this text is unreadable. The
   plan tiles were fixed for exactly this; restate every property that
   rule sets, not just the border. */
.email-ed-view-btn:hover:not(:disabled) {
  background: var(--c-brand-tint);
  border-color: var(--c-brand);
  color: var(--c-brand-deep);
  box-shadow: none;
  transform: none;
}
.email-ed-view-btn.active {
  background: var(--c-brand);
  border-color: var(--c-brand);
  color: var(--c-on-accent);
}
.email-ed-view-btn.active:hover:not(:disabled) {
  background: var(--c-brand);
  border-color: var(--c-brand);
  color: var(--c-on-accent);
}
.email-ed-actions { display: flex; align-items: center; gap: 0.75rem; }

.email-ed .email-body-notes { padding: 0.6rem 1rem 0; }

.email-ed-body { flex: 1; display: flex; min-height: 0; }
.email-ed-pane { flex: 1 1 0; min-width: 0; min-height: 0; display: flex; }

.email-ed-code { border-right: 1px solid var(--c-border); }
/* The textarea is MOVED in here, so it is styled by position rather
   than by a class the mover has to remember to add and remove. */
.email-ed-code > textarea {
  flex: 1;
  width: 100%;
  border: 0;
  border-radius: 0;
  resize: none;
  padding: 1rem;
  background: var(--c-surface);
  color: var(--c-text);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.82rem;
  line-height: 1.6;
  tab-size: 2;
  white-space: pre;
  overflow: auto;
}
/* A prose body is not code, and a monospace face full of soft-wrapped
   sentences reads worse than the default. */
.email-ed-plain .email-ed-code > textarea {
  font-family: inherit;
  font-size: 0.95rem;
  white-space: pre-wrap;
}
/* ⚠️ AN INSET RING, NOT NO RING. This pane fills its container, so the
   base rule's outer halo would have sat under the toolbar and outside
   the scroll box -- which is presumably why both were killed here. That
   left the editor as the one text field in the app with NO focus
   indicator at all; inset draws it inside the pane where it belongs. */
.email-ed-code > textarea:focus {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--c-brand, #6366f1);
}

.email-ed-preview { background: var(--c-surface-subtle); }
#email-ed-frame {
  flex: 1;
  width: 100%;
  border: 0;
  /* The preview renders somebody else's HTML, which sets its own
     background -- and an email that assumes white would be unreadable
     over a dark pane. This is the mail client's paper, not the app's
     surface, so it is fixed in both modes like --c-qr-ground. */
  background: var(--c-paper, #ffffff);
}

.email-ed[data-ed-view="code"] .email-ed-preview,
.email-ed[data-ed-view="preview"] .email-ed-code { display: none; }
.email-ed[data-ed-view="code"] .email-ed-code { border-right: 0; }

/* Split is two panes side by side and needs the width for it. Under
   900px the view buttons still work -- you read one at a time. */
@media (max-width: 900px) {
  .email-ed[data-ed-view="split"] .email-ed-preview { display: none; }
  .email-ed[data-ed-view="split"] .email-ed-code { border-right: 0; }
  .email-ed-views { margin-left: 0; width: 100%; }
}

/* ── {{Tag}} bindings, inside the email editor ─────────────────────── */

.email-merge-panel {
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--c-border);
  background: var(--c-surface-subtle);
  max-height: 34vh;
  /* A template can carry a lot of placeholders. Scroll the panel rather
     than letting it push the code and preview panes off the screen --
     they are what the rows are describing. */
  overflow-y: auto;
}
.email-merge-panel[hidden] { display: none; }

.email-merge-head { margin-bottom: 0.6rem; font-size: 0.85rem; }

.email-merge-row {
  display: grid;
  grid-template-columns: minmax(120px, 1.4fr) 150px minmax(0, 1.6fr) minmax(0, 1fr);
  gap: 0.5rem;
  align-items: center;
  padding: 0.4rem 0;
  border-top: 1px solid var(--c-border);
  /* min-width:0 on the children below -- a grid item will not shrink
     under its content, and one of these tags is a whole sentence. */
}
.email-merge-row > * { min-width: 0; }

.email-merge-tag {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.76rem;
  overflow-wrap: anywhere;
  color: var(--c-text);
}
.email-merge-row-unbound .email-merge-tag { color: var(--c-warning-text, var(--c-text)); font-weight: 600; }

.email-merge-value { display: flex; gap: 0.4rem; }
.email-merge-value > * { flex: 1 1 0; min-width: 0; }

.email-merge-row .form-input { padding: 0.3rem 0.5rem; font-size: 0.8rem; }

.email-merge-result {
  font-size: 0.78rem;
  color: var(--c-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.cs-channel-state {
  font-size: 0.76rem;
  font-weight: 600;
  padding: 0.15rem 0.5rem;
  border-radius: 999px;
  white-space: nowrap;
}

.cs-channel-ok { background: var(--c-success-tint, #dcfce7); color: var(--c-success-text, #166534); }
.cs-channel-warn { background: var(--c-warning-tint, #fef3c7); color: var(--c-warning-text, #92400e); }
.cs-channel-stop { background: var(--c-danger-tint, #fee2e2); color: var(--c-danger-text, #991b1b); }
/* Neutral, never a filled colour: "not linked" is not a problem, it is
   simply a door that does not exist for this person. */
.cs-channel-none { background: var(--c-surface-subtle, #f1f5f9); color: var(--c-text-muted, #64748b); }

.cs-channel-action { flex: 0 0 auto; font-size: 0.8rem; padding: 0.3rem 0.6rem; }

.cs-channel-note { margin: 0.1rem 0 0; font-size: 0.8rem; }

/* The stored consent wording, quoted. pre-wrap so a sentence written
   with line breaks survives -- it is evidence, and reflowing it changes
   what was recorded. */
.cs-channel-consent {
  margin: 0.1rem 0 0.4rem;
  font-size: 0.8rem;
  font-style: italic;
  white-space: pre-wrap;
  border-left: 2px solid var(--c-border, #e2e8f0);
  padding-left: 0.6rem;
}


/* ── Meta sandbox banner (Admin -> Social Channels) ─────────────────────
   Amber, never red: sandbox being on is a deliberate configuration, not
   a fault, and a red strip on a working install teaches people to
   dismiss the colour. Never green either -- it is a restriction, and
   green would read as "all good, carry on sending". */
.soc-sandbox {
  display: flex;
  gap: 0.75rem;
  align-items: flex-start;
  padding: 0.9rem 1rem;
  margin-bottom: 1.25rem;
  border: 1px solid var(--c-warning, #f59e0b);
  border-left-width: 4px;
  border-radius: 8px;
  background: var(--c-warning-tint, #fef3c7);
  color: var(--c-warning-text, #92400e);
}

.soc-sandbox-icon { font-size: 1.2rem; line-height: 1.4; flex: 0 0 auto; }

/* min-width: 0 so a long recipient id wraps inside the banner instead of
   stretching it past the column. */
.soc-sandbox-text { min-width: 0; }

.soc-sandbox-text strong { display: block; margin-bottom: 0.2rem; }

/* The muted paragraphs inherit the banner's own ink rather than the
   page's grey -- muted grey on an amber ground is the one combination
   here that drops below readable. */
.soc-sandbox-text p { margin: 0.2rem 0 0; font-size: 0.86rem; color: inherit; opacity: 0.9; }

.soc-sandbox-list {
  margin: 0.35rem 0 0;
  padding-left: 1.1rem;
  font-size: 0.86rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  word-break: break-all;
}


/* ── Social channel setup guidance ──────────────────────────────────────
   A native <details>, so the disclosure is the browser's and there is no
   open/closed state in JS to drift from the arrow. */
.soc-setup {
  margin: 0.6rem 0 0.9rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 8px;
  background: var(--c-surface-subtle, #f8fafc);
}

.soc-setup > summary {
  cursor: pointer;
  padding: 0.6rem 0.8rem;
  font-size: 0.88rem;
  font-weight: 500;
}

.soc-setup-body { padding: 0 0.9rem 0.8rem; }

.soc-setup-body ol { margin: 0.4rem 0 0.6rem; padding-left: 1.2rem; font-size: 0.88rem; }
.soc-setup-body ol li { margin-bottom: 0.3rem; }

.soc-setup-link { display: inline-block; margin-bottom: 0.4rem; text-decoration: none; }

/* Connected but not receiving. Amber, not red: the connection is fine
   and the credentials are good -- one switch was not thrown. Red would
   read as "this is broken", which sends somebody reconnecting an account
   that is already connected. */
.soc-warn {
  margin: 0.5rem 0 0;
  padding: 0.5rem 0.7rem;
  border-left: 3px solid var(--c-warning, #f59e0b);
  background: var(--c-warning-tint, #fef3c7);
  color: var(--c-warning-text, #92400e);
  font-size: 0.84rem;
  border-radius: 0 6px 6px 0;
}

/* The per-channel on/off switch, in the card head opposite the title.
   ⚠️ It sits beside a Disconnect button further down the same card and
   means something quite different, so the label reads On/Off rather than
   borrowing any connected/disconnected wording. */
.soc-channel-switch {
  border-bottom: none;
  padding: 0;
  margin: 0;
  flex: 0 0 auto;
  gap: 0.4rem;
}

.soc-switch-label {
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--c-text-muted, #64748b);
}

/* Embedded Signup: bringing your own WhatsApp number.
   Set off from the status line above it, because it is an offer rather
   than a report -- the paragraph says what this Linxi sends on today,
   this says what it could send on instead. */
.soc-es {
  margin-top: 0.9rem;
  padding-top: 0.8rem;
  border-top: 1px solid var(--c-border, #e2e8f0);
}

.soc-es-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-bottom: 0.25rem;
}

/* ⚠️ NEUTRAL, never the success pill. "Coming soon" is a statement about
   time, not an endorsement, and borrowing the green badge that means
   "recommended" elsewhere in this app would read as one -- the same
   mistake the A2P use-case cards were corrected for. Outlined and muted,
   so it reads as a label on a control that is deliberately inert. */
.soc-soon {
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  padding: 0.12rem 0.45rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 999px;
  color: var(--c-text-muted, #64748b);
  background: var(--c-surface-subtle, #f8fafc);
}

.soc-share {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.6rem;
}

/* break-all so a long Page username wraps inside the card instead of
   widening the panel it sits in. */
.soc-share-url {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.84rem;
  padding: 0.15rem 0.4rem;
  border-radius: 4px;
  background: var(--c-surface-subtle, #f1f5f9);
  word-break: break-all;
}

.soc-share-copy { font-size: 0.78rem; padding: 0.25rem 0.55rem; }


/* ── Social setup wizard ────────────────────────────────────────────────
   Inline inside the channel card, not a modal: every step refers to
   something the user is doing on Facebook or on their phone, so they are
   switching windows constantly and a modal would lose their place. */
.soc-wizard {
  margin-top: 0.9rem;
  padding: 0.9rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 10px;
  background: var(--c-surface-subtle, #f8fafc);
}

.soc-wiz-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0.7rem;
}

.soc-wiz-count { font-size: 0.8rem; font-weight: 600; color: var(--c-text-muted, #64748b); }

.soc-wiz-dots { display: flex; gap: 0.3rem; }

/* The dots are the progress cue and the "Step 1 of 3" text is the fact.
   Either one alone is enough, which is what keeps this readable on a
   narrow phone and through a screen reader. */
.soc-wiz-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--c-border, #e2e8f0);
}
.soc-wiz-dot.done { background: var(--c-success, #16a34a); }
.soc-wiz-dot.at { background: var(--c-brand, #6366f1); transform: scale(1.3); }

.soc-wiz-p { margin: 0 0 0.6rem; font-size: 0.9rem; line-height: 1.5; }

.soc-wiz-ticks { list-style: none; margin: 0 0 0.8rem; padding: 0; font-size: 0.88rem; }
.soc-wiz-ticks li { padding-left: 1.4rem; position: relative; margin-bottom: 0.25rem; }
.soc-wiz-ticks li::before {
  content: "✓";
  position: absolute; left: 0;
  color: var(--c-success, #16a34a);
  font-weight: 700;
}

.soc-wiz-steps { margin: 0 0 0.8rem; padding-left: 1.3rem; font-size: 0.88rem; line-height: 1.55; }
.soc-wiz-steps li { margin-bottom: 0.35rem; }

/* Buttons stack full-width under 560px: these are read on a phone while
   the other hand is in the Instagram app, and a row of half-width
   buttons is a row of mis-taps. */
.soc-wizard button,
.soc-wiz-ext {
  display: inline-block;
  margin: 0 0.4rem 0.4rem 0;
  text-decoration: none;
}

.soc-wiz-nav { margin-top: 0.6rem; padding-top: 0.6rem; border-top: 1px solid var(--c-border, #e2e8f0); }

@media (max-width: 560px) {
  .soc-wizard button,
  .soc-wiz-ext { display: block; width: 100%; margin-right: 0; text-align: center; }
}

/* ── Everything connected ───────────────────────────────────────────── */
.soc-allset {
  margin-bottom: 1.25rem;
  padding: 1rem;
  border: 1px solid var(--c-success, #16a34a);
  border-left-width: 4px;
  border-radius: 10px;
  background: var(--c-success-tint, #dcfce7);
  color: var(--c-success-text, #166534);
}

.soc-allset-title { margin: 0 0 0.5rem; font-size: 1rem; }

.soc-allset-list { list-style: none; margin: 0 0 0.6rem; padding: 0; font-size: 0.9rem; }
.soc-allset-list li { margin-bottom: 0.25rem; }

/* Inherit the banner's ink rather than the page's grey — muted grey on a
   green ground is the one pairing here that drops below readable. */
.soc-allset p { color: inherit; opacity: 0.9; font-size: 0.86rem; }

/* ── Security tab (113) ──────────────────────────────────────────────
   Every colour is a token, so this follows the palette and dark mode.
   The three names that were never DEFINED -- --border, --text-muted,
   --bg-subtle -- are deliberately not used here; it is --c-border,
   --c-text-muted and --c-surface-subtle. */
.wssec-score { text-align: center; margin: 0.5rem 0 1rem; }
.wssec-score-value { font-size: 2.6rem; font-weight: 700; line-height: 1; color: var(--c-text, #0f172a); }
.wssec-score-label { font-size: 0.8rem; color: var(--c-text-muted, #64748b); margin-bottom: 0.6rem; }
.wssec-score-bar {
  height: 8px; border-radius: 999px; overflow: hidden;
  background: var(--c-surface-subtle, #f1f5f9); max-width: 420px; margin: 0 auto;
}
.wssec-score-fill { height: 100%; background: var(--c-success, #16a34a); transition: width 0.3s ease; }

.wssec-score-parts { display: flex; flex-wrap: wrap; gap: 0.4rem 1rem; margin: 0.8rem 0; }
.wssec-part { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.84rem; }
.wssec-part.is-todo { color: var(--c-text-muted, #64748b); }
.wssec-part-mark { font-size: 0.8em; }

.wssec-cards {
  display: grid; gap: 0.75rem; margin: 1rem 0;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
.wssec-card {
  border: 1px solid var(--c-border, #e2e8f0); border-radius: 10px;
  padding: 0.7rem 0.8rem; background: var(--c-surface-subtle, #f8fafc);
  /* min-width: 0 so a long detail line cannot push the grid wider than
     the column it was given. */
  min-width: 0;
}
.wssec-card-head { display: flex; align-items: center; gap: 0.4rem; margin-bottom: 0.3rem; }
.wssec-card-glyph { font-size: 1rem; }
.wssec-card-state {
  margin-left: auto; font-size: 0.72rem; font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.02em; color: var(--c-text-muted, #64748b);
}

.wssec-subhead { font-size: 0.95rem; margin: 1.2rem 0 0.4rem; }

.wssec-row { display: flex; justify-content: space-between; gap: 1rem; padding: 0.3rem 0; font-size: 0.88rem; }
.wssec-row-label { color: var(--c-text-muted, #64748b); }
.wssec-row-value { font-weight: 600; }

.wssec-toggle { align-items: flex-start; }
.wssec-toggle-text { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.wssec-toggle-label { font-weight: 500; }

/* A locked feature. ⚠️ Neutral and informative, never the danger red --
   this is a thing you could have, not a thing that is wrong. */
.wssec-locked {
  border: 1px solid var(--c-border, #e2e8f0); border-radius: 10px;
  padding: 0.9rem 1rem; margin: 0.8rem 0;
  background: var(--c-surface-subtle, #f8fafc);
}
.wssec-locked-head { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; flex-wrap: wrap; }
.wssec-lock { font-size: 1rem; }
.wssec-locked p { margin: 0 0 0.5rem; }

.wssec-events { list-style: none; padding: 0; margin: 0.5rem 0 0; }
.wssec-event {
  display: flex; align-items: baseline; gap: 0.5rem; padding: 0.4rem 0;
  border-bottom: 1px solid var(--c-border, #e2e8f0); font-size: 0.86rem;
}
.wssec-event:last-child { border-bottom: none; }
.wssec-event-text { flex: 1 1 auto; min-width: 0; }

/* The audit table scrolls inside itself rather than widening the page --
   the rule every wide block in this app follows. */
.wssec-table { overflow-x: auto; margin-top: 0.6rem; }
.wssec-audit-row {
  display: grid; grid-template-columns: 5.5rem 9rem 11rem 1fr;
  gap: 0.6rem; padding: 0.35rem 0; font-size: 0.83rem;
  border-bottom: 1px solid var(--c-border, #e2e8f0); min-width: 34rem;
}
.wssec-audit-row.is-failure .wssec-audit-what { color: var(--c-danger-text, #991b1b); }
.wssec-audit-who, .wssec-audit-what { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.wssec-session {
  display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 0;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
}
.wssec-session:last-of-type { border-bottom: none; }
.wssec-session-main { flex: 1 1 auto; min-width: 0; }

.wssec-iplist { margin: 0.6rem 0; }
.wssec-ip-row { display: flex; align-items: center; gap: 0.6rem; padding: 0.3rem 0; }
.wssec-ip {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.85rem;
  background: var(--c-surface-subtle, #f1f5f9); padding: 0.15rem 0.4rem; border-radius: 4px;
}
.wssec-ip-add { display: flex; gap: 0.5rem; margin: 0.5rem 0; }
.wssec-ip-add input { flex: 1 1 auto; min-width: 0; }

.wssec-cert {
  display: flex; align-items: center; gap: 0.75rem; padding: 0.45rem 0;
  border-bottom: 1px solid var(--c-border, #e2e8f0); font-size: 0.86rem;
}
.wssec-cert-id { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.8rem; }
.wssec-cert-desc { flex: 1 1 auto; min-width: 0; }

.wssec-field-list { margin: 0.3rem 0 0.8rem; padding-left: 0; list-style: none; font-size: 0.88rem; }
.wssec-field-list li { padding: 0.15rem 0; }
.wssec-member-list { margin: 0.4rem 0 0; padding-left: 1.1rem; font-size: 0.88rem; }

@media (max-width: 700px) {
  .wssec-audit-row { grid-template-columns: 1fr; min-width: 0; gap: 0.15rem; }
  .wssec-session { flex-wrap: wrap; }
}

/* ── AI & Privacy panel (114) ────────────────────────────────────── */
.wssec-score-fill.is-full { background: var(--c-warning, #f59e0b); }

.wssec-ai-features { margin: 0.4rem 0 1rem; }

/* ⚠️ Auto-reply is set apart from the other toggles ON PURPOSE. It is
   the only control on this page that puts words in front of a real
   person with nobody looking, and a row in a list of five reads as one
   preference among equals. Amber only once ARMED -- an unarmed switch
   is not a warning. */
.wssec-ai-autoreply {
  margin: 1rem 0;
  padding: 0.8rem 1rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 10px;
  background: var(--c-surface-subtle, #f8fafc);
}
.wssec-ai-autoreply.is-armed {
  border-color: var(--c-warning, #f59e0b);
  background: var(--c-warning-tint, #fef3c7);
}
.wssec-ai-autoreply .wssec-subhead { margin-top: 0; }

.wssec-ai-steps {
  margin: 0.5rem 0 0.8rem;
  padding-left: 1.2rem;
  font-size: 0.86rem;
  line-height: 1.7;
}

.wssec-model { align-items: flex-start; }
.wssec-ai-models { margin: 0.4rem 0; }

.wssec-ai-log { margin-top: 0.5rem; }
.wssec-ai-row {
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
}
.wssec-ai-row:last-child { border-bottom: none; }
.wssec-ai-row-head {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.wssec-ai-row.is-failure .wssec-ai-row-head strong { color: var(--c-danger-text, #991b1b); }

/* ── Account security panels + platform overview ─────────────────── */
.acct-2fa-state.is-on { margin: 0 0 0.6rem; }
.acct-2fa-row { display: flex; gap: 0.5rem; align-items: center; margin: 0.5rem 0; flex-wrap: wrap; }
.acct-2fa-row input { width: 8rem; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.15em; }
.acct-2fa-qr {
  display: block; margin: 0.6rem 0; border-radius: 8px;
  /* ⚠️ A white ground under the QR WHATEVER THE THEME. An inverted QR
     code does not scan, so this is one of the few colours that must not
     follow the palette -- it is a token holding the same value in both
     modes, the same treatment --c-chrome-* gets, rather than a bare hex
     that the palette sweep would keep flagging. */
  background: var(--c-qr-ground); padding: 8px;
}
.acct-2fa-secret {
  display: inline-block; margin: 0.2rem 0 0.6rem; padding: 0.3rem 0.55rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.85rem;
  background: var(--c-surface-subtle, #f1f5f9); border-radius: 6px; word-break: break-all;
}
.acct-backup-codes {
  display: grid; gap: 0.4rem; margin: 0.7rem 0;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
}
.acct-backup-code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.88rem;
  padding: 0.35rem 0.5rem; text-align: center;
  background: var(--c-surface-subtle, #f1f5f9); border-radius: 6px;
}
.acct-activity-row {
  display: grid; grid-template-columns: 6rem 1fr auto; gap: 0.6rem;
  padding: 0.4rem 0; font-size: 0.86rem;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
}
.acct-activity-row:last-child { border-bottom: none; }
.acct-activity-what { min-width: 0; }

.psec-types { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0.5rem 0; }
.psec-type {
  font-size: 0.82rem; padding: 0.2rem 0.55rem; border-radius: 999px;
  border: 1px solid var(--c-border, #e2e8f0); background: var(--c-surface-subtle, #f8fafc);
}
.psec-event {
  padding: 0.55rem 0; border-bottom: 1px solid var(--c-border, #e2e8f0);
}
.psec-event:last-of-type { border-bottom: none; }
/* Severity tints the left edge only -- a fully coloured row in a list of
   fifteen is noise, and the glyph already carries the level. */
.psec-event.sev-high, .psec-event.sev-critical {
  border-left: 3px solid var(--c-danger, #dc2626); padding-left: 0.6rem;
}
.psec-event.sev-medium { border-left: 3px solid var(--c-warning, #f59e0b); padding-left: 0.6rem; }

@media (max-width: 700px) {
  .acct-activity-row { grid-template-columns: 1fr; gap: 0.1rem; }
}

/* ── The admin PIN gate (116) ─────────────────────────────────────────
   Standalone pages, not the app shell. Deliberately dark in BOTH modes,
   like the header and the E2E module: this is a boundary, and it should
   not look like an ordinary form on an ordinary page. Its text uses
   --c-chrome-text, which is fixed, so a palette change cannot turn the
   words on a lock screen invisible. */
body.admin-gate {
  min-height: 100vh;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
  background: var(--c-inverse-surface, #0f172a);
  color: var(--c-chrome-text, #cbd5e1);
}

.admin-gate-card {
  width: 100%;
  max-width: 22rem;
  text-align: center;
}

.admin-gate-mark { font-size: 2.5rem; line-height: 1; margin-bottom: 0.5rem; }

.admin-gate-card h1 {
  font-size: 1.35rem;
  margin: 0 0 0.5rem;
  color: var(--c-highlight, #ffffff);
}

.admin-gate-lead {
  font-size: 0.9rem;
  line-height: 1.6;
  margin: 0 0 1.25rem;
  opacity: 0.85;
}

.admin-gate-card label {
  display: block;
  text-align: left;
  font-size: 0.82rem;
  font-weight: 600;
  margin: 0 0 0.3rem;
}

.admin-gate-card input {
  width: 100%;
  box-sizing: border-box;
  margin-bottom: 0.8rem;
  padding: 0.6rem 0.7rem;
  font-size: 1.05rem;
  /* Monospace and spaced, so a typed PIN is countable at a glance. */
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: 0.18em;
  border-radius: 8px;
  border: 1px solid var(--c-chrome-border, #334155);
  background: var(--c-chrome-surface, #1e293b);
  color: var(--c-highlight, #ffffff);
}

.admin-gate-card input:focus-visible {
  outline: 2px solid var(--c-brand, #6366f1);
  outline-offset: 1px;
}

.admin-gate-card button { width: 100%; }

/* ⚠️ Hidden until there is something to say, and it takes no space when
   empty -- an always-present empty line above the button reads as a
   rendering fault on a screen this sparse. */
.admin-gate-error {
  display: none;
  margin: 0.8rem 0 0;
  font-size: 0.86rem;
  color: var(--c-danger-tint, #fecaca);
}
.admin-gate-error.is-shown { display: block; }

.admin-gate-note {
  margin: 1rem 0 0;
  font-size: 0.78rem;
  opacity: 0.7;
  line-height: 1.5;
}

.admin-gate-back {
  display: inline-block;
  margin-top: 1.2rem;
  font-size: 0.82rem;
  color: inherit;
  opacity: 0.7;
  text-decoration: none;
}
.admin-gate-back:hover { opacity: 1; text-decoration: underline; }

/* The 🔒 Lock Admin entry, appended to the admin dropdown when the
   session is unlocked. Separated from the tab entries above it by a
   hairline, because it is an action and they are destinations. */
.admin-lock-btn {
  width: 100%;
  text-align: left;
  border-top: 1px solid var(--c-border, #e2e8f0);
  margin-top: 0.25rem;
  padding-top: 0.45rem;
}
@media (max-width: 900px) {
  .email-merge-row { grid-template-columns: 1fr; }
  .email-merge-panel { max-height: 45vh; }
}

/* ── the inline email preview ──────────────────────────────────────── */

.email-preview-inline {
  margin-top: 0.6rem;
  border: 1px solid var(--c-border);
  border-radius: 8px;
  overflow: hidden;
  background: var(--c-surface-subtle);
}
.email-preview-inline[hidden] { display: none; }

.email-preview-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.45rem 0.7rem;
  border-bottom: 1px solid var(--c-border);
  font-size: 0.8rem;
}

#email-preview-frame {
  display: block;
  width: 100%;
  /* Tall enough to read a real email in, short enough that the composer
     below it is still reachable. The editor's split view is where you go
     for the whole thing. */
  height: 60vh;
  min-height: 380px;
  border: 0;
  /* The mail client's paper, fixed in both modes -- an email assumes the
     white background every client gives it, and flipping it dark would
     make the preview lie about what the recipient sees. */
  background: var(--c-paper);
}

@media (max-width: 768px) {
  #email-preview-frame { height: 50vh; min-height: 280px; }
}

/* ── the admin footer preview ──────────────────────────────────────── */

.email-footer-preview { margin-top: 0.5rem; }
.email-footer-frame {
  display: block;
  width: 100%;
  height: 220px;
  border: 1px solid var(--c-border);
  border-radius: 8px;
  /* The mail client's paper, fixed in both modes -- see --c-paper. */
  background: var(--c-paper);
}

/* ── Save / Schedule / Send, and the send buffer ───────────────────── */

.composer-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.25rem;
  flex-wrap: wrap;
  margin-top: 0.75rem;
  padding-top: 0.85rem;
  border-top: 1px solid var(--c-border);
}
.composer-actions[hidden] { display: none; }

.composer-actions-safe { display: flex; gap: 0.5rem; flex-wrap: wrap; }

/* Send sits on its own, pushed away from the two reversible actions.
   The gap IS the separation -- putting all three in one row is what let
   somebody hit Send meaning to hit Save. */
.composer-actions-send { display: flex; margin-left: auto; }
.composer-actions-send > button {
  padding: 0.65rem 1.5rem;
  font-weight: 600;
}

@media (max-width: 640px) {
  .composer-actions { flex-direction: column; align-items: stretch; }
  .composer-actions-send { margin-left: 0; }
  .composer-actions-send > button { width: 100%; }
  /* On a phone the two are stacked, so the separation has to come from
     space rather than from distance across a row. */
  .composer-actions-send { margin-top: 0.5rem; }
}

.send-countdown {
  margin-top: 0.85rem;
  padding: 1rem 1.1rem;
  border: 1px solid var(--c-warning-border);
  border-radius: 10px;
  background: var(--c-warning-tint);
}
.send-countdown[hidden] { display: none; }

.send-countdown-head { font-size: 1.05rem; color: var(--c-text); }
.send-countdown p { margin: 0.3rem 0 0; font-size: 0.85rem; }

.send-countdown-actions {
  display: flex;
  gap: 0.6rem;
  align-items: center;
  margin-top: 0.85rem;
}

/* Stop is the bigger, heavier control. Somebody reaching for this panel
   in a hurry wants to stop the send, not hurry it along. */
.send-countdown-stop {
  padding: 0.6rem 1.4rem;
  font-size: 1rem;
  font-weight: 700;
  border: 0;
  border-radius: 8px;
  background: var(--c-danger);
  color: var(--c-on-accent);
  cursor: pointer;
}
/* button:hover:not(:disabled) paints the indigo gradient over this --
   restate every property it sets, not just one. The plan tiles and the
   editor's view buttons were both fixed for exactly this. */
.send-countdown-stop:hover:not(:disabled) {
  background: var(--c-danger-strong, var(--c-danger));
  color: var(--c-on-accent);
  box-shadow: none;
  transform: none;
}

.send-countdown-now { padding: 0.5rem 1rem; font-size: 0.85rem; }

/* ── Send Test ─────────────────────────────────────────────────────── */

.send-test-panel {
  margin-top: 0.85rem;
  padding: 0.9rem 1rem;
  border: 1px solid var(--c-border);
  border-radius: 10px;
  background: var(--c-surface-subtle);
}
.send-test-panel[hidden] { display: none; }

.send-test-row { display: flex; gap: 0.5rem; align-items: center; }
/* The address is the field being read, so it takes the room; the two
   buttons stay their own width. */
.send-test-row > input { flex: 1 1 auto; min-width: 0; }
.send-test-row > button { flex: 0 0 auto; }
.send-test-close { padding: 0.4rem 0.7rem; }

.send-test-panel p { margin: 0.55rem 0 0; font-size: 0.83rem; line-height: 1.5; }
.send-test-bad { color: var(--c-danger-deep); }

@media (max-width: 560px) {
  .send-test-row { flex-wrap: wrap; }
  .send-test-row > input { flex: 1 1 100%; }
}

/* Save feedback in the email editor header. It is a live region, so it
   must not shift the row when it fills -- hence a fixed max-width and an
   ellipsis rather than wrapping. */
.email-ed-status {
  max-width: 42ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.8rem;
  color: var(--c-text-muted);
}
.email-ed-status.ok { color: var(--c-success-text); }
.email-ed-status.bad { color: var(--c-danger-deep); }

/* ⚠️ `.campaign-rate-row` sets display:flex, and an AUTHOR rule beats the
   UA stylesheet's [hidden]{display:none} at any specificity -- so the
   When radios rendered in full despite carrying the attribute. Every
   element in this file that sets its own display needs this beside it;
   .myai-save-bar, .email-ed and .email-merge-panel each already do. */
.campaign-rate-row[hidden] { display: none; }

/* A contact an active chip already includes: ticked and disabled, because
   unticking cannot remove them. Kept legible rather than greyed to
   nothing -- they ARE in the audience, which is the fact being shown. */
.recipient-manual-bychip { opacity: 0.85; cursor: default; }
.recipient-manual-bychip input[type="checkbox"] { cursor: default; }

/* ── campaign detail rows ──────────────────────────────────────────── */

.campaign-row-channel { margin-right: 0.35rem; }

/* A failure's reason, under the row it belongs to. It was shown nowhere,
   so a recipient read "failed" with no cause anywhere on the screen. */
.campaign-row-error {
  margin-top: 0.2rem;
  font-size: 0.75rem;
  color: var(--c-danger-deep);
  white-space: normal;
}

/* ── the sent-message viewer ───────────────────────────────────────── */

.campaign-message-modal {
  width: min(860px, 94vw);
  max-height: 90vh;
  display: flex;
  flex-direction: column;
}
.campaign-message-modal .panel-head { margin-bottom: 0.4rem; }
#campaign-message-meta { font-size: 0.82rem; word-break: break-word; }

.campaign-message-body {
  flex: 1;
  min-height: 0;
  margin-top: 0.75rem;
  border: 1px solid var(--c-border);
  border-radius: 8px;
  overflow: hidden;
  display: flex;
}
.campaign-message-frame {
  flex: 1;
  width: 100%;
  min-height: 420px;
  border: 0;
  /* The mail client's paper -- see --c-paper. */
  background: var(--c-paper);
}
/* SMS and Linxi are text, and wrap. A <pre> without this scrolls
   sideways on the one channel whose whole content is a paragraph. */
.campaign-message-text {
  flex: 1;
  margin: 0;
  padding: 1rem;
  overflow: auto;
  white-space: pre-wrap;
  word-break: break-word;
  font-family: inherit;
  font-size: 0.9rem;
  line-height: 1.6;
  background: var(--c-surface);
  color: var(--c-text);
}

/* What submitCampaign said, shown where the Send button was rather than
   at the foot of the composer where nobody is looking. */
.send-countdown-said { margin: 0; font-size: 0.9rem; color: var(--c-text); }

/* ── campaign performance ──────────────────────────────────────────── */

.campaign-performance {
  margin: 1rem 0;
  display: grid;
  /* auto-fit + minmax means these lay themselves out: four across on a
     wide screen, one on a phone, with no breakpoint to keep in step. */
  grid-template-columns: repeat(auto-fit, minmax(215px, 1fr));
  gap: 0.85rem;
  align-items: start;
  /* The summary must not push the recipient list off the screen on a
     short viewport -- it scrolls on its own before that happens. */
  max-height: 42vh;
  overflow-y: auto;
}
.campaign-performance[hidden] { display: none; }

.perf-section {
  padding: 0.9rem 1rem;
  border: 1px solid var(--c-border);
  border-radius: 10px;
  background: var(--c-surface-subtle);
  min-width: 0;
}
.perf-section h4 {
  margin: 0 0 0.6rem;
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--c-text-muted);
}

.perf-row { padding: 0.45rem 0; border-top: 1px solid var(--c-border); }
.perf-section .perf-row:first-of-type { border-top: 0; padding-top: 0; }

.perf-label { font-size: 0.85rem; color: var(--c-text-2); }
/* The figure sits right, on its own line's baseline, so a column of them
   reads down the edge rather than needing to be hunted for. */
.perf-value { float: right; font-size: 1rem; font-weight: 700; color: var(--c-text); }
.perf-sub { clear: both; font-size: 0.74rem; line-height: 1.45; padding-top: 0.15rem; }
.perf-note { margin: 0.5rem 0 0; font-size: 0.74rem; }

.perf-bad .perf-value { color: var(--c-danger-deep); }
.perf-warn .perf-value { color: var(--c-warning-text); }

.perf-delta { clear: both; font-size: 0.74rem; font-weight: 600; padding-top: 0.2rem; }
.perf-up { color: var(--c-success-text); }
.perf-down { color: var(--c-danger-deep); }

/* A clicked link: the URL wraps rather than widening the card, and the
   count keeps its place on the right like every other figure. */
.perf-link a {
  display: block;
  clear: both;
  font-size: 0.78rem;
  color: var(--c-brand);
  overflow-wrap: anywhere;
  text-decoration: none;
}
.perf-link a:hover { text-decoration: underline; }


/* Custom Fields -- its OWN section under Lists / Tags / Groups, full width.
   ⚠️ It was a fourth .manage-organize-col, which is min-width:220px, so a
   field's name, column, type and Remove wrapped into four stacked lines.
   A field is a ROW; it only reads as one when it has the width. */
.manage-fields-panel {
  margin: 0.75rem 0;
  padding: 1rem;
  background: var(--c-surface-subtle, #f8fafc);
  border: 1px solid var(--c-border-soft, #e5e7eb);
  border-radius: 8px;
}
.manage-fields-panel.hidden { display: none; }
.manage-fields-panel > h3 { margin: 0 0 0.5rem; font-size: 0.9rem; }

.manage-fields-table { width: 100%; border-collapse: collapse; }
.manage-fields-table td {
  padding: 0.25rem 0.4rem; vertical-align: middle;
  border-bottom: 1px solid var(--c-border-soft, #e5e7eb);
}
.manage-fields-table tr:last-child td { border-bottom: 0; }
/* ⚠️ Named widths so the four cells stay in their lanes down the whole
   list. Without them the label column is sized by the longest value in it
   and the rows step in and out as you read down. `auto` on the last is
   what keeps Remove hard right. */
.manage-fields-table td:nth-child(1) { width: 40%; }
.manage-fields-table td:nth-child(2) { width: 26%; }
.manage-fields-table td:nth-child(3) { width: 9rem; }
.manage-fields-table td:nth-child(4) { width: 1%; white-space: nowrap; text-align: right; }
.manage-fields-table td input,
.manage-fields-table td select { width: 100%; min-width: 0; }

.manage-field-add { display: flex; gap: 0.4rem; margin-top: 0.6rem; flex-wrap: wrap; }
.manage-field-add input { flex: 1 1 14rem; min-width: 0; }
.manage-field-add select { flex: 0 0 auto; }
/* The real column name, shown because an import maps onto THIS and not
   onto the label, and because two Linxis may label one column differently. */
.manage-field-col {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.72rem; color: var(--c-text-muted, #64748b);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.manage-field-note { font-size: 0.72rem; margin-top: 0.4rem; }

@media (max-width: 700px) {
  /* Below this a four-lane row cannot hold, so the label takes the width
     and the rest sit under it -- deliberately, rather than by wrapping. */
  .manage-fields-table td:nth-child(1) { width: 55%; }
  .manage-fields-table td:nth-child(2) { display: none; }
  .manage-fields-table td:nth-child(3) { width: 7rem; }
}

/* The door from a Linxis conversation to the whole contact record. The
   card used to list every custom column as a raw editable row; this
   replaces them, so it sits where they were. */
.profile-record-btn { margin-top: 0.5rem; font-size: 0.78rem; padding: 0.3rem 0.6rem; }

/* Sandboxed HTML preview -- the shared renderer in js/htmlPreview.js.
   The webmail rule above is a descendant selector and still applies to
   its own frame; this is the base every caller gets. */
.html-preview-frame {
  width: 100%; border: 0; display: block; min-height: 6rem;
  background: var(--c-surface, #ffffff);
}
/* An email in a chat thread reads as a DOCUMENT, not a speech bubble.
   ⚠️ The width has to be won on .chat-row: it is max-width:70% and
   .chat-outbound/.chat-inbound push it to an edge, so a 100%-wide bubble
   inside one is still only 70% of the thread and still hard against the
   side. This overrides both, and centres what is inside it. */
.chat-row-html {
  max-width: 100%;
  width: 100%;
  align-self: center;
  align-items: center;
}
/* The subject sits over the middle of the document it belongs to rather
   than at whichever edge the direction would have put it. */
.chat-row-html .chat-email-subject { align-self: center; text-align: center; }

/* The white ground is the EMAIL'S own, and is a literal rather than a
   token on purpose: mail HTML assumes a light background and carries
   hardcoded hex, so a dark bubble behind it gives black text on black. */
.chat-bubble-html {
  width: 100%;
  padding: 0; overflow: hidden; background: #ffffff;
  border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.6rem;
}
.chat-bubble-html .html-preview-frame { border-radius: 0.6rem; }


/* ── Stat blocks (.an-stat*) ──────────────────────────────────────────
   ⚠️ THESE FIVE CLASSES HAD NO CSS AT ALL. adminCredits.js, masterView.js
   and partnerDashboard.js all build .an-stat-grid > .an-stat markup, and
   every one of them rendered as bare stacked text -- the value, the label
   and the hint on three separate lines with nothing to say they belonged
   together. The markup was always right; the rules were never written.
   REQ-0045 reported it on the credits panel; fixing the class fixes all
   three rather than special-casing one.

   Shaped like .stat-card, which is the app's existing idiom for exactly
   this, but left-aligned: these carry a hint line of prose underneath and
   centred prose is harder to read than centred figures. */
.an-stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 0.75rem;
  margin: 0.75rem 0 1.25rem;
}

.an-stat {
  background: var(--c-surface, #ffffff);
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 8px;
  padding: 0.85rem 1rem;
  min-width: 0;
}

/* The figure leads, because it is what somebody came to read. */
.an-stat-value {
  font-size: 1.5rem;
  font-weight: 700;
  line-height: 1.15;
  color: var(--c-text, #0f172a);
  overflow-wrap: anywhere;
}

.an-stat-label {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--c-text-muted, #64748b);
  margin-top: 0.15rem;
}

/* The qualifier -- "5 credit(s) - 2026-08". Smaller and quieter than the
   label, or the three lines read as equals and the figure stops leading. */
.an-stat-hint {
  font-size: 0.72rem;
  color: var(--c-text-faint, #94a3b8);
  margin-top: 0.3rem;
}


/* ── Secure Clearance ─────────────────────────────────────────────────
   The two gate pages. ⚠️ Dark in BOTH modes, on purpose and unchanged:
   every colour here is a --c-chrome-* or status token, never a surface one
   and never the new --c-head-* family, which follows the theme. A boundary
   should not restyle itself because somebody prefers a light app.

   ⚠️ Own class names (.clearance-*) rather than more .admin-gate-* ones.
   body.admin-gate stays for the page ground, but .admin-gate-card carries
   `max-width: 22rem; text-align: center` at the SAME specificity as
   anything added beside it -- so every override was a source-order bet.
   The panel is its own element now and wins nothing by accident. */
body.admin-gate {
  /* A ground with some depth to it, rather than a flat slab. */
  background:
    radial-gradient(60rem 40rem at 50% -10%, color-mix(in srgb, var(--c-brand, #6366f1) 16%, transparent) 0%, transparent 70%),
    var(--c-inverse-surface, #0f172a);
}

.clearance-shell {
  width: 100%;
  max-width: 30rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.clearance-panel {
  position: relative;
  overflow: hidden;
  border: 1px solid var(--c-chrome-border, #334155);
  border-radius: 16px;
  background: var(--c-chrome-surface, #1e293b);
  box-shadow: 0 24px 60px color-mix(in srgb, var(--c-shadow, #000000) 55%, transparent);
}

/* A brand hairline across the top. Decoration, and marked aria-hidden in
   the markup so it is not announced as anything. */
.clearance-rail {
  height: 3px;
  background: linear-gradient(90deg,
    var(--c-brand, #6366f1) 0%, var(--c-brand-alt, #8b5cf6) 55%, var(--c-info, #3b82f6) 100%);
}

.clearance-head {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  padding: 1.15rem 1.25rem;
  border-bottom: 1px solid var(--c-chrome-border, #334155);
}

.clearance-badge {
  flex: 0 0 auto;
  width: 2.75rem;
  height: 2.75rem;
  display: grid;
  place-items: center;
  border-radius: 12px;
  border: 1px solid var(--c-chrome-border, #334155);
  background: color-mix(in srgb, var(--c-brand, #6366f1) 18%, transparent);
}
.clearance-badge-glyph { font-size: 1.35rem; line-height: 1; }

.clearance-head-text { flex: 1 1 auto; min-width: 0; }

.clearance-kicker {
  margin: 0 0 0.15rem;
  font-size: 0.62rem;
  font-weight: 800;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--c-brand-tint, #c7d2fe);
}

.clearance-head h1 {
  margin: 0;
  font-size: 1.15rem;
  line-height: 1.2;
  color: var(--c-highlight, #ffffff);
}

/* ⚠️ A DOT PLUS A WORD. Colour is not the message -- the rule the "SMS
   Available" pill follows -- so the state reads with no colour perception
   at all. The dot is aria-hidden; the word carries it. */
.clearance-state {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.2rem 0.55rem;
  border-radius: 999px;
  border: 1px solid var(--c-chrome-border, #334155);
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
}
.clearance-state[data-state="locked"] { color: var(--c-warning-tint, #fde68a); }
/* Setup is not a locked state -- nothing is configured yet -- so it is
   informational rather than a warning. */
.clearance-state[data-state="setup"] { color: var(--c-info-tint, #bfdbfe); }
.clearance-dot { width: 0.4rem; height: 0.4rem; border-radius: 50%; background: currentColor; }

.clearance-body { padding: 1.25rem 1.25rem 1.35rem; }

.clearance-lead {
  margin: 0 0 1.1rem;
  font-size: 0.88rem;
  line-height: 1.6;
  color: var(--c-chrome-text, #cbd5e1);
}

.clearance-form label {
  display: block;
  margin: 0 0 0.35rem;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--c-chrome-text, #cbd5e1);
}
.clearance-form label + input { margin-bottom: 0.9rem; }

.clearance-form input {
  width: 100%;
  box-sizing: border-box;
  padding: 0.7rem 0.8rem;
  /* Monospace and spaced, so a typed PIN is countable at a glance. */
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 1.05rem;
  letter-spacing: 0.22em;
  border-radius: 10px;
  border: 1px solid var(--c-chrome-border, #334155);
  background: color-mix(in srgb, var(--c-scrim, #0f172a) 55%, transparent);
  color: var(--c-highlight, #ffffff);
}
.clearance-form input:focus-visible {
  outline: none;
  border-color: var(--c-brand, #6366f1);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-brand, #6366f1) 30%, transparent);
}

/* The qualifier beside a label -- guidance, not part of the control's name. */
.clearance-hint {
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  opacity: 0.65;
}

/* ⚠️ A REAL WARNING, NOT DECORATION. The fields are type="password", so a
   capitalised PIN is invisible while it is typed and the only symptom is a
   refusal -- which on the lock screen costs one of five attempts. It takes
   no space when hidden: [hidden] loses to any author display rule, and
   this block sets one. */
.clearance-caps {
  margin: -0.35rem 0 0.8rem;
  font-size: 0.76rem;
  color: var(--c-warning-tint, #fde68a);
}
.clearance-caps[hidden] { display: none; }

.clearance-submit {
  width: 100%;
  padding: 0.7rem 1rem;
  border: 0;
  border-radius: 10px;
  font: inherit;
  font-size: 0.92rem;
  font-weight: 700;
  cursor: pointer;
  color: var(--c-on-accent, #ffffff);
  background: linear-gradient(135deg, var(--c-brand, #6366f1) 0%, var(--c-brand-alt, #8b5cf6) 100%);
}
.clearance-submit:hover:not(:disabled) { filter: brightness(1.08); }
.clearance-submit:disabled { opacity: 0.6; cursor: default; }

/* Hidden until there is something to say, and it takes no space when empty
   -- a permanent blank line above the button reads as a rendering fault on
   a screen this sparse. */
.clearance-error {
  display: none;
  margin: 0.9rem 0 0;
  padding: 0.6rem 0.7rem;
  border-radius: 8px;
  border: 1px solid var(--c-danger, #dc2626);
  background: color-mix(in srgb, var(--c-danger, #dc2626) 16%, transparent);
  font-size: 0.85rem;
  color: var(--c-danger-tint, #fecaca);
}
.clearance-error.is-shown { display: block; }

.clearance-facts {
  margin: 1.1rem 0 0;
  padding: 0.9rem 0 0 1.05rem;
  border-top: 1px solid var(--c-chrome-border, #334155);
  font-size: 0.78rem;
  line-height: 1.6;
  color: var(--c-chrome-text, #cbd5e1);
  opacity: 0.85;
}
.clearance-facts li + li { margin-top: 0.3rem; }

.clearance-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  padding: 0.8rem 1.25rem;
  border-top: 1px solid var(--c-chrome-border, #334155);
  background: color-mix(in srgb, var(--c-scrim, #0f172a) 35%, transparent);
  font-size: 0.74rem;
  color: var(--c-chrome-text, #cbd5e1);
}
.clearance-foot p { margin: 0; opacity: 0.8; }
.clearance-foot a { color: var(--c-brand-tint, #c7d2fe); text-decoration: none; }
.clearance-foot a:hover { text-decoration: underline; }

@media (max-width: 480px) {
  .clearance-head { flex-wrap: wrap; }
  .clearance-state { order: 3; }
}

@media (forced-colors: active) {
  /* The dot and the rail are background fills, which forced-colors drops.
     A border keeps the dot's shape; the word beside it carries the meaning
     either way. */
  .clearance-dot { border: 1px solid currentColor; }
  .clearance-rail { border-bottom: 1px solid currentColor; }
}


/* ── Coms builder sidebar (REQ-0048) ──────────────────────────────────
   Fourteen controls in one column read as one undifferentiated list. The
   headings below break it into the questions it is actually asking:
   what is this, what does it say, how is it filed, what does it show.

   ⚠️ THE HEADINGS SIT INSIDE THE data-coms-only / data-static-only
   WRAPPERS where the fields they name do. Those wrappers are shown and
   hidden by the kind toggle, so a heading placed outside one would
   announce a group that is not there for the other kind. */
.coms-side-group {
  margin: 1.5rem 0 0.15rem;
  padding-top: 0.9rem;
  border-top: 1px solid var(--c-border, #e2e8f0);
  font-size: 0.66rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--c-text-muted, #64748b);
}
/* No rule above the first one -- a divider at the very top of a column
   reads as a cut-off section rather than a separator. */
.coms-builder-side > .coms-side-group:first-of-type,
.coms-builder-side > [data-coms-only] > .coms-side-group:first-child,
.coms-builder-side > [data-static-only] > .coms-side-group:first-child {
  margin-top: 1.1rem;
}

/* At 22rem the address prefix and its input finally fit on one line
   without the input collapsing to a few characters. */
.coms-slug-row { flex-wrap: nowrap; }
.coms-slug-row input { flex: 1 1 auto; min-width: 0; }
.coms-slug-row span { flex: 0 0 auto; }

/* Room for the thumbnail to be a thumbnail rather than a strip. */
.coms-cover-preview img { max-height: 9rem; object-fit: cover; }

.coms-hint { font-size: 0.72rem; color: var(--c-text-faint, #94a3b8); margin: 0.3rem 0 0; }


/* ── Header nav buttons (REQ-0046) ────────────────────────────────────
   ⚠️ SCOPED TO header, AND ADDITIVE. The bare `.nav-tab-btn` rule is left
   exactly as it was, because that class is NOT header-only: the E2E Secure
   launcher wears it on the Linx tab and the Network/Feed toggle wears it on
   the Network page, and 10 of the 38 in index.html are outside the header.
   Repointing the bare rule would restyle those too, which nobody asked for.

   Specificity: `header .nav-tab-btn` is 0,1,1 -- above the bare rule and
   below `.nav-tab-btn.active` (0,2,0), so the ACTIVE state still wins and
   the selected tab keeps its gradient. The hover pair matches at 0,3,1
   against the existing 0,3,0. */
header .nav-tab-btn {
  color: var(--c-head-text, #cbd5e1);
  border-color: var(--c-head-chip, #374151);
}

header .nav-tab-btn:hover:not(:disabled) {
  border-color: var(--c-head-border, #4b5563);
  color: var(--c-head-ink, #ffffff);
  background: var(--c-head-control-hover, #3f3f46);
}

/* ── Header BUTTONS on hover ──────────────────────────────────────────
   ⚠️ `button:hover:not(:disabled)` (0,2,1) paints every button in the
   app with the brand gradient, and it BEATS a `.class:hover` (0,2,0) --
   so the bell, the user-menu trigger, the balance chip, the admin
   trigger and the mobile toggle were already taking that gradient on
   hover whatever their own hover rule said. On the light header that
   left the ink dark on indigo: the username, the caret and the balance
   unreadable the moment the pointer touched them.

   Stated here on purpose, at the generic rule's own specificity and
   later in the file, so the hover is a decision rather than an accident
   of the cascade: the gradient stays, the ink goes white on it. Equal in
   both themes, because the gradient does not change between them.
   `.header-balance-chip.low` / `.empty` (0,2,0) lose to this too, which
   is right -- amber on indigo would not read. */
.bell-btn:hover:not(:disabled),
.user-menu-trigger:hover:not(:disabled),
.header-balance-chip:hover:not(:disabled),
.admin-menu-trigger:hover:not(:disabled),
.mobile-nav-toggle:hover:not(:disabled) {
  background: var(--btn-grad-hover);
  color: var(--c-on-accent, #ffffff);
}


/* ── Themed header logo (REQ-0046) ────────────────────────────────────
   Exactly one is shown. ⚠️ Named for the THEME each appears in: logo.png
   belongs on the LIGHT header, logo-dark.png on the dark one. Naming them
   after their INK is how they end up swapped -- and the artwork has been
   replaced at least once since this shipped, so a note describing the ink
   goes stale while the slot names do not. t-header-theme measures the two
   files and asserts the light slot is the lighter of the pair.

   Default (light) shows .header-logo-light; both dark paths swap them.
   The `:root:not([data-theme="light"])` guard on the media query is the
   same one every other token block uses, so an explicit Light choice
   still wins under a dark OS. */
.header-logo-dark { display: none; }

:root[data-theme="dark"] .header-logo-light { display: none; }
:root[data-theme="dark"] .header-logo-dark { display: block; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .header-logo-light { display: none; }
  :root:not([data-theme="light"]) .header-logo-dark { display: block; }
}

/* ⚠️ Set by siteBrand.js when logo-dark.png does not load. It has to beat
   the dark rules above, hence the extra class rather than an inline style
   -- the panel contract in this app forbids inline display, and an inline
   one would also survive a later upload. */
.no-dark-logo .header-logo-dark { display: none !important; }
.no-dark-logo .header-logo-light { display: block !important; }

/* Separates the two logo uploads (REQ-0046) -- they write different files
   and only one of them regenerates the icon set, so they should not read
   as one control with two drop zones. */
.logo-split { border: 0; border-top: 1px solid var(--c-border, #e2e8f0); margin: 1.5rem 0 1rem; }

/* Dark-theme logo preview. ⚠️ THE WHOLE STRIP SITS ON A DARK GROUND,
   because that is the only place this artwork is ever shown -- it is light
   by definition, so the admin card's own light surface would render it as
   a pale shape on white or as nothing at all. The gradient is the header's
   own, so the preview is the real background rather than an approximation.

   It reuses .logo-preview-row / .logo-preview-item / .logo-preview-img
   from the strip above it, so the two read as the same thing at the same
   sizes -- which is the point. */
.logo-preview-row-dark {
  padding: 0.9rem 1rem;
  border-radius: 10px;
  border: 1px solid var(--c-chrome-border, #334155);
  background: linear-gradient(135deg, #17121f 0%, #241a3d 55%, #1a1329 100%);
}
.logo-preview-row-dark.hidden { display: none; }
.logo-preview-row-dark .logo-preview-label { color: var(--c-chrome-text, #cbd5e1); }

/* The size the header actually renders it at, beside the icon sizes -- a
   mark that survives at 192 and turns to mud at 40 is a real outcome.
   ⚠️ SHARED BY BOTH STRIPS. It was .logo-dark-img-header and the light
   half used the generic .logo-preview-img-lg, so one section previewed
   "in the header" at 40px and the other at 60. It reads the header's own
   token, so the preview follows the header rather than restating it. */
.logo-preview-img-header {
  width: var(--header-logo-size, 40px);
  height: var(--header-logo-size, 40px);
  object-fit: contain;
  display: block;
}

/* Which logo feeds the one favicon set. */
.logo-favicon-source {
  margin: 0.75rem 0 1rem;
  padding: 0.75rem 0.9rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 8px;
}
.logo-favicon-source legend {
  padding: 0 0.35rem;
  font-size: 0.78rem;
  font-weight: 700;
  color: var(--c-text-muted, #64748b);
}
.logo-favicon-source label {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin-right: 1.1rem;
  font-size: 0.86rem;
}
/* ⚠️ The disabled option stays VISIBLE and says why, rather than being
   hidden -- a choice that vanishes is one somebody assumes is not there. */
.logo-favicon-source input:disabled + span,
.logo-favicon-source label:has(input:disabled) { opacity: 0.55; }
.logo-favicon-hint { font-size: 0.74rem; margin: 0.5rem 0 0; }

/* The "these look swapped" advisory on the logo panel. Amber, because it
   is a thing to fix rather than a failure -- both files uploaded fine. */
.logo-swap-note {
  margin: 0.5rem 0 0.75rem;
  padding: 0.6rem 0.75rem;
  border-radius: 8px;
  border: 1px solid var(--c-warning-border, #fde68a);
  background: var(--c-warning-tint, #fffbeb);
  color: var(--c-warning-text, #92400e);
  font-size: 0.82rem;
  line-height: 1.55;
}
.logo-swap-note.hidden { display: none; }

/* The favicon set's own strip, inside the source chooser. Neutral ground:
   these are shown by browsers on chrome we do not control, so previewing
   them on either theme's colour would be a claim about where they appear. */
.logo-preview-row-favicon {
  margin-top: 0.7rem;
  padding-top: 0.7rem;
  border-top: 1px solid var(--c-border, #e2e8f0);
}

/* The platform Anthropic key, on Admin -> Linxi AI -> The account.
   ⚠️ The state line takes the tone of what it says: green only when a key
   is actually present, because "the assistant is off" is the one thing
   this panel must not render reassuringly. */
.lai-key-state {
  font-size: 0.85rem;
  font-weight: 500;
  margin: 0 0 0.5rem;
}
.lai-key-state-on  { color: var(--c-success-text, var(--c-success)); }
.lai-key-state-off { color: var(--c-danger); }
.lai-key-state-env { color: var(--c-text-muted); }
.lai-key-form {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: 0.35rem;
}
.lai-key-form .mnw-input {
  flex: 1 1 22rem;
  min-width: 0;          /* or a long key pushes the buttons off the row */
  max-width: 34rem;
}

/* "optional" beside a subhead. Neutral by design -- it is a statement
   about whether the field is required, not a status, so it must not
   borrow the success or warning pills. */
.lai-optional {
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  vertical-align: middle;
  margin-left: 0.4rem;
  padding: 0.1rem 0.4rem;
  border-radius: 2rem;
  color: var(--c-text-muted);
  background: var(--c-surface-subtle);
  border: 1px solid var(--c-border);
}
.lai-key-note { margin-top: 0.35rem; }

/* ── Anthropic credit, at the top of Admin -> Linxi AI ──────────────
   ⚠️ NO GREEN. This is an estimate against a figure somebody typed, and
   a success colour would claim a confidence the number does not have.
   Neutral when healthy, amber when low, red only when the arithmetic has
   gone negative -- which really does mean the account has very likely
   run dry. */
.lai-credit {
  border: 1px solid var(--c-border);
  border-radius: 0.75rem;
  padding: 0.9rem 1.1rem;
  margin-bottom: 1rem;
  background: var(--c-surface-subtle);
}
.lai-credit-head {
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
}
.lai-credit-head .lai-subhead { margin: 0; }
.lai-credit-refresh { padding: 0.15rem 0.5rem; line-height: 1.2; }
.lai-credit-figure {
  font-size: 1.7rem; font-weight: 700; margin: 0.35rem 0 0.5rem;
  color: var(--c-text);
}
.lai-credit-low  .lai-credit-figure { color: var(--c-warning-text); }
.lai-credit-dry  .lai-credit-figure { color: var(--c-danger); }
.lai-credit-bar {
  height: 8px; border-radius: 4px; overflow: hidden;
  background: color-mix(in srgb, var(--c-text-muted) 22%, transparent);
  margin-bottom: 0.5rem;
}
.lai-credit-fill {
  display: block; height: 100%; border-radius: 4px;
  background: var(--c-brand); transition: width 0.3s ease;
}
.lai-credit-low .lai-credit-fill { background: var(--c-warning); }
.lai-credit-dry .lai-credit-fill { background: var(--c-danger); }
.lai-credit-caveat { margin-top: 0.3rem; }
.lai-credit-actions {
  display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;
  margin-top: 0.7rem;
}
.lai-credit-link { font-size: 0.8rem; }
.lai-credit-log { margin-top: 0.6rem; }
.lai-credit-log summary { cursor: pointer; font-size: 0.8rem; color: var(--c-text-muted); }
.lai-credit-row {
  display: flex; gap: 0.6rem; align-items: baseline;
  font-size: 0.8rem; padding: 0.25rem 0;
  border-bottom: 1px solid var(--c-border);
}
.lai-credit-row-kind {
  font-size: 0.62rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.04em; padding: 0.08rem 0.35rem; border-radius: 2rem;
  background: var(--c-surface); border: 1px solid var(--c-border);
}
.lai-credit-row-amount { font-weight: 600; margin-left: auto; }
.lai-credit-row-del { background: none; border: 0; cursor: pointer; color: var(--c-text-muted); }

/* ── Admin → Billing → Square ───────────────────────────────────────
   ⚠️ SANDBOX IS NOT A SUCCESS STATE. Every payment on a sandbox install
   is fake, so it is amber; only production is neutral-positive. Painting
   sandbox green is the one thing this panel must not say. */
.sq-env {
  display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap;
  border: 1px solid var(--c-border); border-left-width: 4px;
  border-radius: 0.5rem; padding: 0.6rem 0.9rem; margin-bottom: 0.9rem;
}
.sq-env strong { font-size: 1.05rem; }
.sq-env-note { font-size: 0.8rem; color: var(--c-text-muted); }
.sq-env-sandbox { border-left-color: var(--c-warning); }
.sq-env-prod    { border-left-color: var(--c-brand); }

.sq-grid { display: grid; gap: 0.3rem; margin-bottom: 1.1rem; }
.sq-row {
  display: flex; gap: 0.75rem; align-items: baseline;
  font-size: 0.82rem; padding: 0.3rem 0;
  border-bottom: 1px solid var(--c-border);
}
.sq-row-label { color: var(--c-text-muted); min-width: 12rem; }
.sq-row-value { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; word-break: break-all; }

.sq-ok    { color: var(--c-success-text, var(--c-success)); }
.sq-warn  { color: var(--c-warning-text); }
.sq-bad   { color: var(--c-danger); }
.sq-muted { color: var(--c-text-muted); }

.sq-test { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; margin-bottom: 1rem; }

.sq-plans { display: grid; gap: 0.2rem; margin-top: 0.5rem; }
.sq-plan-row {
  display: flex; gap: 0.75rem; align-items: baseline;
  font-size: 0.82rem; padding: 0.3rem 0;
  border-bottom: 1px solid var(--c-border);
}
.sq-plan-name  { min-width: 11rem; font-weight: 500; }
.sq-plan-price { min-width: 5rem; color: var(--c-text-muted); }
.sq-plan-state { margin-left: auto; font-size: 0.78rem; }
.sq-caveat { margin-top: 0.8rem; }

/* --- Name on card (129) --------------------------------------------
 *
 * One block, three surfaces: the checkout modal, Add Funds and the
 * number wizard's step 2. It sits ABOVE the Square iframe on all three,
 * because that iframe is a black box we cannot put a field inside -- and
 * a name asked for underneath the card number reads as an afterthought
 * on the one field somebody has to get exactly right.
 *
 * ⚠️ Two boxes side by side, never one. The server stores the halves
 * separately (legal_name_records), so splitting a single "Name on card"
 * string on whitespace is a guess about a surname -- see
 * src/services/legalName.js.
 *
 * Every colour is a token. `--border`, `--text-muted` and `--bg-subtle`
 * are NOT tokens on this install and never were: 51 declarations used
 * them bare and did nothing at all.
 */
.cardholder-fields { margin-bottom: 0.9rem; }

.cardholder-row {
  display: flex;
  gap: 0.5rem;
}

/* min-width: 0 -- a flex item refuses to shrink below its content, so
 * without it a long placeholder pushes the pair wider than the modal. */
.cardholder-input {
  flex: 1 1 0;
  min-width: 0;
  width: 100%;
  padding: 0.7rem 0.8rem;
  font-size: 0.9rem;
  font-family: inherit;
  line-height: 1.5;
  color: var(--c-text);
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: 0.6rem;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.cardholder-input:focus {
  outline: none;
  border-color: var(--c-brand);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-brand) 15%, transparent);
}

.cardholder-input::placeholder { color: var(--c-text-dim); }

.cardholder-hint {
  margin: 0.4rem 0 0;
  font-size: 0.75rem;
  line-height: 1.45;
  color: var(--c-text-muted);
}

/* Under 480px the two boxes stack: a first-name field roughly nine
 * characters wide invites an abbreviation, on the one form where the
 * value has to match what is printed. */
@media (max-width: 480px) {
  .cardholder-row { flex-direction: column; }
}

/* --- Legal name records (129) ---------------------------------------
 *
 * The list under the Legal Name form on /account/legal-name: what name
 * was given, on which payment, and when.
 */
.legal-name-records { margin-top: 1.25rem; }

.legal-name-record {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem 0.75rem;
  padding: 0.65rem 0;
  border-top: 1px solid var(--c-border);
}

.legal-name-record-name { font-weight: 600; color: var(--c-text); }
.legal-name-record-what { color: var(--c-text-2); font-size: 0.85rem; }
.legal-name-record-when {
  margin-left: auto;
  color: var(--c-text-muted);
  font-size: 0.8rem;
  white-space: nowrap;
}

/* The +10 pill. Neutral-positive, and it says CONFIRMED rather than
 * verified -- nothing checked this name against an issuer, and a green
 * "verified" tick would be the false claim 092 had to rename three
 * columns to undo. */
.legal-name-points {
  display: inline-block;
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--c-success);
  background: color-mix(in srgb, var(--c-success) 14%, transparent);
}

/* The build section's secondary action: swap between editing the page
 * that is there and replacing it. A link, not a button, because it
 * changes what the button above WILL do rather than doing anything
 * itself -- and two buttons of equal weight is two things to choose
 * between when there is only one act. */
.coms-ai-rebuild {
  display: block;
  margin-top: 0.55rem;
  font-size: 0.78rem;
  color: var(--c-text-muted);
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  text-align: left;
}
.coms-ai-rebuild:hover {
  background: none;
  box-shadow: none;
  transform: none;
  color: var(--c-brand);
  text-decoration: underline;
}

/* --- Visual / HTML toggle on the markup boxes ------------------------
 *
 * Three sections carry it: the landing page, the site header and the
 * site footer.
 */
.ve-tabs { display: flex; gap: 0.3rem; margin-bottom: 0.6rem; }
.ve-tabs button { font-size: 0.82rem; padding: 0.35rem 0.75rem; }
/* ⚠️ `.active` is set by the controller from its own `mode`, not by
 * whichever button was clicked last -- so the highlight cannot disagree
 * with which editor is actually showing. */
.ve-tabs button.active {
  background: var(--c-brand-tint);
  border-color: var(--c-brand);
  color: var(--c-brand-deep);
}

.ve-toolbar {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  flex-wrap: wrap;
  padding: 0.45rem 0.55rem;
  border: 1px solid var(--c-border);
  border-bottom: none;
  border-radius: 8px 8px 0 0;
  background: var(--c-surface-subtle);
}
.ve-toolbar button {
  min-width: 2.1rem;
  padding: 0.25rem 0.5rem;
  font-size: 0.85rem;
}
.ve-toolbar-sep {
  width: 1px;
  align-self: stretch;
  margin: 0 0.25rem;
  background: var(--c-border);
}
/* Pushed to the end and allowed to wrap away on a narrow panel: it is a
 * reminder, not a control. */
.ve-toolbar-note { margin-left: auto; }

/* ⚠️ The frame is sized on the WRAP, like .sl-preview-wrap iframe. An
 * id-scoped rule here would cover one of the three and leave the other
 * two at the browser's 300x150 default -- which is exactly what happened
 * to the previews, twice. */
.ve-frame-wrap {
  border: 1px solid var(--c-border);
  border-radius: 0 0 8px 8px;
  overflow: hidden;
  background: var(--c-surface);
}
.ve-frame-wrap iframe {
  display: block;
  width: 100%;
  height: 60vh;
  min-height: 340px;
  border: 0;
  background: var(--c-surface);
}
@media (max-width: 900px) {
  .ve-frame-wrap iframe { height: 50vh; min-height: 260px; }
  .ve-toolbar-note { display: none; }
}

/* ── Admin -> Site -> Forms (130) ─────────────────────────────────── */
/* ⚠️ Two columns: build on the left, what you are about to paste on the
 * right. They collapse under 1100px because the snippet box and the
 * preview are both unreadable in half of a narrow panel. */
.sfm-cols {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 1.5rem;
  align-items: start;
  margin-top: 1rem;
}
@media (max-width: 1100px) { .sfm-cols { grid-template-columns: minmax(0, 1fr); } }

/* ⚠️⚠️ THE PREVIEW COLUMN GROWS WITH THE FORM AND SCROLLS NOTHING.
   It was sticky with a viewport-bounded height, which made the preview a
   box you scrolled inside while the field list scrolled outside it --
   two scrollbars for one form. Josh's call: it grows as the fields on
   the left grow, so the two columns are the same page and the window is
   the only thing that scrolls.
   The iframe cannot size itself from out here -- it is sandboxed to an
   opaque origin -- so the frame REPORTS its height by postMessage and
   adminForms.js applies it. See `previewEditorJs`. */

/* ── the builder's sections ───────────────────────────────────────────
 *
 * ⚠️ THE COLUMN WAS A FLAT RUN OF SIBLINGS. The <h4>s were there, but a
 * heading with no grouping around it reads as one more line in the
 * stream: "Fields" sat immediately against "After they submit" with
 * nothing between them, and a label ran into the input above it. Reported
 * as being hard to decipher, which is exactly what it was.
 *
 * ⚠️ A RULE AND SPACE, NOT A CARD. The field rows inside are already
 * bordered boxes on a tinted ground; putting these in cards too would be
 * boxes inside boxes, and the nesting would read as structure that is not
 * there. */
.sfm-sect { margin: 0 0 1.5rem; }
.sfm-sect:last-of-type { margin-bottom: 0; }
.sfm-sect-title {
  margin: 0 0 0.75rem;
  padding-bottom: 0.4rem;
  border-bottom: 1px solid var(--c-border);
  font-size: 0.95rem;
  font-weight: 700;
}
/* ⚠️ The pair is what needs the spacing, not the elements: a label
 * belongs to the input UNDER it, so the gap goes above the label and not
 * between the two. Without this a label sits equidistant between the
 * field above and the field below, which is what made them run together.
 *
 * ⚠️⚠️ EVERY SELECTOR HERE IS A DIRECT CHILD, AND `.sfm-stack` IS HOW A
 * NESTED BLOCK OPTS BACK IN. A wrapper inside a section -- the policy URL
 * pair, the notify overrides, the alt text under the header image -- put
 * its fields one level deeper, where NONE of these reached: the labels
 * stayed `display: inline` and ran into their own inputs, and the inputs
 * fell back to the browser's ~20-character default with no width rule
 * anywhere (the base `input[type="url"]` rule sets font, colour, border
 * and padding, and no width at all). That is why a 32-character URL could
 * not be read in the box holding it.
 *
 * ⚠️ The `>` is NOT loosened to a descendant selector, and that is the
 * whole reason this is opt-in: `.sfm-sect input` would catch the checkbox
 * inside every `label.radio-option` and set it to `width: 100%`, and the
 * two flex rows (`.sfm-newfield-row`, `.sfm-add`) size their own fields
 * on purpose. A block declares itself a stack of label-and-field pairs;
 * nothing is swept up by being nested somewhere. */
.sfm-sect > label,
.sfm-stack > label {
  display: block;
  margin: 0.85rem 0 0.25rem;
  font-size: 0.86rem;
  font-weight: 600;
}
.sfm-sect > label:first-of-type,
.sfm-stack > label:first-of-type { margin-top: 0; }
/* ⚠️ THIS PAIR IS (0,2,1) AND SO IS THE `:first-of-type` PAIR ABOVE, so
 * source order is what makes a tick box keep its own margin -- it has to
 * stay BELOW. `#sfm-notify-how` opens with three of these, and reversed
 * they would render block and bold with the box floating inside. */
.sfm-sect > label.radio-option,
.sfm-stack > label.radio-option {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  font-weight: 400;
  margin: 0.6rem 0 0;
}
.sfm-sect > input,
.sfm-sect > select,
.sfm-sect > textarea,
.sfm-stack > input,
.sfm-stack > select,
.sfm-stack > textarea { width: 100%; box-sizing: border-box; }
/* An explanatory line belongs to the heading above it, not to the control
 * below -- so it keeps its own space and the control keeps the label's. */
.sfm-sect > p.muted-small,
.sfm-stack > p.muted-small { margin: 0 0 0.6rem; }

.sfm-fields { display: flex; flex-direction: column; gap: 0.6rem; margin: 0 0 0.85rem; }
.sfm-field {
  border: 1px solid var(--c-border);
  border-radius: 8px;
  padding: 0.65rem 0.75rem;
  background: var(--c-surface-subtle);
}
/* ⚠️ A BREAK IS NOT A FIELD AND MUST NOT LOOK LIKE ONE. It is the line
   between two screens, so it reads as a rule across the column with the
   accent on it rather than as one more card in the stack. */
.sfm-field-break {
  border-style: dashed;
  border-color: var(--c-brand);
  background: transparent;
}
.sfm-field-html { background: var(--c-surface); }
.sfm-html-input {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.82rem;
  white-space: pre;
  overflow-wrap: normal;
  overflow-x: auto;
}
.sfm-step-head {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  margin: 0.35rem 0 0;
}
.sfm-step-badge {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--c-brand);
}
.sfm-field-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
}
.sfm-width { display: flex; align-items: center; gap: 0.35rem; }
/* ⚠️ Small and quiet: two of these sit on every question in the list, so
   at the ordinary button size they would be the loudest thing on screen
   in a panel whose subject is the questions. */
.sfm-width button {
  padding: 0.2rem 0.55rem;
  font-size: 0.78rem;
  font-weight: 500;
  border-radius: 6px;
}
/* ⚠️⚠️ A FILL, NOT A TINT. This was `--c-brand-tint` (#f5f3ff) on a
   `--c-surface-subtle` card (#f8fafc) -- two near-identical whites, so
   the selected state was applied and invisible, and the control read as
   a button that does nothing. A two-option toggle has to say which one
   is on at a glance or it is not a toggle.
   ⚠️ `--c-on-fill`, not `--c-on-accent`: --c-brand LIGHTENS in dark
   mode, and this is the token for text on a fill that does. */
.sfm-width button.is-on {
  background: var(--c-brand);
  border-color: var(--c-brand);
  color: var(--c-on-fill);
}
/* Or hover would repaint the selected one back to the unselected grey --
   `.secondary:hover:not(:disabled)` is (0,3,0) and beats (0,2,1). */
.sfm-width button.is-on:hover:not(:disabled) {
  background: var(--c-brand);
  border-color: var(--c-brand);
  color: var(--c-on-fill);
  filter: brightness(1.06);
}
/* The per-row Save. ⚠️ Quiet: it flushes an edit that is already being
   saved, so it must not read as the thing that makes edits stick. */
.sfm-field-save { font-size: 0.78rem; padding: 0.2rem 0.55rem; border-radius: 6px; }
/* ⚠️ `--c-success-text`, NOT `--c-success-deep`. There is no such token
   -- a bare var() naming one that is never declared is invalid at
   computed-value time and does NOTHING, silently, which is how
   --c-danger-text, --border and --text-muted each shipped here. The
   success family is: --c-success, -alt, -border, -text, -tint, -tint-2. */
.sfm-field-save.is-saved {
  background: var(--c-success-tint);
  border-color: var(--c-success-border);
  color: var(--c-success-text);
}
/* A submission, with whatever was attached to it. */
.sfm-sub { padding: 0.4rem 0; border-bottom: 1px solid var(--c-border); }
.sfm-sub:last-child { border-bottom: 0; }
.sfm-sub-img { display: flex; align-items: center; gap: 0.6rem; margin: 0.35rem 0 0.15rem; }
/* ⚠️ A bounded box, not the image's own size: a phone photo is 1600px
   after resizing and would run the panel off the page. */
.sfm-sub-img img {
  width: 96px;
  height: 72px;
  object-fit: cover;
  border-radius: 6px;
  border: 1px solid var(--c-border);
  display: block;
}
.sfm-field-head { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.sfm-field-head code { font-size: 0.75rem; color: var(--c-text-muted); }
.sfm-field-tools { margin-left: auto; display: flex; gap: 0.25rem; }
.sfm-field-tools button { min-width: 2rem; padding: 0.15rem 0.4rem; font-size: 0.8rem; }
/* Reading the submissions.
   ⚠️ A TALL MODAL WITH ITS OWN SCROLL, not a page-length one: the list
   is paged and the detail can be long, and a dialog that grows past the
   viewport takes its own Close button off screen. The head is pinned so
   the way out is always reachable. */
.sfm-subs-modal {
  width: min(46rem, 94vw);
  max-height: min(80vh, 48rem);
  display: flex;
  flex-direction: column;
}
.sfm-subs-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex: 0 0 auto;
}
.sfm-subs-head h3 { margin: 0; }
/* Both views scroll inside the modal rather than the modal growing. */
#sfm-subs-list-view, #wfm-subs-list-view,
#sfm-subs-detail-view, #wfm-subs-detail-view {
  min-height: 0;
  overflow-y: auto;
  flex: 1 1 auto;
}
.sfm-subs-bar {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin: 0.75rem 0 0.5rem;
}
.sfm-subs-bar select { margin: 0; }
.sfm-subs-bar span { margin-left: auto; }
.sfm-subs-rows { display: flex; flex-direction: column; gap: 0.4rem; }
/* ⚠️ A real <button>, so it is keyboard-reachable and announced as a
   control -- a clickable <div> is neither. Restated colours because the
   base `button` rule fills with the brand gradient on hover. */
.sfm-sub-row {
  display: block;
  width: 100%;
  text-align: left;
  font: inherit;
  font-weight: 400;
  padding: 0.5rem 0.65rem;
  border: 1px solid var(--c-border);
  border-radius: 8px;
  background: var(--c-surface);
  color: var(--c-text);
  cursor: pointer;
}
.sfm-sub-row:hover:not(:disabled) {
  background: var(--c-surface-subtle);
  border-color: var(--c-brand);
  color: var(--c-text);
}
.sfm-sub-row-head { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.sfm-sub-tick { font-weight: 700; }
.sfm-sub-tick.is-ok { color: var(--c-success-text); }
.sfm-sub-tick.is-bad { color: var(--c-danger); }
.sfm-sub-reason { color: var(--c-warning-text); font-size: 0.85rem; }
.sfm-sub-tag {
  font-size: 0.75rem;
  padding: 0.05rem 0.4rem;
  border-radius: 999px;
  background: var(--c-surface-subtle);
  border: 1px solid var(--c-border);
  color: var(--c-text-muted);
}
.sfm-subs-pager {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0.75rem 0 0;
  flex: 0 0 auto;
}
.sfm-subs-pager span { margin: 0 auto; }
.sfm-sub-detail-head { display: flex; align-items: baseline; gap: 0.6rem; margin: 0.75rem 0 0; }
.sfm-sub-h { margin: 1rem 0 0.4rem; font-size: 0.95rem; }
.sfm-sub-fact { display: flex; gap: 0.6rem; padding: 0.2rem 0; flex-wrap: wrap; }
.sfm-sub-fact-label { color: var(--c-text-muted); min-width: 7rem; }
/* ⚠️ break-word, or one long unbroken value -- a user agent, a pasted
   URL -- pushes the dialog wider than the screen. */
.sfm-sub-fact-value { overflow-wrap: anywhere; }
.sfm-sub-answer { padding: 0.35rem 0; border-bottom: 1px solid var(--c-border); }
.sfm-sub-answer:last-of-type { border-bottom: 0; }
/* ⚠️ pre-wrap keeps the line breaks somebody typed into a long answer.
   It is still textContent -- never a <br> built from a replace(). */
.sfm-sub-answer-value { white-space: pre-wrap; overflow-wrap: anywhere; margin-top: 0.15rem; }
/* The screen a question was on, in the submission detail. A label rather
   than a heading: `.sfm-sub-h` is the sections of this panel, and a
   screen divider sitting at the same weight would read as another one. */
.sfm-sub-screen {
  margin: 0.9rem 0 0.3rem;
  font-size: 0.72rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--c-text-muted);
  border-top: 1px solid var(--c-border);
  padding-top: 0.5rem;
}
.sfm-sub-screen:first-of-type { border-top: 0; padding-top: 0; }
.sfm-sub-consent { padding: 0.35rem 0; }
.sfm-sub-consent-text {
  font-style: italic;
  color: var(--c-text-muted);
  font-size: 0.85rem;
  margin-top: 0.15rem;
}
/* The count in the forms list, which opens all this. ⚠️ It sits inside a
   muted line, so it reads as part of the sentence rather than as a
   button parked in it -- underlined and brand-coloured, not filled. */
.sfm-count-btn {
  font: inherit;
  padding: 0;
  border: 0;
  background: none;
  color: var(--c-brand);
  text-decoration: underline;
  cursor: pointer;
}
.sfm-count-btn:hover:not(:disabled) {
  background: none;
  color: var(--c-brand-deep);
}

/* Changing which contact field a question writes to. ⚠️ QUIET: it is a
   property of the question, not a warning about it, so it must not read
   as a problem the way .sfm-warn does -- and it sits BETWEEN the header
   and the question's own inputs because it changes what those inputs
   mean. Its own surface so it does not read as one more of them. */
.sfm-bind {
  margin-top: 0.5rem;
  padding: 0.5rem 0.6rem;
  border: 1px dashed var(--c-border);
  border-radius: 6px;
  background: var(--c-surface);
}
.sfm-bind > p { margin: 0 0 0.4rem; }
.sfm-bind > p:last-child { margin: 0.4rem 0 0; }
/* ⚠️ min-width: 0 or a long "Label — column_name" refuses to shrink and
   pushes the row wider than the panel -- the composer rows' rule. */
.sfm-bind-select { width: 100%; min-width: 0; box-sizing: border-box; margin: 0; }
/* Matches the per-row Save beside it: these are row tools, not the
   panel's own buttons, and one of them reading larger than the other
   makes the header look misaligned. */
.sfm-bind-btn { font-size: 0.78rem; padding: 0.2rem 0.55rem; border-radius: 6px; }
.sfm-field-body { display: flex; flex-direction: column; gap: 0.4rem; margin-top: 0.5rem; }
/* The consent sentence is shown so an admin can read what people will be
 * agreeing to. It is deliberately not an input. */
.sfm-consent-text { margin: 0.5rem 0 0; font-style: italic; }
.sfm-add { display: flex; gap: 0.5rem; align-items: center; margin: 0 0 1rem; }
.sfm-add select { flex: 1 1 auto; min-width: 0; }

/* Adding a contact field to the Linxi from inside the builder.
   ⚠️ Its own surface, because it is not an edit to the form: pressing
   Create changes what every contact in that Linxi carries. A row that
   looked like the rest of the field list would read as one more thing
   the autosave will pick up. */
/* The picture at the top of a form, in the builder (172).
   The tile is a fixed-height well rather than a box sized to the image:
   an empty one has to hold its place, or the section jumps every time
   somebody uploads or removes a picture. */
.sfm-headimg { margin: 0 0 0.75rem; }
.sfm-headimg-box {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 120px;
  padding: 0.75rem;
  border: 1px dashed var(--c-border);
  border-radius: 8px;
  background: var(--c-surface-subtle);
  text-align: center;
}
.sfm-headimg-box img { max-width: 100%; height: auto; border-radius: 4px; }
.sfm-headimg-box p { margin: 0; }
.sfm-headimg-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0.6rem 0 0.4rem;
}
.sfm-newfield {
  margin: -0.5rem 0 1rem;
  padding: 0.85rem 1rem;
  border: 1px solid var(--c-border);
  border-radius: 8px;
  background: var(--c-surface-subtle);
}
.sfm-newfield > p { margin: 0 0 0.6rem; }
.sfm-newfield > p:last-child { margin: 0.6rem 0 0; }
.sfm-newfield-row { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
/* ⚠️ min-width: 0 or the name box refuses to shrink below its
   placeholder and pushes the buttons out of the panel. */
.sfm-newfield-row input[type="text"] { flex: 2 1 12rem; min-width: 0; margin: 0; }
.sfm-newfield-row select { flex: 1 1 8rem; min-width: 0; margin: 0; }
.sfm-newfield-row button { flex: 0 0 auto; }
@media (max-width: 700px) {
  .sfm-newfield-row > * { flex: 1 1 100%; }
}

.sfm-arm {
  margin-top: 1.25rem;
  padding: 0.85rem 1rem;
  border: 1px solid var(--c-border);
  border-radius: 8px;
}
/* ⚠️ AMBER, never the danger red. These are things to finish before
 * switching on, not failures -- a half-built form is the ordinary state
 * of one somebody is working on. */
.sfm-warn { color: var(--c-warning-text); }

/* ⚠️ A SITEWIDE FIELD'S BADGE. Every other row in the builder writes a
   CONTACT in this Linxi; a Site Profile or Site Brand row writes the
   SUBMITTER'S own account, which is a different record belonging to a
   different person -- and nothing else on the row says so.

   ⚠️ VIOLET, not the brand accent and not the warning amber. The brand
   accent is what `.sfm-step-badge` already uses for a screen heading, so
   borrowing it would make these read as the same kind of marker; amber
   means "this is a problem", which a correctly-configured sitewide field
   is not. The muted line beside it carries `.sfm-warn` when it really is
   one.

   ⚠️ Both tokens are declared in `:root` AND in the dark block -- a
   token pair has to move together, or this is a pale chip on a dark
   card. */
.sfm-site-badge {
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 0.1rem 0.4rem;
  border-radius: 999px;
  white-space: nowrap;
  color: var(--c-violet-text);
  /* ⚠️ `-tint-2`, NOT `-tint`. The first is #faf9ff in light -- all but
     white, and this chip sits on a #f8fafc card, so it would be applied
     and invisible. Exactly the bug the width toggle's selected state had
     (`--c-brand-tint` on `--c-surface-subtle`), which read as a control
     that did nothing. */
  background: var(--c-violet-tint-2);
}

/* ⚠️ Sized on the WRAP, not the id -- the preview frames were got wrong
 * that way twice. */
/* ⚠️ NO HEIGHT, NO overflow, NO resize. Every one of those puts a
   scrollbar back: a fixed height clips a long form, `overflow: auto`
   draws the bar, and `resize` needs the bar to exist. The box is exactly
   as tall as the frame inside it, and the frame is exactly as tall as
   the form it is rendering. */
.sfm-preview-wrap {
  border: 1px solid var(--c-border);
  border-radius: 8px;
}
/* ⚠️ The inline height set from the frame's own report wins over this;
   it is the floor for the moment before the first message arrives, and
   for a frame whose script never ran. */
.sfm-preview-wrap iframe {
  width: 100%;
  height: 320px;
  border: 0;
  display: block;
  /* ⚠️ An <iframe> draws its own scrollbar when its document overflows.
     It cannot here — the frame is sized to that document — but this is
     what makes it true rather than nearly true. */
  overflow: hidden;
}
#sfm-subs { display: flex; flex-direction: column; gap: 0.2rem; }
/* The modal's buttons sit right, like every other confirm here. */
.sfm-modal-actions { justify-content: flex-end; }

/* ── how the snippet renders, mirrored into the preview frame ─────────
 * ⚠️ THESE RULES ARE FOR THE PREVIEW ONLY. The snippet is pasted on
 * somebody else's site and inherits THEIR stylesheet -- Linxi ships no
 * CSS with it, deliberately, because a form that arrives with opinions
 * about fonts and colours fights the page it lands on. The class names
 * are there so the site owner can style it; these are what the preview
 * uses so the admin sees a form rather than a stack of naked inputs. */
.sfm-preview-wrap iframe { background: var(--c-surface); }


/* Group headings in Admin -> Site -> Navigation. Twenty checkboxes in one
 * column is a list nobody reads; these say where each tab is reached
 * from. First one loses its top margin so the section does not open with
 * a gap. */
.site-nav-group {
  margin: 1.1rem 0 0.4rem;
  font-size: 0.82rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--c-text-muted);
}
.site-nav-group:first-child { margin-top: 0; }

/* ⚠️ Amber, not red: an unwired feature is a gap, not a failure. */
.ap-unwired { color: var(--c-warning-text); }

/* The inverted switches. A different colour from .ap-unwired: "nobody
   has this yet" and "this flag does nothing" are different facts and
   reading them as one is how a grantable feature gets mistaken for a
   dead one. */
.ap-grantable { color: var(--c-brand-deep, #4338ca); font-weight: 600; }


/* ── Admin → Carriers ────────────────────────────────────────────────
   Every colour here is a token. A literal would not follow the palette
   an admin sets in Admin → Branding, and would not flip in dark mode --
   which this stylesheet's own history records as the mistake that made
   whole panels unreadable.

   ⚠️ --c-on-status, not --c-surface, for text on a saturated status
   fill: --c-surface is the PAGE BACKGROUND and inverts, so using it as
   a text colour is navy-on-indigo in dark mode. */
.carrier-toolbar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;
}
.carrier-live-note { color: var(--c-text-muted); font-size: 0.85rem; }

.carrier-stat-grid {
  display: grid; gap: 1rem; margin: 1rem 0;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}
.carrier-stat {
  border: 1px solid var(--c-border); border-radius: 10px;
  padding: 0.9rem 1rem; background: var(--c-surface-subtle);
}
.carrier-stat-label { font-size: 0.78rem; color: var(--c-text-muted); text-transform: uppercase; letter-spacing: 0.04em; }
.carrier-stat-value { font-size: 1.5rem; font-weight: 600; margin-top: 0.25rem; color: var(--c-text); }
.carrier-stat-note { font-size: 0.8rem; color: var(--c-text-muted); margin-top: 0.25rem; }

/* Wide tables scroll inside their own container -- the page body must
   never scroll horizontally. */
.carrier-table-wrap { overflow-x: auto; }
.carrier-table { width: 100%; border-collapse: collapse; font-size: 0.88rem; }
.carrier-table th, .carrier-table td {
  text-align: left; padding: 0.55rem 0.7rem;
  border-bottom: 1px solid var(--c-border); vertical-align: top;
}
.carrier-table th { color: var(--c-text-muted); font-weight: 600; white-space: nowrap; }
.carrier-table td.carrier-mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.8rem; }

.carrier-pill {
  display: inline-block; padding: 0.12rem 0.5rem; border-radius: 999px;
  font-size: 0.75rem; font-weight: 600; white-space: nowrap;
}
.carrier-pill-ok   { background: var(--c-success-tint); color: var(--c-success-text, var(--c-success)); }
.carrier-pill-warn { background: var(--c-warning-tint); color: var(--c-warning-text, var(--c-warning)); }
.carrier-pill-bad  { background: var(--c-danger-tint);  color: var(--c-danger); }
/* ⚠️ Neutral, deliberately. An unknown state must not wear the brand
   colour -- that is what made an empty column read as "in hand" on the
   AI queue panel. */
.carrier-pill-none { background: var(--c-surface-subtle); color: var(--c-text-muted); }

.carrier-drift {
  margin: 0.35rem 0 0; padding: 0; list-style: none;
  font-size: 0.8rem; color: var(--c-warning-text, var(--c-warning));
}
.carrier-notice {
  border: 1px solid var(--c-border); border-left: 3px solid var(--c-warning);
  border-radius: 8px; padding: 0.8rem 1rem; margin: 1rem 0;
  background: var(--c-warning-tint); color: var(--c-text);
}
.carrier-notice-bad { border-left-color: var(--c-danger); background: var(--c-danger-tint); }
.carrier-notice-ok  { border-left-color: var(--c-success); background: var(--c-success-tint); }
.carrier-notice-flat { border-left-color: var(--c-border); background: var(--c-surface-subtle); }

.carrier-field { margin: 0.9rem 0; }
.carrier-field label { display: block; font-weight: 600; margin-bottom: 0.3rem; }
.carrier-field-note { font-size: 0.82rem; color: var(--c-text-muted); margin-top: 0.3rem; }
.carrier-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem; }
.carrier-key-value {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.82rem; word-break: break-all;
  background: var(--c-surface-subtle); border: 1px solid var(--c-border);
  border-radius: 6px; padding: 0.5rem 0.6rem; color: var(--c-text);
}
.carrier-bar { display: flex; height: 8px; border-radius: 4px; overflow: hidden; background: var(--c-surface-subtle); }
.carrier-bar span { display: block; height: 100%; }

@media (max-width: 700px) {
  .carrier-toolbar { flex-direction: column; align-items: stretch; }
}


/* ── The contact record, under the Linxi Label tile ──────────────────
   #contact-profile-card moved out of the middle column and into
   .links-card-panel, directly under #links-card-visual. That column is
   300px, and the record was laid out for the conversation panel's full
   width -- so the parts that assumed room have to give it up here.

   ⚠️ Scoped to .links-card-panel rather than changed outright: the SAME
   ids render inside the Contacts tab's contact sheet, which is
   full-width, and narrowing them there would be a regression in a place
   nobody asked about. */
.links-card-panel .contact-profile-card {
  border-bottom: 0;
  padding-bottom: 0;
  margin-top: 1rem;
  margin-bottom: 0;
}

/* Two columns of inputs inside 300px minus padding is ~130px each --
   narrower than the placeholder text. One column. */
.links-card-panel #profile-avatar.avatar-large {
  width: 36px;
  height: 36px;
  font-size: 0.85rem;
}

/* Full-width buttons rather than a ragged wrap at this measure. */
.links-card-panel .profile-header-actions {
  flex-direction: column;
  align-items: stretch;
  gap: 0.35rem;
}
.links-card-panel .profile-header-actions .row-action-btn-sm { width: 100%; }
.links-card-panel .contact-linxi-badge,
.links-card-panel #profile-opted-out-badge { align-self: flex-start; }


/* ⚠️ WHO YOU ARE TALKING TO, AT THE TOP LEFT. Photo, name,
   organization. It used to sit BETWEEN Previous and Next -- the two
   halves of one control at opposite ends of the row with somebody's name
   wedged between them -- and the buttons are paired on the right now.

   ⚠️ NO LEFT MARGIN: it is the leftmost thing in the row and used to
   need clearance on both sides.

   ⚠️ flex: 1 so it takes the room up to the buttons, min-width: 0 so a
   long organization shrinks the labels instead of pushing the buttons
   off the panel -- flex items refuse to go below their content width
   without it, and all the give in this row is on this side. */
.link-detail-identity {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 0.4rem;
  flex: 1;
  min-width: 0;
  margin: 0 0.75rem 0 0;
}

/* ⚠️ THE TWO HALVES OF ONE CONTROL, TOGETHER. Moving between
   conversations used to mean crossing somebody's name to get from
   Previous to Next.

   ⚠️ flex-shrink: 0 -- the identity is flex: 1 and both its labels
   ellipsise, so without this the buttons would be squeezed before the
   name gave up a single character. */
.link-detail-nav-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-shrink: 0;
}

/* ⚠️ It was .avatar-large (a 56px block) in the record. At the top of a
   nav row it has to sit on the line with the buttons. */
.link-detail-identity #profile-avatar {
  width: 30px;
  height: 30px;
  font-size: 0.75rem;
  flex-shrink: 0;
}

/* The name is TEXT, not a control -- one element holding "First Last",
   so the two sit tight with a single space and there is no inter-input
   gap to close. */
.link-detail-name {
  font-weight: 600;
  color: var(--c-text, #0f172a);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
  flex: 0 1 auto;
}

/* ⚠️ NOTHING IN THIS ROW IS A FORM CONTROL. The photo, the name and the
   organization are all text now, so there is no box to strip and nothing
   for edit mode to reveal -- the .profile-field and .editing rules that
   used to sit here are gone with the inputs they styled. Editing a name
   or an organization lives in Full record and in the Contacts tab's edit
   modal. */

/* ⚠️ flex: 1 IS THE CENTRING. The organization takes the free space
   left over after the photo and the name, and centres its text inside
   it -- so it sits in the middle of the header rather than hard against
   Next. (It was margin-left: auto for a while, which absorbs that same
   space on one side only and pins it to the right edge.)

   ⚠️ AND IT CARRIES .link-detail-name IN THE MARKUP, so weight, colour,
   nowrap and ellipsis all come from the rule the person's name uses.
   Only position and alignment are its own: one definition of what these
   look like, so the two cannot drift apart. */
.link-detail-identity .link-detail-org {
  flex: 1;
  text-align: center;
}

@media (max-width: 768px) {
  /* Three things on one line at this width is a scrum -- the buttons
     keep the ends and the identity takes its own row. */
  /* ⚠️ THE WORDS GO, THE ARROWS STAY. Two labelled buttons plus a name
     and an organization is more than a phone row can hold, and the words
     carry the least -- an arrow beside a conversation is unambiguous.
     The aria-label lives on the BUTTON, so hiding the span costs a
     screen reader nothing.

     ⚠️ AND THE IDENTITY NO LONGER TAKES ITS OWN ROW. It was order: 3
     with flex-basis: 100%, which was the only way to fit three children
     when the middle one had a labelled button either side of it. With
     the buttons paired and their words dropped everything fits on one
     line, and both labels ellipsise, so the row gives before it wraps. */
  .link-detail-nav-actions .nav-btn-label {
    display: none;
  }

  /* ⚠️ THE BUTTONS ARE MOSTLY PADDING ONCE THE WORDS GO. The base rule
     is 0.5rem 1rem, sized for a label -- around an arrow that is two
     fat boxes for two glyphs. Tightened to fit what is actually in
     them.

     ⚠️ AND THIS TAKES THEM UNDER THE 44px TOUCH TARGET, which is worth
     knowing rather than discovering: roughly 31x31 now. The 0.5rem gap
     on .link-detail-nav-actions is what stops two small adjacent
     targets becoming one mis-tap, and it is deliberately not tightened
     with them. */
  .link-detail-nav-actions button {
    padding: 0.4rem 0.6rem;
  }

  /* ⚠️⚠️ overflow: hidden ON AN ANCESTOR KILLS position: sticky. The
     nearest ancestor with a non-visible overflow becomes the sticky
     element's scroll container, and .links-layout's own `overflow:
     hidden` does not scroll -- so the composer below would simply never
     stick, with nothing to say why. Released here only; the desktop
     rule still clips the three columns to the card's rounded corners.

     ⚠️ SAFE BECAUSE THE WIDTH IS HELD ANOTHER WAY. That overflow was
     also the last line of defence against a too-wide child; the track
     is minmax(0, 1fr) and all three panels are min-width: 0, which is
     what actually stops the layout being pushed past the viewport. */
  .links-layout {
    overflow: visible;
  }

  /* ⚠️ STICKY, NOT FIXED. On mobile the contact list sits BELOW the
     conversation, so a composer fixed to the viewport would float over
     the list as well -- a reply box hovering over a list of names you
     are trying to scroll. Sticky keeps it at the bottom of the screen
     while the conversation is on it and lets it go when you scroll past.

     ⚠️ IT NEEDS ITS OWN BACKGROUND. Sticky content is transparent by
     default, so the thread would scroll visibly through it. */
  .reply-box {
    position: sticky;
    bottom: 0;
    z-index: 2;
    background: var(--c-surface, #ffffff);
    /* ⚠️ Same 4px, same reason -- and the same env() term, because the
       sticky composer sits on the bottom edge of the screen too. */
    padding-bottom: calc(4px + env(safe-area-inset-bottom, 0px));
  }

  /* ⚠️ AND NOT IN DIRECT, where the panel is already fixed to the
     viewport and the composer is a flex item pinned under a thread that
     scrolls inside itself. Sticky there is at best a no-op and at worst
     a second opinion about where the box belongs. */
  #links-section.links-direct-open .reply-box {
    position: static;
  }
}


/* ── Direct: this conversation, on its own ────────────────────────────
 *
 * ⚠️ ONE RENDERER, ONE THREAD. This is a MODE on the panel that already
 * shows the conversation -- not a second view of the same messages. A
 * dedicated screen would need its own copy of renderChatThread, the
 * reply form, the channel pickers, the send path and the socket
 * handlers, and the copy nobody is looking at is the one that stops
 * matching. Everything below is layout.
 *
 * ⚠️⚠️ inset: 0 IS NOT THE VISIBLE SCREEN ON A PHONE, and the comment
 * that stood here claimed it was. A fixed box is laid out against the
 * INITIAL CONTAINING BLOCK, which is the LAYOUT viewport -- on iOS
 * Safari and Chrome for Android that is the tall one, measured with the
 * URL bar retracted. So `bottom: 0` put the bottom edge below the part
 * of the screen you can actually see, and the composer sat under the
 * URL bar with the thread running off the end of the page.
 *
 * 100dvh is the DYNAMIC viewport: it shrinks and grows with the URL bar,
 * which is the measurement this needs. 100vh stays as the line above it
 * for anything that does not know dvh -- which is the old behaviour, not
 * a worse one.
 */
#links-section.links-direct-open {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: auto;
  height: 100vh;
  height: 100dvh;
  z-index: 60;
  border-radius: 0;
  margin: 0;
  max-width: none;
  overflow: hidden;
  /* ⚠️ A grid whose track is `auto` would size to the panel's content
     and leave the rest of the screen showing whatever is behind. One
     row, taking the height. */
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
}

/* ⚠️ display: none, not visibility or opacity -- these are grid items,
   and anything that leaves them in the layout leaves their tracks in it
   too, so the conversation would still be sharing the screen with two
   empty columns. */
#links-section.links-direct-open .links-list-panel,
#links-section.links-direct-open .links-card-panel {
  display: none;
}

#links-section.links-direct-open .links-detail-panel {
  display: flex;
  flex-direction: column;
  min-height: 0;
  max-height: none;
  order: 0;
  border-bottom: 0;
  margin-bottom: 0;
  /* ⚠️ 4px AT THE BOTTOM, and the safe area on top of it. The last line
     in here is "Replying by …", which is a confirmation rather than a
     control and wants to sit tight to the edge.

     ⚠️ THE env() TERM IS NOT THE MARGIN, it is the home indicator. On a
     phone that has one, 4px alone puts the gesture bar straight across
     that line; env() resolves to 0 on every device that does not, so
     this really is 4px there. */
  padding: 0.75rem 1rem calc(4px + env(safe-area-inset-bottom, 0px));
}

/* ⚠️⚠️ #chat-thread AND .reply-box ARE CHILDREN OF #link-detail, NOT OF
   THE PANEL. This is the bug that made the thread run off the bottom of
   the screen: the panel was the flex column and #chat-thread was given
   flex: 1 -- but it is a grandchild, so `flex` applied to nothing, the
   plain block between them grew with its content, and the composer was
   pushed past the end of a container with overflow: hidden.

   Every link in the chain has to be a flex column AND a shrinkable flex
   item, or the first one that is not passes its content's full height
   down to the rest.

   ⚠️ .hidden is display: none !important, so this cannot reveal the
   panel for a contact that is not open -- and setDirectView refuses to
   open with nothing selected anyway. */
#links-section.links-direct-open #link-detail {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

/* ⚠️ min-height: 0 IS LOAD-BEARING ON A FLEX CHILD THAT SCROLLS. A flex
   item's default min-height is auto, so the thread refuses to shrink
   below its own content and pushes the composer off the bottom of the
   screen instead of scrolling inside itself. */
#links-section.links-direct-open #chat-thread {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  max-height: none;
}

#links-section.links-direct-open .reply-box {
  flex: 0 0 auto;
}

/* ⚠️ THE FILTERS GO, THE REPLY PICKER STAYS. Direct is the whole
   conversation, so narrowing it is not on offer -- but choosing what to
   answer ON still is, and that is a different control at the bottom of
   the panel (#reply-channel-toggle), which is deliberately untouched
   here. Selected on the data attribute so the Direct and E2E buttons,
   which wear .channel-btn for the look and carry no channel, stay. */
#links-section.links-direct-open #thread-channel-toggle .channel-btn[data-thread-channel] {
  display: none;
}

.thread-all-note {
  display: none;
  font-size: 0.75rem;
  align-self: center;
}

#links-section.links-direct-open .thread-all-note {
  display: inline;
}

/* ⚠️ THE FLOATING WIDGETS ARE HIDDEN WHILE DIRECT IS OPEN, rather than
   being out-stacked. Direct is z-index 60; the AI bubble is 9999, its
   panel 9998 and the requests FAB 9997 -- so covering them by stacking
   would mean lifting Direct past 9999, and #error-modal-overlay is a
   .modal-overlay at 1000 that IS reachable from inside this view (the
   failed-message ❌ in the thread opens it). The delivery-failure
   details would then have opened behind the screen you were reading.

   ⚠️ TOASTS AND MODALS DELIBERATELY STAY. A "Message sent" toast and a
   failure modal are both about the thing you are doing in here; the two
   bubbles are doorways to somewhere else, which is what makes them the
   ones to put away.

   ⚠️ AND THE PANEL, NOT JUST THE BUTTON. The AI panel can already be
   open when Direct is, and hiding only its launcher would leave the
   panel floating over the conversation with no way to shut it. */
/* ⚠️ THE PAGE BEHIND MUST NOT SCROLL. Direct covers the viewport, so a
   drag that misses the thread scrolled the app underneath it -- and on
   mobile that scrolling is also what collapses and expands the URL bar,
   which resizes the 100dvh box mid-gesture. Locking it makes the height
   stable as well as stopping the page moving. */
body.direct-view-open {
  overflow: hidden;
}

body.direct-view-open .ai-bubble-btn,
body.direct-view-open .ai-chat-panel {
  display: none;
}

.thread-direct-btn[aria-expanded="true"] {
  background: var(--c-brand-strong, #4f46e5);
  color: var(--c-on-accent, #ffffff);
  border-color: var(--c-brand-strong, #4f46e5);
}

/* ── E2E Secure: the way out, and a shape that fits a phone ───────────
 *
 * ⚠️ .sec-shell SETS ITS OWN --sec-* PALETTE and is dark in BOTH themes
 * on purpose -- the same arrangement as the header and the legal hero.
 * The top bar draws from that palette, not from the page tokens, or it
 * would be a light strip on a dark module in light mode.
 */
.sec-topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.7rem 0.9rem;
  border-bottom: 1px solid var(--sec-line);
  font-size: 0.85rem;
  flex: 0 0 auto;
}

.sec-topbar-title { font-weight: 700; }

.sec-close-btn {
  border: 1px solid var(--sec-line);
  background: none;
  color: var(--sec-text);
  border-radius: 0.5rem;
  padding: 0.3rem 0.7rem;
  font-size: 0.78rem;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
}

.sec-close-btn:hover { border-color: var(--sec-accent); color: var(--c-on-accent, #ffffff); }

/* ⚠️ 19rem OF RAIL ON A 390px PHONE LEFT ~86px FOR THE CONVERSATION.
   .sec-main had no responsive rule at all, so the module was unusable on
   a phone -- the rail stacks above the chat now, capped, so the
   conversation gets the screen. */
@media (max-width: 820px) {
  .sec-main {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr);
  }

  .sec-rail {
    border-right: 0;
    border-bottom: 1px solid var(--sec-line);
    max-height: 40vh;
  }

  .sec-shell { min-height: 80vh; }
  .sec-topbar { padding: 0.6rem 0.7rem; }
}


/* ⚠️ VERY SMALL, AND IT SHARES .chat-meta's MUTED INK. It sits beside a
   timestamp; anything that reads as loudly as the message would be
   competing with it. Its own token would be a second answer to "how
   quiet is the meta line". */
.chat-channel-tag {
  font-size: 0.62rem;
  letter-spacing: 0.01em;
  opacity: 0.85;
  white-space: nowrap;
}


/* ── /linxis fills the window ──────────────────────────────────────────
 *
 * The height comes from --header-h, published by js/headerHeight.js from
 * the real measured chrome (header offsetTop + offsetHeight), so no pixel
 * value is written here and the layout follows the header if it changes.
 * The 0px default keeps the calc() valid for the moment before JS runs.
 *
 * ⚠️ DESKTOP ONLY. Below 769px the three panels are STACKED
 * (grid-template-columns: minmax(0,1fr)) and the PAGE is meant to scroll;
 * a definite height there would clip the stack against .links-layout's
 * own overflow: hidden.
 *
 * ⚠️ min-height: 0 IS THE PART THAT MAKES THEM SCROLL. A grid item
 * defaults to min-height: auto and refuses to shrink below its content,
 * so without it the panels push the row past the height just set and
 * overflow: hidden swallows the overspill instead of scrolling it.
 *
 * Direct is unaffected: #links-section.links-direct-open sets its own
 * height and is (1,1,0) against this rule's (0,1,0).
 */
:root { --header-h: 0px; }

@media (min-width: 769px) {
  .links-layout {
    height: calc(100vh - var(--header-h));
    grid-template-rows: minmax(0, 1fr);
  }

  .links-list-panel,
  .links-card-panel {
    min-height: 0;
    overflow-y: auto;
  }

  /* ⚠️ THE PANEL SCROLLS NOTHING; THE THREAD DOES. Left as a plain block
     with overflow-y: auto it scrolls as ONE piece -- the contact header
     slides off the top and the composer off the bottom, which is what
     "it rolls under the page" is. It is the flex CONTAINER now. */
  .links-detail-panel {
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }

  /* ⚠️ #chat-thread is a GRANDCHILD of the panel, so flex on it does
     nothing until #link-detail is a column too. */
  #link-detail {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
  }

  /* (1,0,0) beats .chat-thread's (0,1,0) 45vh -- no !important needed. */
  #chat-thread {
    flex: 1;
    min-height: 0;
    max-height: none;
    overflow-y: auto;
  }

  /* Holds its size, so it pins to the bottom instead of being pushed. */
  .reply-box {
    flex: 0 0 auto;
  }
}


/* ── /mail fills the window ────────────────────────────────────────────
 *
 * Same shape as the /linxis block above, and it replaces a guess: both
 * webmail panels carried `max-height: calc(100vh - 200px)` -- a hardcoded
 * estimate of "header/nav chrome" whose own comment says it was put there
 * to close this exact gap. 200px is not the real figure; --header-h is,
 * measured by js/headerHeight.js.
 *
 * ⚠️ DESKTOP ONLY -- below 769px webmail is a one-column mobile view that
 * swaps panels, and a definite height would clip it.
 *
 * ⚠️ NO overflow-y ON THE LIST PANEL. Its inner .webmail-message-list is
 * already `flex: 1; overflow-y: auto`, so a second scroller on the panel
 * would nest two scrollbars. Only the folder panel gains one, because
 * nothing inside it scrolls.
 */
@media (min-width: 769px) {
  body:has(#tab-webmail:not(.hidden)) main {
    padding: 0;
  }

  .webmail-layout {
    height: calc(100vh - var(--header-h));
    grid-template-rows: minmax(0, 1fr);
  }

  .webmail-folder-panel,
  .webmail-list-panel,
  .webmail-reading-panel {
    min-height: 0;
    max-height: none;
  }

  .webmail-folder-panel {
    overflow-y: auto;
  }
}


/* ── /linxis and /mail run edge to edge on mobile ──────────────────────
 *
 * All four sides. The card meets the header directly and runs to the
 * bottom of the scroll.
 *
 * ⚠️ LAST IN THE FILE ON PURPOSE. The /linxis selector is identical to the
 * one in the mobile block above -- same specificity -- so source order is
 * what decides it, and this has to come after.
 *
 * ⚠️ THE PANELS KEEP THEIR OWN PADDING (0.6rem-0.65rem), so the text does
 * not end up against the glass; what goes is the ring around the card.
 */
@media (max-width: 768px) {
  main:has(> #tab-links:not(.hidden)),
  body:has(#tab-webmail:not(.hidden)) main {
    padding: 0;
  }

  /* ⚠️ THE CORNERS ARE THE "4px GAP". .card is border-radius: 8px, and a
     full-bleed card curves away from the top and bottom edges -- the page
     background shows through the curve and reads as a hairline band above
     and below the content. Square it off, and drop the side borders,
     which now sit on the screen edge where they mark nothing. The top and
     bottom borders stay: they are the seam against the header and the end
     of the content. */
  main:has(> #tab-links:not(.hidden)) > #tab-links > .card,
  body:has(#tab-webmail:not(.hidden)) main .card.webmail-layout {
    border-radius: 0;
    border-left: none;
    border-right: none;
  }
}


/* ── The mobile tab drawer is a 3-across grid of squares ───────────────
 *
 * Nine tabs, so it lands as exactly three full rows.
 *
 * ⚠️ ON #main-nav.open, NOT #main-nav. The open state is what carries
 * `display: flex` in the block above -- setting grid on the base rule
 * would be overridden by it the moment the drawer opens. Same selector,
 * later in the file, so it wins on source order.
 *
 * ⚠️ THE ICON SITS OVER THE LABEL WITH NO MARKUP CHANGE. Each entry is
 * `<span aria-hidden>emoji</span> Label`, and in a flex column the span
 * is one item and the bare text node becomes an anonymous second one --
 * so they stack without wrapping the label in anything.
 */
@media (max-width: 768px) {
  #main-nav.open {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0.4rem;
  }

  #main-nav .nav-tab-btn {
    width: auto;
    aspect-ratio: 1 / 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.3rem;
    text-align: center;
    padding: 0.4rem 0.25rem;
    font-size: 0.68rem;
    line-height: 1.2;
    /* "Compose & Send" is the longest label; two lines fit, a third
       would push the glyph off a square tile. */
    overflow: hidden;
  }

  #main-nav .nav-tab-btn span[aria-hidden="true"] {
    font-size: 1.5rem;
    line-height: 1;
  }
}

/* ── A2P sub-use-cases and manual-mode filings (migration 136) ───────── */
.a2p-subs-row { margin-top: 0.4rem; }
.a2p-subs-row .filter-chip.active { background: var(--c-brand, #6366f1); border-color: var(--c-brand, #6366f1); }
.a2p-subs-row .filter-chip:disabled { opacity: 0.6; cursor: default; }
.a2p-mode-badge {
  display: inline-block; font-size: 0.7rem; font-weight: 700; letter-spacing: 0.02em;
  padding: 0.15rem 0.55rem; border-radius: 999px; margin-left: 0.4rem; vertical-align: middle;
  background: var(--c-surface-subtle, #f8fafc); color: var(--c-text-muted, #64748b);
  border: 1px solid var(--c-border, #e2e8f0);
}
.a2p-mode-badge.is-manual { background: var(--c-info-tint, #eff6ff); color: var(--c-info, #2563eb); border-color: var(--c-info-border, #bfdbfe); }
.a2p-adopt { margin: 0 0 1rem; font-size: 0.85rem; }
.a2p-adopt summary { cursor: pointer; font-weight: 600; }

/* ── A2P brand cards and the sole-proprietor OTP step (migration 137) ── */
.a2p-brand-otp { margin-top: 0.6rem; padding: 0.75rem; border-radius: 0.6rem; border: 1px solid var(--c-warning-border, #fde68a); background: var(--c-warning-tint, #fffbeb); }
.a2p-brand-otp.is-verified { border-color: var(--c-success-border, #bbf7d0); background: var(--c-success-tint, #f0fdf4); }
.a2p-brand-otp.is-expired { border-color: var(--c-danger-border, #fecaca); background: var(--c-danger-tint, #fef2f2); }
.a2p-brand-otp-row { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; margin-top: 0.5rem; }
.a2p-brand-otp-row input { width: 9rem; padding: 0.45rem 0.6rem; border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.5rem; font-size: 1rem; letter-spacing: 0.15em; }
.a2p-brand-identity { display: inline-block; font-size: 0.7rem; font-weight: 700; padding: 0.15rem 0.55rem; border-radius: 999px; margin-left: 0.4rem; vertical-align: middle; background: var(--c-surface-subtle, #f8fafc); color: var(--c-text-muted, #64748b); border: 1px solid var(--c-border, #e2e8f0); }
.a2p-brand-identity.is-verified { background: var(--c-success-tint, #f0fdf4); color: var(--c-success, #16a34a); border-color: var(--c-success-border, #bbf7d0); }
.a2p-brand-identity.is-unverified { background: var(--c-warning-tint, #fffbeb); color: var(--c-warning-strong, #d97706); border-color: var(--c-warning-border, #fde68a); }
.mnc-a2p-action { color: var(--c-info, #2563eb) !important; }
.a2p-brand-edit { margin-top: 0.6rem; padding: 0.75rem; border-radius: 0.6rem; border: 1px solid var(--c-border, #e2e8f0); background: var(--c-surface-subtle, #f8fafc); }
.a2p-brand-field { margin-bottom: 0.6rem; }
.a2p-brand-field.is-identity { padding: 0.5rem; border-radius: 0.5rem; border: 1px solid var(--c-warning-border, #fde68a); background: var(--c-warning-tint, #fffbeb); }
.a2p-brand-field.is-locked { opacity: 0.9; }
.a2p-brand-input { width: 100%; padding: 0.5rem 0.6rem; border: 1px solid var(--c-border, #e2e8f0); border-radius: 0.5rem; font-size: 0.825rem; background: var(--c-surface, #fff); color: var(--c-text, #0f172a); }
.a2p-brand-field-note { font-size: 0.7rem; color: var(--c-text-muted, #64748b); margin: 0.25rem 0 0; }
.a2p-brand-field-note.is-warning { color: var(--c-warning-strong, #d97706); }
.a2p-brand-hint { display: inline-block; margin-top: 0.3rem; font-size: 0.7rem; padding: 0.2rem 0.5rem; border-radius: 999px; border: 1px solid var(--c-brand-border, #c7d2fe); background: var(--c-brand-tint, #eef2ff); color: var(--c-brand-deep, #4338ca); cursor: pointer; }
.cp-check { display: flex; gap: 0.6rem; align-items: flex-start; font-size: 0.85rem; font-weight: 500; cursor: pointer; margin: 0.25rem 0 0.5rem; }
.cp-check input { margin-top: 0.2rem; }
.cp-check small { display: block; font-size: 0.74rem; color: var(--c-text-muted, #64748b); font-weight: 400; margin-top: 0.15rem; }
.cp-hint { font-size: 0.72rem; margin: 0.25rem 0 0; }
.cp-hint button { background: none; border: none; padding: 0; color: var(--c-brand, #6366f1); font-weight: 600; cursor: pointer; font-size: inherit; }
.cp-cleared { margin: 1rem 0; padding: 0.85rem 1rem; border-radius: 0.6rem; border: 1px solid var(--c-warning-border, #fde68a); background: var(--c-warning-tint, #fffbeb); color: var(--c-warning-strong, #92400e); font-size: 0.85rem; }

/* ── The Team tab's head (linxiNetwork.js) ─────────────────────────
   The Chat / Feed toggle and the "which Linxi" note on one row. The
   toggle reuses .view-toggle; only the row and the feed panel's own
   title and note are new. */
.team-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap; }
.team-head-note { font-size: 0.8rem; }
.team-feed-title { margin: 0; font-size: 1.05rem; font-weight: 700; }
.team-feed-note { font-size: 0.8rem; margin: 0.75rem 0 0; }

/* ── The social site's topbar nav (network.html, feed.html, profile.html)
   Three pages, one strip: the brand on the left, the way between the
   pages in the middle, the way into the app on the right. Sits inside
   .info-topbar, which every public page already uses. */
.info-topbar-nav { display: flex; gap: 0.25rem; align-items: center; flex: 1; justify-content: center; }
.info-topbar-nav a { color: var(--c-chrome-text, #cbd5e1); text-decoration: none; font-size: 0.85rem; font-weight: 600; padding: 0.35rem 0.75rem; border-radius: 999px; position: relative; white-space: nowrap; }
.info-topbar-nav a:hover { color: var(--c-on-accent, #ffffff); background: color-mix(in srgb, var(--c-highlight, #ffffff) 10%, transparent); }
.info-topbar-nav a.active { color: var(--c-on-accent, #ffffff); background: color-mix(in srgb, var(--c-highlight, #ffffff) 16%, transparent); }
.social-unread-badge { display: inline-block; min-width: 18px; padding: 0 0.35rem; border-radius: 999px; background: var(--c-brand, #6366f1); color: var(--c-on-accent, #ffffff); font-size: 0.68rem; font-weight: 700; line-height: 18px; text-align: center; margin-left: 0.35rem; vertical-align: middle; }
@media (max-width: 640px) {
  .info-topbar-nav a { padding: 0.35rem 0.5rem; font-size: 0.8rem; }
}

/* ── The same strip on a phone.
   ⚠️⚠️ IT HAD NO MENU AT ALL. Everything above is one nowrap flex row --
   brand, four pill links, Log in, Sign up -- and nothing in it could wrap
   or fold, so on a 390px phone the bar simply ran off the side: on
   sensorium.events the row wants about 700px signed in, and the only
   concession to a small screen was the rule above making the pills a
   little smaller. The builder's templates (siteChromeDefaults'
   `.soc-menu`) have had a hamburger since they shipped, so an admin who
   loaded one got a working phone bar and an admin who kept the built-in
   one did not -- which is the wrong way round, because the built-in one
   is what every install starts with.

   ⚠️ 860px, THE SAME BREAKPOINT THE TEMPLATES USE. The whole design of
   those templates is "the built-in bar, written out"; swapping to the
   menu at a different width would make Load change the layout on a class
   of screens before the admin had edited anything.

   ⚠️⚠️ THE NAV IS NOT COPIED INTO A PANEL -- IT *IS* THE PANEL. The
   obvious build is a second copy of the links inside the <details>, which
   is what the site-header and social-header templates do. This bar cannot:
   `id="social-nav-profile"` and `id="social-unread-badge"` live on those
   links and both are read with getElementById, so a copy is a duplicate id
   and the username and the unread count would go to whichever came first
   -- on a phone, the one that is display:none. The disclosure therefore
   ships EMPTY and opens its own siblings.

   ⚠️ WHICH IS WHY <details> IS WRITTEN BEFORE THE NAV in all six copies of
   this bar. `~` only reaches forward. Move it after the links and the
   markup still validates, the burger still draws, and nothing opens.

   ⚠️ AND THE BAR WRAPS RATHER THAN THE PANEL FLOATING. An absolute panel
   would have to contain the nav and the links, and they are two separate
   elements -- stacking them means knowing the first one's rendered height,
   which CSS does not. `flex-wrap` puts each on its own full-width line
   under the brand, in DOM order, at whatever height it turns out to be. */
.info-topbar-menu { display: none; }
.info-topbar-burger {
  display: block; width: 20px; height: 2px; border-radius: 2px;
  background: currentColor; position: relative;
}
.info-topbar-burger::before,
.info-topbar-burger::after {
  content: ""; position: absolute; left: 0;
  width: 20px; height: 2px; border-radius: 2px; background: currentColor;
}
.info-topbar-burger::before { top: -6px; }
.info-topbar-burger::after { top: 6px; }
/* Open: the outer bars cross and the middle one goes. */
.info-topbar-menu[open] .info-topbar-burger { background: transparent; }
.info-topbar-menu[open] .info-topbar-burger::before { top: 0; transform: rotate(45deg); }
.info-topbar-menu[open] .info-topbar-burger::after { top: 0; transform: rotate(-45deg); }

@media (max-width: 860px) {
  .info-topbar { flex-wrap: wrap; row-gap: 0.5rem; padding: 0.6rem 1rem; }
  /* ⚠️ The brand is `font-weight: 900` at 1.25rem and a white-label name
     can be three words. Shrinking it is what buys the burger its corner
     without hiding the site's own name, which is the one thing on this bar
     that says whose site this is. */
  .info-topbar-logo { font-size: 1.05rem; }
  /* ⚠️ `margin-left: auto` PINS THE BURGER TO THE EDGE rather than the
     bar's `justify-content`. On the wrapped line the burger is the last
     item and space-between does put it right -- but that only holds while
     the line has exactly two items on it, and it is the NAV's `flex: 1`
     that keeps it there on a wide screen. One declaration that does not
     care how many items share the line is the version that survives
     somebody adding a third. */
  .info-topbar-menu { display: block; margin-left: auto; }
  .info-topbar-menu > summary {
    list-style: none; cursor: pointer; width: 2.4rem; height: 2.4rem;
    display: flex; align-items: center; justify-content: center;
    color: var(--c-chrome-text, #cbd5e1);
    border: 1px solid color-mix(in srgb, currentColor 26%, transparent);
    border-radius: 0.6rem;
  }
  .info-topbar-menu > summary::-webkit-details-marker { display: none; }
  .info-topbar-menu > summary::marker { content: ""; }
  .info-topbar-menu > summary:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }

  .info-topbar-nav, .info-topbar-links { display: none; }
  /* ⚠️ `flex: 1 0 100%` and not just `width: 100%`: .info-topbar-nav
     already carries `flex: 1`, whose basis is 0 -- it would share the
     brand's line rather than forcing its own. */
  .info-topbar-menu[open] ~ .info-topbar-nav,
  .info-topbar-menu[open] ~ .info-topbar-links {
    display: flex; flex: 1 0 100%; flex-direction: column;
    align-items: stretch; justify-content: flex-start; gap: 0.15rem;
  }
  /* ⚠️ NOTHING HERE SETS `display` ON THE LINKS THEMSELVES, deliberately.
     Half of .info-topbar-links ships with the `hidden` attribute and
     socialTopbar.js reveals the right half; a display rule on the anchors
     would out-specify the UA's `[hidden]` and put Log in and Open Panel on
     screen together until the script ran, and for ever if it never did. */
  .info-topbar-menu[open] ~ .info-topbar-nav a,
  .info-topbar-menu[open] ~ .info-topbar-links a {
    padding: 0.6rem 0.7rem; border-radius: 0.5rem;
    font-size: 0.95rem; text-align: left;
  }
  .info-topbar-menu[open] ~ .info-topbar-links {
    border-top: 1px solid color-mix(in srgb, var(--c-chrome-text, #cbd5e1) 18%, transparent);
    padding-top: 0.5rem; margin-top: 0.25rem;
  }
  .info-topbar-menu[open] ~ .info-topbar-links .info-topbar-cta { text-align: center; }
}

/* The Team feed composer's audience note, where the social composer has
   its visibility picker. */
.compose-scope-note { font-size: 0.78rem; margin-right: auto; }

/* Linxi overview: the team network address block (workspaceOverview.js) */
.ws-ov-team { display: flex; flex-direction: column; gap: 0.5rem; }
.ws-ov-team-row { display: flex; gap: 0.75rem; align-items: baseline; font-size: 0.85rem; flex-wrap: wrap; }
.ws-ov-team-row span { min-width: 3rem; }
.ws-ov-team-row a { word-break: break-all; }
.ws-ov-team-edit { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 0.5rem; align-items: center; margin-top: 0.5rem; }
.ws-ov-team-edit label { font-size: 0.8rem; }
.ws-ov-team-edit input { min-width: 0; }
.ws-ov-team-edit p { grid-column: 1 / -1; font-size: 0.75rem; margin: 0; }
@media (max-width: 640px) { .ws-ov-team-edit { grid-template-columns: 1fr; } }

/* ── "Powered by" attribution ─────────────────────────────────────────
   Shown only where the deployment asked for it: the element carries
   `hidden` in the markup and brandMeta un-hides it when `poweredBy` is
   on, removing it outright when off. Quiet by design -- it is a credit,
   not a call to action, so it sits below the content in muted type and
   never competes with the page's own links. */
/* The bottom bar of the social footer: the credit on the left, the
   copyright on the right.

   ⚠️ IT SITS INSIDE .page-links-inner, so it inherits that container's
   max-width, centring and side padding -- a bar built outside it would
   line up with the viewport instead of with the links directly above it,
   which reads as two separate footers rather than one.

   ⚠️ flex-wrap PLUS space-between IS THE WHOLE MOBILE STORY. On a narrow
   screen the two halves stack; without the wrap they would be squeezed
   into one line until the copyright ran off the edge. row-gap is what
   keeps them apart once they have stacked. */
/* ⚠️ THE BAR IS A CHILD OF THE NAV, NOT OF .page-links-inner. That inner
   container is max-width 1000px and centred, so a bar inside it stops a
   column short of each edge on a wide screen. Out here it spans the
   footer and carries its own side padding, so the credit reaches the left
   edge and the legal group the right. */
.social-footer-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem 1.5rem;
  padding: 1.15rem 1.5rem 1.35rem;
  border-top: 1px solid var(--c-border, #e2e8f0);
}
/* The copyright and the legal links travel together as the right half --
   three loose children would space evenly instead of hugging the edges. */
.social-footer-legal {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem 1rem;
}
/* ⚠️⚠️ ONE SIZE, SET IN ONE PLACE, FOR BOTH HALVES. These drifted apart by
   accident: an earlier override put `font-size: inherit` on the copyright
   to undo a `.page-links span` rule that never applied to it -- it is a
   <span> in a <div>, not a link's label -- so the copyright inherited the
   body's 16px while the credit sat at 0.78rem. They are the same line and
   must be the same size, so the value lives on the bar and both halves
   inherit it. */
.social-footer-bar,
.social-footer-bar .site-poweredby,
.social-footer-bar .social-footer-copy,
.social-footer-bar .social-footer-legal a {
  font-size: 0.875rem;
}
.social-footer-copy { margin: 0; color: var(--c-text-dim, #94a3b8); }
.social-footer-legal a { color: var(--c-text-dim, #94a3b8); text-decoration: none; }
.social-footer-legal a:hover { color: var(--c-brand, #6366f1); text-decoration: underline; }

/* ⚠️ inline-flex, SO THE MARK AND THE WORDS SHARE ONE ROW. The platform
   logo sits beside the text; as a plain block the <img> and the sentence
   stacked.

   ⚠️ AND margin: 0 INSIDE THE BAR. This rule carried `margin: 1.5rem auto
   0`, whose `auto` centred it -- correct when it was a lone line under a
   page, and wrong inside a flex row, where the auto margin fights
   space-between and drags the credit off the left edge it is meant to
   sit on. */
.site-poweredby {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.78rem;
  color: var(--c-text-muted);
}
/* ⚠️ SIZED IN CSS AS WELL AS ON THE TAG. The width/height attributes
   reserve the box before the image arrives -- it is cross-origin, from
   linxi.app, so it lands after everything local and would otherwise nudge
   the line as it does. The rule is what holds if the logo's intrinsic
   size is not 16px. */
.site-poweredby-logo {
  width: 32px; height: 32px;
  object-fit: contain;
  display: block;
  flex: none;
}
/* The credit is a single link wrapping the mark and the words, so the
   link carries the row. Stated here too, for any footer that is not
   inside nav.page-links. */
.site-poweredby-link {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  text-decoration: none;
  color: inherit;
}

/* ⚠️⚠️ THE CREDIT NOW LIVES INSIDE <nav class="page-links">, SO IT INHERITS
   THE CARD STYLING MEANT FOR THE LINK BOXES. `.page-links a` is
   `display:flex; flex-direction:column` with padding, a border, a radius
   and a background -- applied to the "Linxi" link it stacked "Powered by"
   above the name and drew a box around it, which is the "button" nobody
   put there. `.page-links span` restyled the text beside it for the same
   reason.

   Moving the credit out of the nav would fix it too and cost the shared
   max-width and padding that put it under the links in the first place.
   So the card styling is switched off for this one element instead.

   ⚠️ SPECIFICITY, NOT !important: `.page-links .site-poweredby a` is two
   classes and a type, which beats `.page-links a` on its own. */
/* ⚠️ THE LEGAL LINKS NEED THE SAME ESCAPE AS THE CREDIT LINK. They are
   inside nav.page-links too, so `.page-links a` would draw each of Terms
   and Privacy as its own card. */
.page-links .site-poweredby a,
.page-links .social-footer-legal a {
  height: auto;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  transition: none;
}
.page-links .social-footer-legal a { display: inline; }
/* ⚠⚠ inline-FLEX, AND IT HAS TO BE SAID AT THIS SPECIFICITY. The credit
   is one link around the mark AND the words now -- click any part of it
   and you go to the platform -- so this anchor is the flex row. The rule
   above used to say `display: inline` for both, which flattened it and
   put the mark on its own line. `.site-poweredby-link` alone is (0,1,0)
   and loses to `.page-links .site-poweredby a` at (0,2,1). */
/* ⚠⚠ EVERY PROPERTY `.page-links a` SETS, not just display. That rule
   draws the link CARDS in this nav and it is `display: flex` +
   `flex-direction: column`; overriding the display alone left the column
   standing, so the credit's mark sat ON TOP of its words instead of
   beside them. The escape above covers height, padding, border, radius,
   background and transition; these are the two it did not.

   The lesson is the one .soc-actions already carries in
   siteChromeDefaults: a scoped rule out-specifies an unscoped one on the
   properties it DECLARES and on nothing else. t-269 computes the
   difference now rather than trusting anybody to have listed them. */
.page-links .site-poweredby a {
  display: inline-flex;
  flex-direction: row;
  align-items: center;
  gap: 0.35rem;
  text-decoration: none;
}
.page-links .site-poweredby a:hover,
.page-links .site-poweredby a:focus-visible,
.page-links .social-footer-legal a:hover,
.page-links .social-footer-legal a:focus-visible {
  box-shadow: none;
  transform: none;
  border: 0;
}
/* ⚠️ THE SPAN ONLY. `.page-links span` styles the description under each
   link box; the credit's own <span> sits in its reach and needs undoing.
   The copyright is NOT in this list -- an earlier version put it here,
   which is what set it to the body's 16px and broke the pairing. */
.page-links .site-poweredby span {
  font-size: inherit;
  color: inherit;
  line-height: inherit;
}
.site-poweredby a {
  color: var(--c-text-muted);
  text-decoration: none;
  border-bottom: 1px solid var(--c-border);
}
.site-poweredby a:hover { color: var(--c-brand); border-bottom-color: var(--c-brand); }
.site-poweredby a:focus-visible { outline: 2px solid var(--c-brand); outline-offset: 2px; }
/* On the Label card it sits under the call to action, inside a narrow
   column, so it needs no extra room above. */
.public-card-shell .site-poweredby { margin-top: 0.75rem; }

/* ── Copy button on the site editors ──────────────────────────────────
   Sits at the top right of each HTML and CSS box, beside the character
   count. Small and quiet: it is a convenience next to a heading, not one
   of the panel's actions, so it must not read as heavily as Save. */
.ss-copy-btn {
  padding: 0.2rem 0.6rem;
  font-size: 0.75rem;
  line-height: 1.6;
  border-radius: 6px;
  min-width: 5.5rem;   /* holds "Copied" and "Press Ctrl+C" without the row jumping */
}
.ss-copy-btn.is-copied {
  border-color: var(--c-success);
  color: var(--c-success);
}
/* The count and the Copy button travel together at the right of the row,
   so .sl-section-head's space-between still puts the title at one end and
   these at the other rather than spreading three items across it. */
.sl-section-head-actions {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-left: auto;
}

/* ── The people directory, as cards ───────────────────────────────────
   Was a nine-column table. A row per person made the two questions this
   page is actually opened for -- who is this, and is the account doing
   anything -- something you read across, and it could not survive a
   narrow window. The card puts identity at the top, the three numbers
   together, and the actions at the foot. */
.users-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(310px, 1fr));
  gap: 1rem;
  margin-top: 1rem;
}
.user-card {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding: 1rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 12px;
  background: var(--c-surface, #ffffff);
  box-shadow: 0 1px 2px color-mix(in srgb, var(--c-shadow, #000000) 6%, transparent);
}
/* A platform identity reads differently from a person, at a glance. */
.user-card-system {
  background: var(--c-surface-subtle, #f8fafc);
  border-style: dashed;
}
.user-card-head { display: flex; align-items: center; gap: 0.7rem; min-width: 0; }
.user-card-avatar {
  flex: 0 0 auto;
  width: 40px; height: 40px;
  border-radius: 50%;
  display: grid; place-items: center;
  font-weight: 600; font-size: 0.85rem;
  background: var(--c-brand, #6366f1);
  color: var(--c-on-accent, #ffffff);
}
.user-card-avatar-system { background: var(--c-text-muted, #64748b); }
.user-card-who { min-width: 0; }
.user-card-name-row { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }
.user-card-name {
  font-weight: 600;
  color: var(--c-text, #0f172a);
  overflow-wrap: anywhere;
}
.user-card-username { font-size: 0.8rem; color: var(--c-text-muted, #64748b); overflow-wrap: anywhere; }
.user-card-contact { min-width: 0; }
/* ⚠️ An address is long and arbitrary. It wraps rather than being
   clipped: a truncated email on the page you use to identify somebody is
   the one thing this card must not do. */
.user-card-email {
  font-size: 0.83rem;
  color: var(--c-text-2, #334155);
  text-decoration: none;
  overflow-wrap: anywhere;
}
.user-card-email:hover { color: var(--c-brand, #6366f1); text-decoration: underline; }
.user-card-biz { font-size: 0.78rem; color: var(--c-text-muted, #64748b); margin-top: 0.15rem; }
.user-card-tags { display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; }
.user-card-stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.5rem;
  padding: 0.6rem 0;
  border-top: 1px solid var(--c-border, #e2e8f0);
  border-bottom: 1px solid var(--c-border, #e2e8f0);
}
.user-card-stat { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.user-card-stat-n {
  font-weight: 600;
  font-size: 0.95rem;
  color: var(--c-text, #0f172a);
  font-variant-numeric: tabular-nums;
}
.user-card-stat-l {
  font-size: 0.68rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--c-text-muted, #64748b);
}
.user-card-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.5rem; flex-wrap: wrap;
  margin-top: auto;
}
.user-card-joined { font-size: 0.75rem; color: var(--c-text-muted, #64748b); }
.user-card-actions { display: flex; gap: 0.35rem; flex-wrap: wrap; }
.user-card-actions button { padding: 0.3rem 0.6rem; font-size: 0.78rem; }

/* The sort bar that replaced the table's clickable column headers. */
.users-sortbar {
  display: flex; align-items: center; gap: 0.4rem;
  flex-wrap: wrap; margin-top: 0.75rem;
}
.users-sortbar-label {
  font-size: 0.72rem; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--c-text-muted, #64748b); font-weight: 600; margin-right: 0.15rem;
}
/* ⚠️ The direction is shown, not just the column. A sort control that
   says which field but not which way leaves you pressing it to find out. */
.users-sortbar .users-sortable.sorted::after {
  content: ' \2193';
  font-size: 0.85em;
}
.users-sortbar .users-sortable.sorted[data-sort-dir="asc"]::after { content: ' \2191'; }

/* ── Admin → Users → Access Profiles: the profile list ────────────────
   A profile is a named set of switches, and its three counts are the
   whole summary -- how many accounts have it, how many tabs it hides,
   how many features it turns off. As a flat row those were a
   middot-joined line of prose; here they are three figures you can
   compare down a column. Same visual language as the directory cards one
   panel over, on the same tokens, so both follow light and dark. */
.ap-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 1rem;
  margin-top: 1rem;
}
.ap-card {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  padding: 1rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 12px;
  background: var(--c-surface, #ffffff);
  box-shadow: 0 1px 2px color-mix(in srgb, var(--c-shadow, #000000) 6%, transparent);
}
/* One of the three role profiles every account resolves through. It
   cannot be deleted, so it reads as furniture rather than as something
   somebody made. */
.ap-card-builtin { background: var(--c-surface-subtle, #f8fafc); }
.ap-card-title { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }
.ap-card-name {
  font-weight: 600;
  color: var(--c-text, #0f172a);
  overflow-wrap: anywhere;
}
.ap-badge-builtin {
  background: var(--c-surface-sunken, #f1f5f9);
  color: var(--c-text-muted, #64748b);
  border: 1px solid var(--c-border, #e2e8f0);
}
.ap-card-desc {
  font-size: 0.8rem;
  line-height: 1.45;
  color: var(--c-text-2, #334155);
  overflow-wrap: anywhere;
  flex: 1;
}
.ap-card-stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.5rem;
  padding: 0.6rem 0;
  border-top: 1px solid var(--c-border, #e2e8f0);
  border-bottom: 1px solid var(--c-border, #e2e8f0);
}
.ap-card-stat { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.ap-card-stat-n {
  font-weight: 600;
  font-size: 0.95rem;
  color: var(--c-text, #0f172a);
  font-variant-numeric: tabular-nums;
}
/* Zero is shown rather than omitted -- a profile that hides nothing is
   what a new one IS, and a missing figure reads as one that failed to
   load. Muted, so the card still says so at a glance. */
.ap-card-stat-quiet .ap-card-stat-n { color: var(--c-text-dim, #94a3b8); font-weight: 500; }
/* People waiting on somebody. Amber, never the success colour: it is
   work outstanding, not a thing that went well -- and it is drawn only
   when the count is non-zero, so a permanent grey 0 never trains anyone
   to skip past it. */
.ap-card-stat-waiting .ap-card-stat-n { color: var(--c-warning); }
.ap-card-stat-waiting .ap-card-stat-l { color: var(--c-warning); }
.ap-card-stat-l {
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--c-text-muted, #64748b);
}
.ap-card-actions { display: flex; gap: 0.4rem; }

/* ── Access profile → the signup form ─────────────────────────────────
   Everything under "Offer this on the signup form" is meaningless until
   it is switched on, so it is hidden rather than greyed: a disabled icon
   box beside a disabled dropdown reads as a panel that failed to load,
   and the switch above it already says why they are not there. */
.ap-signup-rest {
  margin-top: 0.75rem;
  padding-left: 0.9rem;
  border-left: 2px solid var(--c-border, #e2e8f0);
}
/* The icon is one or two emoji. A full-width box invites a sentence,
   which is exactly what the server drops. */
.ap-signup-icon { max-width: 7rem; font-size: 1.1rem; }

/* ── Admin → Users → Access profiles: the editor ──────────────────────
   Two columns from 900px, one below it. It was a single column of six
   stacked lists, and Tabs and Features are long enough on their own that
   everything after them sat below the fold on a laptop.

   ⚠️ Each .ap-col is ONE WHOLE BLOCK -- a heading, its explanation and
   the list it describes. A CSS multi-column would have been fewer rules
   and would break a column between a heading and its own list, which is
   the one place a break must never land. */
.ap-edit-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0 2rem;
  margin-top: 0.5rem;
}
@media (min-width: 900px) {
  .ap-edit-grid { grid-template-columns: 1fr 1fr; }
  /* Who has it is a list of people with a dropdown each; halving it
     would wrap every name. */
  .ap-edit-grid > .ap-col-wide { grid-column: 1 / -1; }
}
/* ⚠️ min-width: 0 on a grid child. Without it a long profile name or an
   unbroken email in the members list pushes its track wider than the
   column and the whole editor scrolls sideways. */
.ap-col { min-width: 0; }
/* The first heading in a column has the column's own top edge above it,
   so it does not need the separation the ones below it do. */
.ap-col > h4:first-child { margin-top: 0; }

/* The Advanced Setup opener. It was an inline style on the wrapper div;
   it needs a class so the row can be hidden until at least one access
   profile is offered on the signup form. */
.register-advanced-row { margin: 1rem 0; text-align: center; }

/* ── Admin -> Billing -> Plans ──────────────────────────────────────
   The pricing catalogue editor. Every colour is a token, so the panel
   follows the palette and both themes like everything else. */
.apl-card { display: flex; flex-direction: column; gap: 0.75rem; }
.apl-slug { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.85em; }
.apl-h { margin: 0.25rem 0 0; font-size: 0.95rem; color: var(--c-text-muted); }
.apl-price-warn { margin: 0; }

/* Two columns from 720px. ⚠️ `min-width: 0` on the children or a long
   placeholder pushes a track wider than the card and the panel scrolls
   sideways -- the composer rows' rule. */
.apl-grid { display: grid; grid-template-columns: 1fr; gap: 0.6rem 1rem; }
.apl-grid > * { min-width: 0; }
@media (min-width: 720px) { .apl-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.apl-field { display: flex; flex-direction: column; gap: 0.2rem; }
.apl-field input, .apl-field select { width: 100%; }
.apl-label { font-size: 0.85rem; font-weight: 500; color: var(--c-text-muted); }

.apl-flags { display: flex; flex-wrap: wrap; gap: 1rem; }
.apl-check { display: flex; align-items: center; gap: 0.4rem; font-size: 0.9rem; }

.apl-features { display: flex; flex-direction: column; gap: 0.5rem; }
.apl-feature-list { display: flex; flex-direction: column; gap: 0.35rem; }
.apl-feature-row { display: flex; gap: 0.4rem; align-items: center; }
.apl-feature-row input { flex: 1 1 auto; min-width: 0; }
.apl-add { align-self: flex-start; }

.apl-tokens { display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: center; }
.apl-token {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.8rem; padding: 0.15rem 0.45rem;
  border: 1px solid var(--c-border); border-radius: 4px;
  background: var(--c-surface-subtle); color: var(--c-text);
}
.apl-token:hover { background: var(--c-brand-tint); border-color: var(--c-brand); }

.apl-actions { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }

/* Pages panel: the two slug namespaces (site pages vs the help centre) */
.sp-sections { display: flex; gap: 0.4rem; margin: 0.5rem 0; }
.sp-section {
  padding: 0.3rem 0.75rem; font-size: 0.9rem;
  border: 1px solid var(--c-border); border-radius: 6px;
  background: var(--c-surface); color: var(--c-text-muted); font-weight: 500;
}
.sp-section.active { background: var(--c-brand-tint); border-color: var(--c-brand); color: var(--c-text); }
.sp-new-info { display: flex; gap: 0.4rem; flex-wrap: wrap; margin: 0.5rem 0; }
.sp-new-info input { flex: 1 1 12rem; min-width: 0; }

/* ── a picture on a contact (migration 154) ──────────────────────────
   The thumbnail sits beside its buttons rather than above them: a
   picture field is one row in a column of fields, and a stacked frame
   would make it three times the height of everything around it.

   ⚠️ Every colour is a token, so this follows the palette and dark mode.
   The frame's ground is --c-surface-subtle, NOT a literal white: a
   transparent PNG on a white square that stays white in dark mode is the
   `--c-surface` / `--c-on-accent` mix-up this stylesheet already records
   at length. */
.cs-image-row { display: flex; gap: 0.6rem; align-items: flex-start; }
.cs-image-frame {
  width: 4.5rem; height: 4.5rem; flex: 0 0 auto;
  border: 1px solid var(--c-border); border-radius: 0.4rem;
  background: var(--c-surface-subtle);
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
}
/* `contain`, never `cover`: a brand logo cropped to a square is a logo
   somebody cannot recognise, and this frame holds logos as often as it
   holds faces. */
.cs-image-frame img { width: 100%; height: 100%; object-fit: contain; }
.cs-image-none { font-size: 0.62rem; color: var(--c-text-muted); text-align: center; }
.cs-image-actions {
  display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center;
  /* Or a long refusal from the carrier's own words pushes the row wider
     than the sheet's column. */
  min-width: 0; flex: 1 1 0;
}
.cs-image-actions small { flex: 1 1 100%; font-size: 0.62rem; }
.cs-image-error { flex: 1 1 100%; font-size: 0.62rem; color: var(--c-danger); }
.manage-field-avatar label {
  display: inline-flex; align-items: center; gap: 0.3rem;
  font-size: 0.72rem; white-space: nowrap;
}

/* ── site pages in the built-in header (migration 155) ───────────────
   Two renderings of one list: a row on desktop, a <details> disclosure
   on a phone. Both are emitted by appMap.withHeaderPages from the same
   array, and only one is ever visible.

   ⚠️ EVERY COLOUR HERE IS A CHROME TOKEN, NOT A SURFACE ONE. The header
   is dark in BOTH modes -- it is `--c-inverse-surface` with
   `--c-on-accent` text -- so a menu panel painted with `--c-surface`
   would be a white box hanging off a navy bar in dark mode, which is
   exactly the `--c-surface` / `--c-on-accent` mix-up this stylesheet
   already records at length. The panel uses the same pair the bar does. */
.site-header-pages {
  /* What actually puts them top right. */
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 1.25rem;
}

.site-header-pages a,
.site-header-menu-panel a {
  color: inherit;
  text-decoration: none;
  font-size: 0.85rem;
  font-weight: 600;
  /* Quieter than the brand, which is the thing being navigated FROM. */
  opacity: 0.85;
}

.site-header-pages a:hover,
.site-header-pages a:focus-visible,
.site-header-menu-panel a:hover,
.site-header-menu-panel a:focus-visible {
  opacity: 1;
  text-decoration: underline;
}

/* The disclosure. Hidden on desktop, and the row is hidden on a phone --
   they swap at one breakpoint so both can never be on screen at once. */
.site-header-menu {
  display: none;
  position: relative;
  margin-left: auto;
}

.site-header-menu > summary {
  /* Both, because Safari uses its own pseudo-element for the marker and
     `list-style: none` alone leaves the triangle there. */
  list-style: none;
  cursor: pointer;
  padding: 0.25rem 0.4rem;
  font-size: 1.3rem;
  line-height: 1;
  border-radius: 0.3rem;
}

.site-header-menu > summary::-webkit-details-marker { display: none; }
.site-header-menu > summary:focus-visible { outline: 2px solid var(--c-on-dark-brand); outline-offset: 2px; }

.site-header-menu-panel {
  position: absolute;
  right: 0;
  top: calc(100% + 0.45rem);
  min-width: 11rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.4rem;
  border-radius: 0.45rem;
  background: var(--c-inverse-surface);
  border: 1px solid var(--c-chrome-border);
  box-shadow: 0 10px 24px color-mix(in srgb, var(--c-shadow) 45%, transparent);
  /* Above the hero, which is the next thing down and also dark. */
  z-index: 60;
}

.site-header-menu-panel a { padding: 0.45rem 0.55rem; border-radius: 0.3rem; }
.site-header-menu-panel a:hover,
.site-header-menu-panel a:focus-visible {
  background: color-mix(in srgb, var(--c-highlight) 12%, transparent);
  text-decoration: none;
}

@media (max-width: 720px) {
  .site-header-pages { display: none; }
  .site-header-menu { display: block; }
}

/* ── calendar task assignees (migration 157) ─────────────────────────
   ⚠️ Tokens only, and no fixed-dark ones: this sits inside the ordinary
   app surface, not the header chrome, so --c-surface and --c-text are
   the right pair here and the chrome family would be wrong. */
.cal-assign-list { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.3rem; }

.cal-assign-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.3rem 0.6rem;
  border: 1px solid var(--c-border);
  border-radius: 999px;
  background: var(--c-surface);
  font-size: 0.78rem;
  cursor: pointer;
  /* Or a long name wraps the chip onto two lines and the row loses its
     shape. */
  white-space: nowrap;
}

.cal-assign-chip.is-on {
  border-color: var(--c-brand);
  background: var(--c-brand-tint);
}

/* The name in the Tasks table. Not a control -- it is a fact about the
   row -- so it is quieter than the chip somebody clicks. */
.cal-who-chip {
  display: inline-block;
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
  background: var(--c-surface-subtle);
  border: 1px solid var(--c-border);
  font-size: 0.72rem;
  white-space: nowrap;
}

.cal-task-who { max-width: 16rem; }

/* "Filled in from your account" above the ticket form. Muted and small:
   it explains a convenience, and a signed-in buyer should notice their
   details are already there rather than be told about it in a banner.
   Uses the same tokens as .ev-fact-sub so it follows the palette. */
.ev-signed-in {
  margin: 0 0 0.75rem;
  font-size: 0.82rem;
  line-height: 1.4;
  color: var(--c-text-muted, #64748b);
}

/* ── Event sales & earnings ──────────────────────────────────────────
   The organiser's revenue panel. Every colour is a token so it follows
   the admin's palette and dark mode; `--c-text` and `--c-text-muted`
   are the ordinary page pair here, since this sits on a normal card
   rather than on the dark chrome. */
.ev-sales-row {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 1rem; padding: 0.3rem 0; font-size: 0.86rem;
}
.ev-sales-label { color: var(--c-text-muted, #64748b); }
.ev-sales-value { font-variant-numeric: tabular-nums; color: var(--c-text, #0f172a); }
/* Gross and net are the two figures somebody actually reads; the
   deductions between them are context. */
.ev-sales-gross .ev-sales-value,
.ev-sales-net .ev-sales-value { font-weight: 600; }
.ev-sales-net {
  border-top: 1px solid var(--c-border, #e2e8f0);
  margin-top: 0.35rem; padding-top: 0.5rem;
}
.ev-sales-net .ev-sales-value { font-size: 1.05rem; }
.ev-sales-paid .ev-sales-value { color: var(--c-success-text, #166534); }
/* ⚠️ Amber, not red. Money that has not been credited YET is a pending
   state with a button beside it, not a failure -- the same call the
   A2P "pending" state makes against "rejected". */
.ev-sales-owed .ev-sales-value { color: var(--c-warning-text, #92400e); font-weight: 600; }
.ev-sales-types { margin-top: 0.75rem; }

/* The "keep my ticket in an account" offer on the ticket form. A real
   checkbox with the sentence beside it, not a bare label: the box is the
   consent and the sentence is what is being consented to. Removed
   entirely for a signed-in buyer, who already has the account. */
.ev-account-opt {
  display: flex; align-items: flex-start; gap: 0.55rem;
  margin: 0.65rem 0 0; font-size: 0.8rem; line-height: 1.45;
  color: var(--c-text-muted, #64748b); cursor: pointer;
}
.ev-account-opt input { margin-top: 0.15rem; flex: none; }

/* ── Event → Manage popout ────────────────────────────────────────────
   The sales and door view opened from a row's Manage button.

   ⚠️ `.modal` caps at 400px, which is right for a confirm and too narrow
   for a figures table above a list of names. Written as `.modal.ev-manage`
   (0,2,0) rather than `.ev-manage` (0,1,0) so it beats that cap on
   SPECIFICITY and not on source order -- the trap that kept the carrier
   grid stacked and the event page at 900px.

   ⚠️ It scrolls ITSELF. The overlay is position: fixed, so a modal that
   simply grows runs off the bottom of the window with the attendees under
   it unreachable. */
.modal.ev-manage {
  max-width: min(640px, 92vw);
  max-height: 86vh;
  overflow-y: auto;
}

.ev-manage .modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.ev-manage .modal-head h3 { margin: 0; }

/* A section heading inside the popout, not a page heading. */
.ev-manage-head {
  margin: 1.5rem 0 0.5rem;
  font-size: 0.95rem;
  color: var(--c-text-muted);
}

.ev-manage .coms-row-meta { margin-bottom: 0.75rem; }

/* ── The event page's schedule ─────────────────────────────────────────
   Rendered by src/services/eventPages.js, grouped by day.

   ⚠️ A VIEWPORT query is the right question HERE, unlike the embeddable
   form's rows: this is our own page at a width we can see, not a snippet
   pasted into a column of unknown width on somebody else's site. */
.ev-schedule { margin-top: 2.25rem; }

.ev-sched-head {
  font-size: 1.15rem;
  margin: 0 0 0.25rem;
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  flex-wrap: wrap;
}

.ev-sched-zone {
  font-size: 0.8rem;
  font-weight: 400;
  color: var(--c-text-muted);
}

.ev-sched-day { margin-top: 1.25rem; }

.ev-sched-date {
  font-size: 0.95rem;
  margin: 0 0 0.5rem;
  color: var(--c-text-muted);
}

.ev-sched-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.5rem;
}

.ev-sched-item {
  display: grid;
  grid-template-columns: 8.5rem minmax(0, 1fr);
  gap: 1rem;
  padding: 0.75rem 0.9rem;
  border: 1px solid var(--c-border);
  border-radius: 8px;
  background: var(--c-surface);
}

/* ⚠️ min-width: 0 on the body column, or a long unbroken description
   pushes the row wider than the page -- the composer rows' rule. */
.ev-sched-body { min-width: 0; }

.ev-sched-time { font-weight: 600; }

.ev-sched-title { margin: 0; font-size: 0.98rem; }

.ev-sched-who {
  margin: 0.15rem 0 0;
  font-size: 0.9rem;
  color: var(--c-text);
}

.ev-sched-where,
.ev-sched-desc {
  margin: 0.15rem 0 0;
  font-size: 0.88rem;
  color: var(--c-text-muted);
}

/* A practitioner placement is the thing people scan this list for. */
.ev-sched-practitioner { border-left: 3px solid var(--c-brand); }
.ev-sched-break { opacity: 0.85; }

@media (max-width: 34rem) {
  .ev-sched-item { grid-template-columns: minmax(0, 1fr); gap: 0.25rem; }
}

/* The day heading inside the builder's list and the Manage popout. */
.ev-sched-date-sm {
  margin: 0.9rem 0 0.35rem;
  font-weight: 600;
  font-size: 0.82rem;
  color: var(--c-text-muted);
}

/* The event builder's date readout — what is actually stored, in words.

   ⚠️ Not a success colour until it is confirmed: the line is shown for
   every value, and painting an unconfirmed one green would make Select
   look as though it had already been pressed. */
.ev-date-readout {
  margin: 0.15rem 0 0.6rem;
  font-size: 0.85rem;
  color: var(--c-text-muted);
  min-height: 1em;
}

.ev-date-readout.is-set {
  color: var(--c-success-text);
  font-weight: 600;
}

/* The shared date-and-time picker (js/dateTimePicker.js).

   ⚠️ The minutes are a LIST, not a spinner: `step` on a native
   datetime-local constrains the arrows and the browser's validation and
   does not stop a typed 4:03, which is why this exists at all. */
.modal.dtp-modal { max-width: min(380px, 92vw); }

.dtp-modal label {
  display: block;
  margin: 0.75rem 0 0.25rem;
  font-size: 0.85rem;
  font-weight: 600;
}

.dtp-modal select,
.dtp-modal input[type="date"] { width: 100%; }

.dtp-actions {
  display: flex;
  gap: 0.5rem;
  margin-top: 1.25rem;
}

.dtp-warn {
  margin: 0.5rem 0 0;
  min-height: 1em;
  font-size: 0.85rem;
  color: var(--c-danger);
}

/* ─────────────────────────────────────────────────────────────────────
   Account -> <access profile>: the member's own membership panel.

   ⚠️ Every colour is a token, so the panel follows the palette an admin
   set and both light and dark. A literal here is the thing t-dark-mode
   sweeps for. */
/* ── "Your access level", on Account Type & Access ────────────────────
   Read-only by construction: there is no control in this block, because
   an account cannot move its own profile and a disabled one would read
   as something you could enable. */
.acct-access {
  margin: 0 0 1.4rem;
  padding: 0.9rem 1.1rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-left: 3px solid var(--c-brand, #6366f1);
  border-radius: 0.65rem;
  background: var(--c-surface-subtle, #f8fafc);
}

.acct-access-title,
.acct-type-title { margin: 0 0 0.5rem; font-size: 0.95rem; }
.acct-type-title { margin-top: 0.5rem; }

.acct-access-head {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  margin-bottom: 0.6rem;
}

.acct-access-icon { font-size: 1.25rem; line-height: 1; }
.acct-access-name { font-size: 1.05rem; }

.acct-access-fact {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  font-size: 0.82rem;
  margin-bottom: 0.2rem;
}

.acct-access-fact-label { color: var(--c-text-muted, #64748b); min-width: 8.5rem; }
.acct-access-fact-value { color: var(--c-text, #0f172a); font-weight: 600; }

.acct-access-note { margin: 0.7rem 0 0.3rem; font-size: 0.82rem; }

.acct-access-hides {
  margin: 0 0 0.3rem;
  padding-left: 1.1rem;
  font-size: 0.82rem;
  color: var(--c-text, #0f172a);
}

.acct-access-who {
  margin: 0.6rem 0 0;
  font-size: 0.78rem;
  color: var(--c-text-muted, #64748b);
}

.acct-access-link {
  display: inline-block;
  margin-top: 0.55rem;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--c-brand-deep, #4338ca);
}

@media (max-width: 640px) {
  /* The label column is meant for a desktop row; at this width it just
     squeezes the value it labels. */
  .acct-access-fact { flex-direction: column; gap: 0.1rem; }
  .acct-access-fact-label { min-width: 0; }
}

.mb-summary {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
  gap: 1rem;
  margin: 1rem 0 1.5rem;
}
.mb-fact {
  /* ⚠️ min-width: 0 -- a long profile name or form title would otherwise
     refuse to shrink below its own text and push the grid wider than the
     panel, which is the composer rows' rule. */
  min-width: 0;
  padding: 0.85rem 1rem;
  border: 1px solid var(--c-border);
  border-radius: 10px;
  background: var(--c-surface-subtle);
}
.mb-fact-label {
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--c-text-muted);
}
.mb-fact-value {
  font-weight: 600;
  font-size: 1.02rem;
  color: var(--c-text);
  margin-top: 0.15rem;
  /* A profile name is typed by an admin and can be long. */
  overflow-wrap: anywhere;
}
.mb-fact-note {
  font-size: 0.78rem;
  color: var(--c-text-muted);
  margin-top: 0.3rem;
}
.mb-app-head { align-items: center; }
.mb-field { margin-bottom: 1rem; }
.mb-field-label {
  display: block;
  font-weight: 600;
  font-size: 0.88rem;
  margin-bottom: 0.3rem;
  color: var(--c-text);
}
.mb-field-input {
  width: 100%;
  max-width: 34rem;
  box-sizing: border-box;
}
/* A value that cannot be edited from here renders as a SENTENCE, never a
   disabled input -- a greyed-out box still reads as something you could
   enable, which is the rule the locked A2P mobile and the campaign
   Status dropdown both follow. */
.mb-field-static {
  padding: 0.5rem 0.7rem;
  border: 1px dashed var(--c-border);
  border-radius: 8px;
  background: var(--c-surface-subtle);
  max-width: 34rem;
}
.mb-field-note { margin-top: 0.25rem; }

/* The profile's own field builder, in the access-profile editor. */
.ap-field-new {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.75rem;
  padding: 0.75rem;
  border: 1px dashed var(--c-border);
  border-radius: 10px;
  background: var(--c-surface-subtle);
}
/* ⚠️ min-width: 0 on the text inputs, or a long placeholder refuses to
   shrink and the row pushes the column wider than the editor. */
.ap-field-new input[type="text"] { flex: 1 1 12rem; min-width: 0; }
.ap-field-new select { flex: 0 0 auto; }
.ap-field-check {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.82rem;
  color: var(--c-text-muted);
  white-space: nowrap;
}
.ap-field-check input { margin: 0; }
/* A required marker on the member's side. Danger, because it is the one
   thing that stops a save going through. */
.mb-req { color: var(--c-danger); }
.mb-field-check { width: auto; }

/* Fees: the admin's list, and the member's. */
.ap-fee-amount { flex: 0 0 8rem; min-width: 0; }
/* ⚠️ A switched-off fee is DIMMED, never hidden. It stays on the record
   and stops being due, and hiding it would make a fee somebody paid
   against look as though it never existed. */
.ap-fee-off { opacity: 0.6; }
/* ⚠️ Amber, never the danger colour: payments being off is the shipped
   state and a deliberate one, not a fault. */
.ap-fees-off { color: var(--c-warning); }

.mb-fee {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.85rem 1rem;
  border: 1px solid var(--c-border);
  border-radius: 10px;
  background: var(--c-surface-subtle);
  margin-bottom: 0.75rem;
}
.mb-fee-main { flex: 1 1 16rem; min-width: 0; }
.mb-fee-title { display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap; }
.mb-fee-amount { font-weight: 600; color: var(--c-text); }
/* Settled is muted rather than green: it is a statement of fact, not a
   congratulation, and the row still has to be readable. */
.mb-fee-settled { opacity: 0.75; }
.mb-fee-actions { flex: 0 0 auto; }
#mb-card-container { max-width: 26rem; margin: 0.5rem 0; }

/* The snippet's own state line: generated, or hand-edited.
   ⚠️ AMBER, never the danger colour. A hand-edited snippet is a
   deliberate choice and a supported one — it is not a fault, it is a
   fact somebody needs to keep in mind. */
.sfm-snip-state {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin: 0.5rem 0;
  padding: 0.6rem 0.8rem;
  border-radius: 8px;
  font-size: 0.85rem;
  line-height: 1.45;
  border: 1px solid var(--c-warning);
  background: var(--c-warning-tint);
  color: var(--c-text);
}
/* While the box is open for editing it is the brand colour, not amber:
   nothing has forked yet — Cancel still puts everything back. */
.sfm-snip-state.is-editing {
  border-color: var(--c-brand-border);
  background: var(--c-brand-tint);
}
/* ⚠️ An editable snippet LOOKS editable. A readonly textarea and an open
   one that are painted identically is how somebody types into the wrong
   one and loses it. */
#sfm-snippet.is-editing, #wfm-snippet.is-editing {
  background: var(--c-surface);
  border-color: var(--c-brand);
  box-shadow: 0 0 0 2px var(--c-brand-tint);
}
/* The Copy button while it is saying so. Success, because it did. */
.panel-head-actions button.is-copied {
  border-color: var(--c-success-border);
  background: var(--c-success-tint);
  color: var(--c-success-text);
}

/* ── "Where these leads came from" (174) ────────────────────────────
   A disclosure above the submissions list. It is a <details>, so the
   collapsing is the browser's and there is no open/closed state in JS
   to fall out of step with the arrow.

   ⚠️ `flex: 0 0 auto` like the pager beside it: the rows list is the
   flex child that scrolls, and a summary allowed to grow would take
   the room the list needs. */
.sfm-sources {
  flex: 0 0 auto;
  margin: 0 0 0.75rem;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--c-border);
  border-radius: 8px;
  background: var(--c-surface-subtle);
}
.sfm-sources > summary {
  cursor: pointer;
  font-weight: 600;
  font-size: 0.95rem;
}
/* ⚠️ Its own scroll, capped. A form with twenty campaigns would
   otherwise push the list of submissions off the bottom of the
   dialog -- which is the thing somebody opened. */
.sfm-sources-body { max-height: 16rem; overflow-y: auto; }
/* The first heading inside it sits under the summary, so it does not
   need the full top margin `.sfm-sub-h` carries between sections. */
.sfm-sources-body .sfm-sub-h:first-child { margin-top: 0.5rem; }

/* ── Workflows: the vertical flow builder ─────────────────────────────
   ⚠️ EVERY SURFACE, BORDER AND INK IS A TOKEN, declared in all three
   theme blocks, so this follows the palette an admin sets. The ONE thing
   that is not is --wf-step-color: a per-step-type hue set inline by
   workflows.js. It is a semantic colour system (purple = trigger, amber
   = wait, orange = branch) the way the four knowledge-base categories
   are, not chrome -- repainting it with the brand would make every step
   the same colour and lose the thing it is for. */
.wf-plan-note { margin: 0 0 0.75rem; }
.wf-empty { text-align: center; padding: 2rem 1rem; }
.wf-empty p { margin: 0 0 0.5rem; }

/* ── the list ── */
.wf-card {
  border: 1px solid var(--c-border);
  border-radius: 8px;
  padding: 1rem;
  margin-bottom: 0.75rem;
  background: var(--c-surface-subtle);
}
.wf-card-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.wf-card p { margin: 0.35rem 0 0; }
.wf-stats { font-size: 0.85rem; color: var(--c-text-muted); }
.wf-state { font-size: 0.8rem; font-weight: 600; white-space: nowrap; }
.wf-state.is-on { color: var(--c-success); }
.wf-state.is-off { color: var(--c-text-muted); }
.wf-card-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.75rem; }

.wf-template-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
  gap: 0.75rem;
}
.wf-template {
  border: 1px solid var(--c-border);
  border-radius: 8px;
  padding: 0.9rem;
  background: var(--c-surface);
}
.wf-template p { margin: 0.35rem 0 0.6rem; }

/* ── the builder ── */
.wf-builder-top { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 0.75rem; }
.wf-status-pill {
  font-size: 0.75rem; font-weight: 600; padding: 0.2rem 0.65rem; border-radius: 999px;
  border: 1px solid var(--c-border); color: var(--c-text-muted); background: var(--c-surface-subtle);
}
.wf-status-pill.is-on { color: var(--c-success); border-color: var(--c-success-border); background: var(--c-success-tint); }
.wf-name-input { font-size: 1.05rem; font-weight: 600; }

/* ⚠️⚠️ THE CANVAS SCROLLS SIDEWAYS; THE LINEAR COLUMN DOES NOT MOVE.
   It was a hard 680px, which is right for a straight list of steps and
   wrong the moment anything branches: two columns inside 680px are 320px
   each, and an If/Then nested inside one of those gets about 150px per
   column, then 70px at the next level. The tree was legible for exactly
   one level.

   Each card still carries `max-width: 680px; margin: 0 auto`, so a
   workflow with no branches looks exactly as it did. A branching one is
   allowed to be wider than the panel and scrolls rather than crushing
   its columns. */
.wf-canvas {
  margin: 1.5rem auto 0;
  max-width: 100%;
  overflow-x: auto;
  /* Room for the focus ring on a card at the edge, which `overflow`
     would otherwise clip. */
  padding-bottom: 0.5rem;
}

/* A step card. The left border is the step type. */
.wf-card-step {
  position: relative;
  max-width: 680px;
  margin: 0 auto;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-left: 4px solid var(--wf-step-color, var(--c-border));
  border-radius: 10px;
  padding: 0.85rem 1.1rem 1rem;
}
.wf-card-trigger { border-left-width: 4px; }
.wf-card-step > .wf-card-head {
  display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
  padding-bottom: 0.6rem; margin-bottom: 0.75rem;
  border-bottom: 1px solid var(--c-border);
}
.wf-card-title { font-weight: 700; font-size: 0.82rem; letter-spacing: 0.04em; color: var(--wf-step-color, var(--c-text)); }
.wf-type-select {
  font: inherit; font-weight: 600; font-size: 0.9rem;
  border: 1px solid transparent; background: transparent; color: var(--c-text);
  border-radius: 6px; padding: 0.2rem 0.3rem; max-width: 70%;
}
.wf-type-select:hover, .wf-type-select:focus { border-color: var(--c-border); background: var(--c-surface-subtle); }
.wf-card-remove {
  font: inherit; line-height: 1; padding: 0.2rem 0.45rem; cursor: pointer;
  border: 1px solid transparent; border-radius: 6px;
  background: transparent; color: var(--c-text-muted);
}
.wf-card-remove:hover:not(:disabled) { color: var(--c-danger); border-color: var(--c-danger-border); background: var(--c-danger-tint); }

/* Move a step up or down its own path.

   ⚠️ SAME SHAPE AS THE REMOVE BUTTON BESIDE IT, and quieter: these are
   used far more often than ✕ and should not read as the loudest thing in
   the header. The type dropdown is capped at 70% width, which leaves the
   room for three. */
.wf-card-move {
  font: inherit; line-height: 1; padding: 0.2rem 0.4rem; cursor: pointer;
  border: 1px solid transparent; border-radius: 6px;
  background: transparent; color: var(--c-text-muted);
}
.wf-card-move:hover:not(:disabled) {
  color: var(--c-text); border-color: var(--c-border); background: var(--c-surface-subtle);
}
/* ⚠️ DISABLED AT THE ENDS, NOT HIDDEN. A control that disappears at the
   top of a list makes every card a different shape and moves the buttons
   under the pointer as somebody uses them. */
.wf-card-move:disabled { opacity: 0.3; cursor: default; }
.wf-card-move:focus-visible { outline: 2px solid var(--c-brand); outline-offset: 1px; }
.wf-card-body > .wf-field:last-child { margin-bottom: 0; }
.wf-note { margin: 0.4rem 0 0; font-size: 0.8rem; line-height: 1.45; color: var(--c-text-muted); }
.wf-count { display: block; margin-top: 0.3rem; font-size: 0.75rem; color: var(--c-text-muted); }

/* ── the connector between two cards ── */
.wf-connector { position: relative; height: 38px; display: flex; align-items: center; justify-content: center; }
.wf-connector-line {
  position: absolute; top: 0; bottom: 0; left: 50%; width: 2px; margin-left: -1px;
  background: var(--c-border);
}
/* ⚠️ The arrowhead is a pseudo-element on the LINE, so it cannot be
   selected, copied or read out -- it is decoration, and the connector's
   meaning is carried by the button's own label. */
.wf-connector-line::after {
  content: ''; position: absolute; left: 50%; bottom: -1px; transform: translateX(-50%);
  border-left: 5px solid transparent; border-right: 5px solid transparent;
  border-top: 6px solid var(--c-border);
}
.wf-connector.is-tail .wf-connector-line { bottom: 50%; }
.wf-connector.is-tail .wf-connector-line::after { display: none; }
.wf-add-btn {
  position: relative; z-index: 1;
  font: inherit; font-size: 0.72rem; line-height: 1;
  padding: 0.25rem 0.6rem; cursor: pointer;
  border: 1px solid var(--c-border); border-radius: 5px;
  background: var(--c-surface); color: var(--c-text-muted);
  opacity: 0.55; transition: opacity 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
.wf-connector:hover .wf-add-btn, .wf-add-btn:focus-visible { opacity: 1; color: var(--c-brand-deep); border-color: var(--c-brand); }
/* ⚠️ Always fully visible on a touch screen: there is no hover to reveal
   it with, and a control at 55% opacity reads as disabled. */
@media (hover: none) { .wf-add-btn { opacity: 1; } }

/* ── branching ── */
.wf-branches { margin-top: 0.9rem; }
.wf-fork { height: 22px; position: relative; }
.wf-fork::before {
  content: ''; position: absolute; top: 0; left: 50%; width: 2px; height: 11px;
  margin-left: -1px; background: var(--c-border);
}
.wf-fork::after {
  content: ''; position: absolute; top: 10px; left: 25%; right: 25%; height: 12px;
  border-top: 2px solid var(--c-border);
  border-left: 2px solid var(--c-border);
  border-right: 2px solid var(--c-border);
  border-radius: 6px 6px 0 0;
}
/* ⚠️⚠️ COLUMNS THAT KEEP THEIR WIDTH. `flex: 1 1 0` with `min-width: 0`
   told every branch to shrink to whatever was left -- which is how a
   nested If/Then ended up 150px wide with its own two columns inside it.
   `flex: 0 0 auto` sizes a branch to its CONTENT, so a branch containing
   another fork grows to hold it and the canvas scrolls; a branch holding
   one send step stays narrow.

   ⚠️ THE max-width IS GONE ON PURPOSE. It was the ceiling that made a
   nested fork impossible: a column cannot be 320px wide AND contain two
   260px columns. `min-width` is the floor that matters now -- nothing
   ever renders narrower than a readable card. */
.wf-branch-cols { display: flex; gap: 1.5rem; align-items: flex-start; }
.wf-branch { flex: 0 0 auto; min-width: 260px; }

/* ⚠️ A CARD INSIDE A BRANCH IS NOT CENTRED ON 680px. The cards carry
   `margin: 0 auto; max-width: 680px` for the linear case; inside a
   column that measures its own content, an auto margin makes every card
   the width of the widest one and centres the rest, which reads as a
   ragged column. They fill their branch instead. */
.wf-branch .wf-card-step { max-width: none; margin: 0; width: 100%; }
.wf-branch-head {
  font-size: 0.75rem; font-weight: 700; letter-spacing: 0.05em;
  text-align: center; padding: 0.3rem 0; margin-bottom: 0.6rem;
  border-radius: 6px;
}
.wf-branch-yes .wf-branch-head { color: var(--c-success); background: var(--c-success-tint); }
.wf-branch-no .wf-branch-head { color: var(--c-danger); background: var(--c-danger-tint); }
.wf-branch-steps { display: flex; flex-direction: column; gap: 0.5rem; }
.wf-branch .wf-card-step { padding: 0.6rem 0.75rem 0.7rem; }
.wf-branch .wf-connector { height: 26px; }
.wf-add-branch { opacity: 1; align-self: center; }
.wf-empty-path { margin: 0; font-size: 0.8rem; color: var(--c-text-muted); text-align: center; padding: 0.4rem 0; }
/* The two paths coming back together. Drawn because the engine really
   does rejoin -- the YES block carries an explicit jump over the NO
   block -- and it would be a lie otherwise. */
.wf-merge { height: 22px; position: relative; }
.wf-merge::before {
  content: ''; position: absolute; bottom: 11px; left: 25%; right: 25%; height: 12px;
  border-bottom: 2px solid var(--c-border);
  border-left: 2px solid var(--c-border);
  border-right: 2px solid var(--c-border);
  border-radius: 0 0 6px 6px;
}
.wf-merge::after {
  content: ''; position: absolute; bottom: 0; left: 50%; width: 2px; height: 11px;
  margin-left: -1px; background: var(--c-border);
}

/* ── fields ── */
.wf-section-head { margin: 1.5rem 0 0.5rem; font-size: 1rem; }
.wf-field { margin-bottom: 0.7rem; }
.wf-field > label { display: block; margin: 0 0 0.25rem; font-size: 0.82rem; font-weight: 600; color: var(--c-text-muted); }
.wf-input { width: 100%; max-width: 100%; box-sizing: border-box; }
.wf-input-sm { width: auto; min-width: 7rem; }
.wf-textarea { font-family: inherit; resize: vertical; }
.wf-inline { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }

.wf-chips { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.4rem; }
.wf-chip {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.72rem;
  padding: 0.15rem 0.45rem;
  border: 1px solid var(--c-border);
  border-radius: 999px;
  background: var(--c-surface-subtle);
  color: var(--c-text-muted);
  cursor: pointer;
}
.wf-chip:hover:not(:disabled) { border-color: var(--c-brand); color: var(--c-brand-deep); background: var(--c-brand-tint); }

.wf-save-bar { display: flex; gap: 0.5rem; margin-top: 1.5rem; flex-wrap: wrap; }

.wf-exec { border-bottom: 1px solid var(--c-border); padding: 0.6rem 0; }
.wf-exec > summary { cursor: pointer; display: flex; justify-content: space-between; gap: 1rem; }
.wf-exec p { margin: 0.3rem 0; font-size: 0.85rem; }
.wf-exec-log { margin: 0.5rem 0 0; font-size: 0.85rem; }
.wf-log-line { padding: 0.2rem 0; color: var(--c-text-muted); }
.wf-exec-active, .wf-exec-waiting { color: var(--c-warning); }
.wf-exec-completed { color: var(--c-success); }
.wf-exec-failed { color: var(--c-danger); }
.wf-exec-cancelled { color: var(--c-text-muted); }

/* ⚠️ The two branch columns stack under 640px. Two 320px columns plus a
   1.5rem gap needs about 42rem; below that they are unreadable side by
   side, and a flowchart nobody can read is worse than a list. */
@media (max-width: 640px) {
  .wf-card-head { flex-direction: column; align-items: flex-start; gap: 0.35rem; }
  /* On a phone the columns stack: two 260px columns side by side do not
     fit, and a sideways scroll inside a page that already scrolls
     sideways is the worst of both. */
  .wf-branch-cols { flex-direction: column; gap: 0.75rem; }
  .wf-branch { min-width: 0; width: 100%; }
  .wf-fork::after, .wf-merge::before { left: 10%; right: 10%; }
}

/* The "Insert:" label in front of a row of variable chips. */
.wf-chip-label { font-size: 0.78rem; color: var(--c-text-muted); align-self: center; margin-right: 0.15rem; }

/* The variable picker under a message box. Sized to its content so a
   Linxi with long field labels does not stretch the composer. */
.wf-var-select { max-width: min(22rem, 100%); font-size: 0.85rem; }

/* The draft card the workflow assistant offers. ⚠️ Inside the shared
   .campaign-ai-panel, so it inherits that drawer's width and scrolling
   and only needs its own box. */
.wf-ai-draft {
  margin: 0.6rem 0;
  padding: 0.75rem 0.85rem;
  border: 1px solid var(--c-border);
  border-left: 4px solid var(--c-brand);
  border-radius: 8px;
  background: var(--c-surface-subtle);
}
.wf-ai-draft p { margin: 0.25rem 0 0.6rem; font-size: 0.82rem; }
.wf-builder-actions { display: flex; align-items: center; gap: 0.6rem; }

/* The hour picker. ⚠️ Above the modal band (1300) because it is opened
   FROM inside the builder, which can itself sit under a dialogue -- a
   picker that renders behind the thing that opened it is the stacking
   bug this stylesheet already records once. */
.wf-hour-pop {
  position: fixed;
  z-index: 1400;
  width: 16rem;
  max-height: min(22rem, 70vh);
  display: flex;
  flex-direction: column;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: 10px;
  box-shadow: 0 12px 32px color-mix(in srgb, var(--c-shadow) 22%, transparent);
}
.wf-hour-pop-head {
  padding: 0.6rem 0.8rem;
  font-size: 0.8rem; font-weight: 600;
  color: var(--c-text-muted);
  border-bottom: 1px solid var(--c-border);
}
.wf-hour-list { overflow-y: auto; padding: 0.35rem; display: flex; flex-direction: column; gap: 0.1rem; }
.wf-hour-opt {
  font: inherit; font-size: 0.85rem; text-align: left;
  padding: 0.35rem 0.6rem; border-radius: 6px; cursor: pointer;
  border: 1px solid transparent; background: transparent; color: var(--c-text);
}
.wf-hour-opt:hover:not(:disabled) { background: var(--c-surface-subtle); }
.wf-hour-opt.is-on { background: var(--c-brand-tint); color: var(--c-brand-deep); font-weight: 600; }
.wf-hour-pop-bar {
  display: flex; justify-content: flex-end; gap: 0.5rem;
  padding: 0.6rem 0.8rem; border-top: 1px solid var(--c-border);
}
.wf-hour-row { margin-bottom: 0.5rem; }
.wf-check-label { font-size: 0.85rem; color: var(--c-text); cursor: pointer; }
.wf-hour-btn:disabled { opacity: 0.5; cursor: default; }

/* Site Level flows: what Linxi already runs. Read-only, and visibly a
   different kind of thing from the clonable templates above it. */
.wf-site-auto { margin-top: 1.25rem; border-top: 1px solid var(--c-border); padding-top: 0.9rem; }
.wf-site-auto > summary { cursor: pointer; font-size: 0.95rem; }
.wf-site-auto-note { margin: 0.5rem 0 0.75rem; font-size: 0.85rem; }
.wf-site-auto-group {
  margin: 1rem 0 0.4rem; font-size: 0.78rem; font-weight: 700;
  letter-spacing: 0.05em; text-transform: uppercase; color: var(--c-text-muted);
}
.wf-site-auto-item { padding: 0.5rem 0; border-bottom: 1px solid var(--c-border); }
.wf-site-auto-item:last-child { border-bottom: 0; }
.wf-site-auto-item p { margin: 0.2rem 0 0; font-size: 0.83rem; line-height: 1.5; }
.wf-site-auto-head { display: flex; align-items: center; flex-wrap: wrap; gap: 0.4rem; }
.wf-site-auto-pill {
  font-size: 0.68rem; font-weight: 600; padding: 0.08rem 0.45rem; border-radius: 999px;
  border: 1px solid var(--c-border);
}
.wf-site-auto-pill.is-on { color: var(--c-success); background: var(--c-success-tint); border-color: var(--c-success-border); }
.wf-site-auto-pill.is-off { color: var(--c-text-muted); background: var(--c-surface-subtle); }
.wf-site-auto-when { font-size: 0.72rem; color: var(--c-text-muted); }
.wf-site-auto-where { font-size: 0.78rem; color: var(--c-brand-deep); }
/* The Site Level switch and its editor. Only rendered where the caller
   can actually use it -- there is no disabled state here on purpose: a
   greyed-out box reads as something you could enable. */
.wf-site-auto-switch {
  display: flex; align-items: center; gap: 0.45rem;
  margin: 0.4rem 0 0; font-size: 0.83rem; color: var(--c-text); cursor: pointer;
}
.wf-site-auto-switch input { margin: 0; }
.wf-site-auto-locked {
  margin: 0.35rem 0 0; font-size: 0.78rem; color: var(--c-text-muted); font-style: italic;
}
.wf-site-auto-help, .wf-site-auto-fieldnote { font-size: 0.75rem; margin: 0.25rem 0 0; }
.wf-site-auto-editwrap { margin-top: 0.5rem; }
.wf-site-auto-editwrap > summary { cursor: pointer; font-size: 0.8rem; color: var(--c-brand-deep); }
.wf-site-auto-edit {
  margin-top: 0.5rem; padding: 0.6rem 0.75rem;
  background: var(--c-surface-subtle); border: 1px solid var(--c-border); border-radius: 0.5rem;
}
.wf-site-auto-label { display: block; font-size: 0.78rem; font-weight: 600; margin: 0.5rem 0 0.2rem; }
.wf-site-auto-edit input[type="text"], .wf-site-auto-edit textarea {
  width: 100%; font-size: 0.82rem; line-height: 1.5;
}
.wf-site-auto-actions { display: flex; align-items: center; gap: 0.6rem; margin-top: 0.7rem; }
.wf-template-mirror { margin: 0.2rem 0 0; font-size: 0.75rem; color: var(--c-brand-deep); }

/* A second trigger (OR) or a narrowing condition (AND), under the primary. */
.wf-trigger-alt {
  margin-top: 0.6rem; padding: 0.6rem 0.7rem;
  border: 1px dashed var(--c-border); border-radius: 8px;
  background: var(--c-surface-subtle);
}
.wf-trigger-alt-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.4rem; }
.wf-trigger-op { font-size: 0.7rem; font-weight: 700; letter-spacing: 0.08em; padding: 0.1rem 0.45rem; border-radius: 999px; }
.wf-trigger-op.is-or { color: var(--c-brand-deep); background: var(--c-brand-tint); }
.wf-trigger-op.is-and { color: var(--c-warning); background: var(--c-warning-tint); }
.wf-trigger-add { display: flex; gap: 0.5rem; margin-top: 0.7rem; }
.wf-trigger-add .wf-add-btn { opacity: 1; }


/* The Site Footer / Social Footer switch.

   ⚠️ TWO EDITORS IN ONE PANEL, so the switch has to read as "which one am
   I looking at" rather than as two buttons that do something. A segmented
   control with the active half filled is the shape people already read
   that way. */
.sf-switch {
  display: inline-flex;
  margin: 0 0 1.25rem;
  padding: 3px;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 999px;
  background: var(--c-surface-subtle, #f8fafc);
  gap: 3px;
}
.sf-switch-btn {
  border: 0;
  background: none;
  cursor: pointer;
  padding: 0.4rem 1rem;
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--c-text-muted, #64748b);
}
.sf-switch-btn.is-on {
  background: var(--c-surface, #ffffff);
  color: var(--c-text, #0f172a);
  box-shadow: 0 1px 2px color-mix(in srgb, var(--c-shadow, #000000) 10%, transparent);
}
/* ⚠️ ITS OWN HOVER, or the base rule paints the gradient over a control
   that is meant to read as a quiet segmented switch. A tab strip that
   turns indigo under the pointer looks like a button somebody is about
   to press, which is the opposite of what a tab says. */
.sf-switch-btn:hover {
  background: var(--btn-surface-hover, rgba(0, 0, 0, 0.04));
  color: var(--c-text, #0f172a);
  box-shadow: none;
  transform: none;
}
.sf-switch-btn.is-on:hover {
  background: var(--c-surface, #ffffff);
  color: var(--c-text, #0f172a);
}
.sf-switch-btn:focus-visible {
  outline: 2px solid var(--c-brand, #6366f1);
  outline-offset: 2px;
}


/* ⚠️ THE BUILT-IN SOCIAL BAR STANDS DOWN FOR A CUSTOM ONE. siteChrome
   marks the body when it injects a custom social header; without this the
   page would carry two headers, one above the other. The mark is on
   <body> because the bar being hidden is a SIBLING of the injected
   header, and a rule keyed off the injected element could only reach it
   with :has() -- which this codebase records dropping out on older
   engines, where the failure would be two headers rather than none. */
body[data-social-header-custom] > .info-topbar { display: none; }


/* The social header's background image control. */
.shimg-block {
  margin-top: 1.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--c-border, #e2e8f0);
}
.shimg-title { margin: 0 0 0.35rem; font-size: 0.95rem; }
.shimg-row { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem; margin-top: 0.75rem; }
/* ⚠️ A FIXED BOX WITH object-fit, not an <img> left at its natural size.
   The upload is whatever the admin picked -- a 4000px photograph would
   otherwise push the buttons off the panel while it previews. */
.shimg-preview {
  width: 220px;
  height: 72px;
  object-fit: cover;
  border-radius: 0.5rem;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface-subtle, #f8fafc);
}
.shimg-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }

/* The form builder's accent picker: a swatch, the hex, and a way back to
   the site colour. ⚠️ The text box is the value that counts -- a colour
   input cannot be empty, and empty is how a form says "use the site's". */
.sfm-accent-row { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.35rem; }
.sfm-accent-row input[type="color"] {
  width: 2.6rem; height: 2.4rem; padding: 0.15rem; flex: 0 0 auto;
  border: 1px solid var(--c-border); border-radius: 0.5rem;
  background: var(--c-surface); cursor: pointer;
}
.sfm-accent-row input[type="text"] { flex: 1; min-width: 0; margin: 0; font-family: ui-monospace, monospace; }
.sfm-accent-row button { flex: 0 0 auto; }

/* ── The Coms portal toolbar ──────────────────────────────────────────
   Articles and static pages publish from one list, so the list needs a
   way to show one of them, and an order other than last-touched. */
.coms-toolbar {
  display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap;
  margin: 0 0 0.85rem;
}
.coms-toolbar-group {
  display: inline-flex; gap: 0.15rem; padding: 0.2rem;
  background: var(--c-surface-sunken, #f1f5f9);
  border: 1px solid var(--c-border, #e2e8f0); border-radius: 999px;
}
.coms-toolbar-pick {
  background: none; border: none; cursor: pointer;
  padding: 0.35rem 0.85rem; border-radius: 999px;
  font-size: 0.82rem; font-weight: 600; color: var(--c-text-muted, #64748b);
}
.coms-toolbar-pick:hover { color: var(--c-text, #0f172a); }
/* ⚠️ The on-state is a filled pill, not a colour change. A tab row where
   the difference is only weight reads as "none of these is chosen". */
.coms-toolbar-pick.is-on {
  background: var(--c-surface, #ffffff); color: var(--c-text, #0f172a);
  box-shadow: 0 1px 2px color-mix(in srgb, var(--c-shadow, #000) 12%, transparent);
}
.coms-toolbar-sort { display: inline-flex; align-items: center; gap: 0.45rem; margin: 0; }
.coms-toolbar-sort > span { font-size: 0.8rem; font-weight: 600; color: var(--c-text-muted, #64748b); }
.coms-toolbar-sort select { margin: 0; padding: 0.35rem 0.6rem; font-size: 0.82rem; }
.coms-toolbar-count { margin-left: auto; font-size: 0.8rem; color: var(--c-text-dim, #94a3b8); }

/* The stat chips are the status filter. ⚠️ They were <div>s; as <button>s
   they inherit the global button rules, so the ones that would make a
   plain chip look like a submit control are turned off here rather than
   the chip being left as a div nobody can press. */
button.coms-stat {
  cursor: pointer; font: inherit; text-align: left;
  background: var(--c-surface, #ffffff);
  border: 1px solid var(--c-border, #e2e8f0);
  box-shadow: none;
}
button.coms-stat:hover:not(:disabled) {
  background: var(--c-surface-subtle, #f8fafc);
  border-color: var(--c-brand-border, #c7d2fe);
  color: inherit; transform: none;
}
button.coms-stat.coms-stat-on {
  border-color: var(--c-brand, #6366f1);
  background: color-mix(in srgb, var(--c-brand, #6366f1) 8%, transparent);
}
button.coms-stat.coms-stat-on:hover:not(:disabled) {
  background: color-mix(in srgb, var(--c-brand, #6366f1) 12%, transparent);
}
@media (max-width: 620px) {
  .coms-toolbar-count { margin-left: 0; width: 100%; }
}

/* ── A menu row is a menu row ──────────────────────────────────────────
   ⚠️⚠️ SIX OF THE TEN ROWS IN THE USER MENU ARE ALSO .nav-tab-btn, and
   that is deliberate: Coms, AI, Account, Master View and Partner carry
   `data-tab` so initTabs() does the switching with no extra JavaScript.
   The side effect is that they arrive wearing the NAV STRIP's clothes,
   which are cut for a dark chrome bar, and they beat the menu's own rules
   on specificity:

     .nav-tab-btn:hover:not(:disabled)   (0,3,0)  wins
     .user-menu-item:hover               (0,2,0)  loses

   So hovering one of those six set `color: var(--c-on-accent)` -- WHITE
   TEXT on the menu's pale grey hover -- while the other four went dark
   text on grey. `a:where(.nav-tab-btn):hover` added a lift and a shadow
   on top, so the anchors rose a pixel and the two <button> rows (My
   Requests, Log Out) did not. And `.nav-tab-btn.active` paints
   `var(--btn-grad)` with white text, so whichever tab you were ON showed
   as a gradient pill inside a white dropdown.

   Four different hover behaviours in one ten-row menu.

   ⚠️ SCOPED TO .user-menu-dropdown AND CARRYING :not(:disabled), which
   puts it at (0,4,0) -- above .nav-tab-btn:hover:not(:disabled) at
   (0,3,0) and above .nav-tab-btn.active:hover at (0,3,0) -- rather than
   relying on being later in the file. The nav strip itself is untouched:
   every rule here needs a .user-menu-dropdown ancestor, and the strip has
   none.

   ⚠️ AND IT FIXES THE MENU RATHER THAN REMOVING THE CLASS. Dropping
   .nav-tab-btn from those six anchors would take `data-tab` routing with
   it -- initTabs() and restoreActiveTab both select on that class. */
.user-menu-dropdown .user-menu-item,
.user-menu-dropdown .user-menu-item.active {
  background: none;
  border: none;
  box-shadow: none;
  transform: none;
  color: var(--c-ink, #1a1a1a);
  font-weight: 500;
  letter-spacing: normal;
}
.user-menu-dropdown .user-menu-item-danger,
.user-menu-dropdown .user-menu-item-danger.active {
  color: var(--c-danger-strong, #dc2626);
}
/* ⚠️ The ACTIVE row is marked by weight, not by a filled pill. A menu you
   opened from the page you are already on should say so quietly; a
   gradient row in a white dropdown reads as the selected item of a
   different control. */
.user-menu-dropdown .user-menu-item.active {
  font-weight: 700;
}
.user-menu-dropdown .user-menu-item:hover:not(:disabled),
.user-menu-dropdown .user-menu-item.active:hover:not(:disabled),
.user-menu-dropdown .user-menu-item:focus-visible {
  background: var(--c-page, #f4f5f7);
  border-color: transparent;
  box-shadow: none;
  transform: none;
  color: var(--c-ink, #1a1a1a);
}
.user-menu-dropdown .user-menu-item-danger:hover:not(:disabled),
.user-menu-dropdown .user-menu-item-danger:focus-visible {
  background: color-mix(in srgb, var(--c-danger-strong, #dc2626) 8%, transparent);
  color: var(--c-danger-strong, #dc2626);
}
/* ⚠️ A VISIBLE RING FOR KEYBOARD USERS. The rule above turns off the nav
   strip's focus shadow, and focus that looks exactly like hover is no
   focus indicator at all. */
.user-menu-dropdown .user-menu-item:focus-visible {
  outline: 2px solid var(--c-brand, #6366f1);
  outline-offset: -2px;
}
/* The emoji glyphs sit at different optical weights across the rows; one
   width keeps the labels on a single left edge. */
.user-menu-dropdown .user-menu-item > span[aria-hidden="true"] {
  display: inline-block;
  width: 1.25em;
  font-style: normal;
}

/* ── The Coms builder header, and its collapsible details column ──────
   ⚠️ SCOPED TO .coms-builder, which #ev-builder also carries -- but the
   toggle and the link exist only in #coms-builder's markup, so
   `.side-collapsed` is never on the events one and these rules never
   apply to it. */
.coms-builder-head-left { display: flex; align-items: center; gap: 0.6rem; min-width: 0; }
.coms-builder-head-left h3 { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.coms-side-toggle {
  flex: 0 0 auto;
  padding: 0.3rem 0.6rem;
  font-size: 0.78rem; font-weight: 600;
}
/* ⚠️ The link is an <a> wearing a button's clothes, so it needs the two
   things an anchor does not get from them. */
a.coms-view-link {
  display: inline-flex; align-items: center;
  text-decoration: none; box-sizing: border-box;
}

/* ⚠️⚠️ THE COLUMN IS REMOVED FROM THE GRID, not merely narrowed. A 0-width
   track still lays its contents out and still runs their overflow, so the
   inputs inside kept a scrollbar and the focus order still walked through
   fourteen invisible controls. `display: none` is what actually takes it
   out of both. */
.coms-builder.side-collapsed .coms-builder-body-2 { grid-template-columns: minmax(0, 1fr); }
.coms-builder.side-collapsed .coms-builder-side { display: none; }

/* ⚠️⚠️ ONLY `.coms-builder-body-2`, AND NOT `.coms-builder-body`. The
   three-pane rule looked like a free generalisation for the events
   builder and is a bug: at (0,3,0) it out-ranks the 780px media query
   that collapses this grid to ONE column on a phone, so a collapsed
   builder would have forced two columns onto a 390px screen. The events
   builder has no toggle, so it never carries this class and never needed
   the rule. */

@media (max-width: 720px) {
  /* The header already wraps here; the toggle is the first thing worth
     dropping, since the column is stacked rather than beside anything. */
  .coms-side-toggle { display: none; }
}

/* ── Version controls in the builder header ───────────────────────── */
.coms-version-box { display: inline-flex; align-items: center; gap: 0.35rem; }
.coms-version-box select {
  margin: 0; padding: 0.3rem 0.5rem; font-size: 0.78rem;
  max-width: 15rem; min-width: 7.5rem;
}
.coms-version-box button { padding: 0.3rem 0.6rem; font-size: 0.78rem; font-weight: 600; }
@media (max-width: 900px) {
  /* ⚠️ The SELECT goes and the button stays. Saving a version is the half
     you reach for mid-edit; reading the history is a desk job. */
  .coms-version-box select { display: none; }
}

/* ── The AI panel on a desktop ────────────────────────────────────────
   ⚠️⚠️ IT IS A PHONE-WIDTH COLUMN ON EVERY SCREEN. The panel is fixed at
   a single width, so on a 1600px monitor the page builder's own AI sat in
   a 25rem strip with a four-row textarea while two thirds of the display
   went unused -- and the brief for a page is the longest thing anybody
   types into this app. It widens with the screen instead, and the brief
   grows with it. */
@media (min-width: 1200px) {
  .coms-ai-panel { width: min(34rem, 42vw); }
  .coms-ai-panel #coms-ai-topic { min-height: 11rem; }
}
@media (min-width: 1600px) {
  .coms-ai-panel { width: min(40rem, 34vw); }
  .coms-ai-panel #coms-ai-topic { min-height: 15rem; }
}
/* ⚠️⚠️ AND NOTHING ELSE. I wrote a block here making the sections into
   cards on a wide panel; `.coms-ai-action` is ALREADY a card -- border,
   radius, padding -- so all it did was swap --c-surface-subtle for
   --c-surface and restyle a panel nobody asked to have restyled. The
   `gap` beside it did nothing at all: .coms-ai-scroll is a block, not a
   flex container. Width and room to type is the actual desktop problem. */

/* ── The AI panel, widened on demand ──────────────────────────────────
   ⚠️ A SECOND STEP ON TOP OF THE RESPONSIVE WIDTH, not a replacement for
   it. The breakpoints above give the panel more room as the screen grows;
   this is the button for the moment somebody is writing a long brief and
   wants most of the monitor for it. */
.coms-ai-panel-wide { width: min(64rem, 78vw) !important; }
.coms-ai-panel-wide #coms-ai-topic { min-height: 20rem; }
/* ⚠️ HIDDEN BELOW 1000px, because there the panel is already effectively
   full width -- a control that visibly does nothing is worse than one
   that is not offered. */
.coms-ai-wide { display: none; }
@media (min-width: 1000px) { .coms-ai-wide { display: inline-flex; } }

/* ── The assistant settings dialogue, on a desktop ────────────────────
   ⚠️⚠️ IT WAS min(46rem, 100%) ON EVERY SCREEN -- a 736px column holding
   seven stacked sections, a fourteen-row textarea and a nine-row, five-
   column pricing table, on a monitor with 1600px to spend. Everything was
   a scroll away from everything else, and the two widest things on the
   screen were the two being squeezed hardest.

   ⚠️ IT IS NOT A REDESIGN. Every section, control and word is where it
   was; the sections flow into two columns and the two that are not
   column-shaped span both. Below 1200px nothing changes at all. */
@media (min-width: 1200px) {
  /* ⚠️ max-width TOO, always. See the base rule: width alone is clamped
     to .modal's 400px and the breakpoint silently does nothing. */
  .ai-spot-modal { width: min(76rem, 95vw); max-width: min(76rem, 95vw);
    max-height: min(92vh, 60rem); }
  .ai-spot-body {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0 1.75rem;
    align-content: start;
  }
  /* ⚠️ THE DIVIDER BECOMES A BORDER ALL ROUND. `border-top` between
     stacked sections reads as a separator; in two columns it reads as a
     line under an unrelated heading, because the section above it in the
     DOM is beside it on screen. */
  .ai-spot-body > .ai-spot-sec {
    padding: 0.9rem 1rem;
    margin-top: 1rem;
    border: 1px solid var(--c-border, #e2e8f0);
    border-radius: 12px;
    min-width: 0;
  }
  .ai-spot-body > .ai-spot-sec-wide { grid-column: 1 / -1; }
  /* The prompt has the width now, so it may as well have the height. */
  .ai-spot-sec-wide textarea { min-height: 20rem; }
  /* ⚠️ The pricing table stops needing its own horizontal scroll at this
     width, which is most of the reason it is a full-width section. */
  .ai-spot-sec-wide table { width: 100%; }
}
@media (min-width: 1650px) {
  .ai-spot-modal { width: min(88rem, 92vw); max-width: min(88rem, 92vw); }
  /* ⚠️ STILL TWO COLUMNS. A third was written here and taken out: there
     are exactly four column-shaped sections, so three columns leaves one
     dangling on its own row while the other two sit beside each other.
     Two columns is 2 + 2, which is the shape the content actually has. */
}

/* ── An attached document on the AI panel ─────────────────────────── */
/* ⚠️ CENTRED, AND WITH ROOM. These sat hard against the textarea above
   and the button below with 0.15rem under them, so three unrelated
   controls read as one run-on strip. Centring them also stops the row
   fighting the three-quarter-width button beneath it for a left edge that
   neither of them owns. */
.coms-ai-attach {
  display: flex; align-items: center; justify-content: center;
  gap: 0.5rem; flex-wrap: wrap;
  margin: 0.85rem 0 0.75rem;
}
/* One height for everything on the row, or a pill button beside a select
   beside a chip is three different vertical centres. */
.coms-ai-attach > * { min-height: 2rem; }
.coms-ai-clip {
  background: none;
  border: 1px dashed var(--c-border, #e2e8f0);
  color: var(--c-text-muted, #64748b);
  border-radius: 999px;
  padding: 0.32rem 0.8rem;
  font-size: 0.78rem; font-weight: 600;
  cursor: pointer;
  box-shadow: none;
}
.coms-ai-clip:hover:not(:disabled) {
  border-color: var(--c-brand, #6366f1);
  color: var(--c-text, #0f172a);
  background: none; transform: none; box-shadow: none;
}
.coms-ai-clip:disabled { opacity: 0.45; cursor: not-allowed; }
.coms-ai-chip {
  display: inline-flex; align-items: center; gap: 0.3rem;
  max-width: 100%;
  padding: 0.28rem 0.3rem 0.28rem 0.7rem;
  border-radius: 999px;
  background: var(--c-brand-tint-2, #e0e7ff);
  color: var(--c-text, #0f172a);
  font-size: 0.78rem; font-weight: 600;
}
/* ⚠️ The filename is whatever somebody called a file on their own disk,
   so it is truncated rather than allowed to push the × off the row. */
.coms-ai-chip-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.coms-ai-chip-x {
  background: none; border: none; cursor: pointer; box-shadow: none;
  color: inherit; opacity: 0.6;
  font-size: 0.72rem; line-height: 1;
  padding: 0.25rem 0.35rem; border-radius: 999px;
}
.coms-ai-chip-x:hover:not(:disabled) {
  opacity: 1; background: color-mix(in srgb, var(--c-shadow, #000) 10%, transparent);
  color: inherit; transform: none; box-shadow: none;
}
/* Centred with the row it describes. One line per fact -- "what did it
   get out of my file" is three questions, and a run-on sentence answers
   none of them clearly. */
.coms-ai-attach-note {
  margin: 0 0 0.75rem; text-align: center;
  display: flex; flex-direction: column; gap: 0.15rem;
}
.coms-ai-attach-line { font-size: 0.76rem; line-height: 1.5; }

/* ── Choosing the depth, in the AI panel ──────────────────────────────
   ⚠️ The price and the token budget are the decision, so they are the two
   things given weight. Everything else on the row is supporting detail at
   one size down. */
.coms-ai-tiers { display: flex; flex-direction: column; gap: 0.4rem; margin: 0.15rem 0 0.6rem; }
.coms-ai-tier {
  display: flex; align-items: flex-start; gap: 0.6rem;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 10px;
  background: var(--c-surface, #ffffff);
  cursor: pointer;
  font-weight: 400;
}
.coms-ai-tier:hover:not(.is-off) { border-color: var(--c-brand-border, #c7d2fe); }
.coms-ai-tier.is-on {
  border-color: var(--c-brand, #6366f1);
  background: color-mix(in srgb, var(--c-brand, #6366f1) 6%, transparent);
  box-shadow: 0 0 0 1px var(--c-brand, #6366f1);
}
/* ⚠️ DIMMED, NOT HIDDEN. Somebody choosing a depth should be able to see
   what the ceiling is even when they cannot buy it yet. */
.coms-ai-tier.is-off { opacity: 0.5; cursor: not-allowed; }
.coms-ai-tier input { margin-top: 0.2rem; flex: 0 0 auto; }
.coms-ai-tier-body { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; flex: 1; }
.coms-ai-tier-top { display: flex; align-items: baseline; gap: 0.5rem; }
.coms-ai-tier-top strong { font-size: 0.9rem; }
.coms-ai-tier-price { margin-left: auto; font-weight: 700; font-size: 0.95rem; }
.coms-ai-tier-meta { font-size: 0.75rem; color: var(--c-text-muted, #64748b); line-height: 1.4; }
.coms-ai-tier-note { font-size: 0.72rem; color: var(--c-text-dim, #94a3b8); }
.coms-ai-tier-rec { font-size: 0.72rem; font-weight: 700; color: var(--c-brand, #6366f1); }
.coms-ai-tier-group {
  margin: 0.55rem 0 0.1rem; font-size: 0.72rem; font-weight: 700;
  letter-spacing: 0.05em; text-transform: uppercase; color: var(--c-text-dim, #94a3b8);
}
.coms-ai-ledger {
  margin: 0 0 0.7rem; font-size: 0.78rem; font-weight: 600;
  color: var(--c-text-2, #475569);
}

/* What actually happens, for somebody deciding whether to spend. A
   <details> so it costs nothing to anybody who already knows. */
.coms-ai-how {
  margin-top: 0.8rem; padding-top: 0.7rem;
  border-top: 1px solid var(--c-border, #e2e8f0);
}
.coms-ai-how > summary {
  cursor: pointer; font-size: 0.8rem; font-weight: 700;
  color: var(--c-text-2, #475569);
}
.coms-ai-how > summary::marker { color: var(--c-text-dim, #94a3b8); }
.coms-ai-how-steps {
  margin: 0.6rem 0 0; padding-left: 1.15rem;
  display: flex; flex-direction: column; gap: 0.4rem;
  font-size: 0.8rem; line-height: 1.5; color: var(--c-text-2, #475569);
}
.coms-ai-how-steps strong { color: var(--c-text, #0f172a); }
.coms-ai-how-foot { margin: 0.6rem 0 0; }

/* ⚠️ The one fact somebody needs before spending, so it is a visible line
   and not a folded one. Quiet, but present. */
.coms-ai-safe {
  margin: 0.55rem 0 0;
  padding: 0.5rem 0.65rem;
  border-radius: 8px;
  background: var(--c-surface-subtle, #f8fafc);
  border: 1px solid var(--c-border, #e2e8f0);
  font-size: 0.78rem; line-height: 1.5;
  color: var(--c-text-2, #475569);
}

/* Pages given to the model as reference, beside the attach button. */
.coms-ai-ref {
  margin: 0; padding: 0.3rem 0.5rem;
  font-size: 0.75rem; max-width: 15rem; min-width: 0;
  border-radius: 999px;
}
.coms-ai-refs {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: 0.35rem; margin: 0 0 0.75rem;
}
/* ⚠️ A DIFFERENT TINT FROM THE ATTACHED DOCUMENT. Both are chips with a ×
   in the same corner of the panel, and "the file I uploaded" and "a page
   already on my site" are things somebody must be able to tell apart at a
   glance. */
.coms-ai-chip-ref {
  background: var(--c-surface-sunken, #f1f5f9);
  border: 1px solid var(--c-border, #e2e8f0);
}

/* The folder an import landed in, linked from the attachment line. */
.coms-ai-folder-link {
  color: var(--c-brand, #6366f1);
  text-decoration: underline;
  text-underline-offset: 0.15em;
  font-weight: 600;
}
.coms-ai-folder-link:hover { color: var(--c-brand-strong, #4f46e5); }
.coms-ai-folder-open { text-align: center; margin: 0 0 0.6rem; font-size: 0.76rem; }


/* ── Edit User modal ───────────────────────────────────────────────────
   ⚠️ `.modal` CAPS AT 400px AND THIS ONE MUST NOT. This is the only
   screen that edits an account, and at 400px it was one column of six
   controls -- so the access profile lived on a different page from the
   role it has to be read against. Wider here, and one column again on a
   phone, where two columns of inputs is worse than scrolling. */
.edit-user-modal {
  max-width: 860px;
  /* The overlay already pads 1.5rem; this keeps a tall form inside the
     viewport on a laptop AND on a phone in landscape, which is the case
     that overflows first. */
  max-height: calc(100vh - 3rem);
  display: flex;
  flex-direction: column;
  padding: 0;
}

.eu-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 1.25rem 1.5rem 0.75rem;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
}

.edit-user-modal .eu-head h3 { margin: 0; }

.eu-subtitle {
  margin: 0.15rem 0 0;
  font-size: 0.85rem;
  color: var(--c-text-muted, #64748b);
}

.eu-close {
  background: none;
  border: none;
  font-size: 1.5rem;
  line-height: 1;
  padding: 0 0.25rem;
  cursor: pointer;
  color: var(--c-text-muted, #64748b);
}
.eu-close:hover { color: var(--c-text, #0f172a); }

/* Facts an admin reads while deciding what to change, and that nothing
   on this form edits. Wraps rather than scrolls -- a row of chips that
   scrolls sideways hides the one at the end. */
.eu-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  padding: 0.75rem 1.5rem;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface-alt, #f8fafc);
}
.eu-facts:empty { display: none; }

.eu-fact {
  font-size: 0.78rem;
  padding: 0.2rem 0.5rem;
  border-radius: 999px;
  background: var(--c-surface, #fff);
  border: 1px solid var(--c-border, #e2e8f0);
  color: var(--c-text-muted, #64748b);
}
.eu-fact strong { color: var(--c-text, #0f172a); font-weight: 600; }

.edit-user-modal form {
  overflow-y: auto;
  padding: 1rem 1.5rem 0;
  flex: 1 1 auto;
}

.eu-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem;
  align-items: start;
}

.eu-section {
  border: 1px solid var(--c-border, #e2e8f0);
  border-radius: 8px;
  padding: 0.75rem 1rem 1rem;
  margin: 0;
  min-width: 0;
}

.eu-section legend {
  font-size: 0.78rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--c-text-muted, #64748b);
  padding: 0 0.35rem;
}

.eu-field { margin-bottom: 0.75rem; }
.eu-field:last-child { margin-bottom: 0; }

.eu-field label {
  display: block;
  font-size: 0.85rem;
  font-weight: 500;
  margin-bottom: 0.25rem;
}

.eu-field input,
.eu-field select {
  width: 100%;
  box-sizing: border-box;
}

.eu-field .muted-small { margin: 0.3rem 0 0; }

/* Two inputs on one line inside a section, collapsing before the
   sections themselves do -- a first/last name pair is the one place two
   narrow boxes still read correctly. */
.eu-pair {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.6rem;
}

.eu-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  padding: 1rem 0;
  margin-top: 0.5rem;
  border-top: 1px solid var(--c-border, #e2e8f0);
  position: sticky;
  bottom: 0;
  background: var(--c-surface, #fff);
}

.eu-actions .result { margin: 0 0 0 auto; font-size: 0.85rem; }

/* ⚠️ ONE COLUMN ON A PHONE, and the breakpoint is on the GRID rather
   than on the modal: the sections stack before the modal runs out of
   room, so a section never gets narrow enough to squeeze its labels. */
@media (max-width: 720px) {
  .eu-grid { grid-template-columns: minmax(0, 1fr); }
  .eu-pair { grid-template-columns: minmax(0, 1fr); }
  .edit-user-modal { max-height: calc(100vh - 2rem); }
  .eu-head,
  .eu-facts { padding-left: 1rem; padding-right: 1rem; }
  .edit-user-modal form { padding-left: 1rem; padding-right: 1rem; }
  .eu-actions .result { margin-left: 0; width: 100%; }
}

/* The directory name, now a link to the public profile. Styled as text
   until hovered so the card does not read as a wall of links. */
.user-card-name-link {
  color: inherit;
  text-decoration: none;
}
.user-card-name-link:hover,
.user-card-name-link:focus-visible {
  text-decoration: underline;
  color: var(--c-info, #2563eb);
}
.user-card-name-ext {
  font-size: 0.7em;
  opacity: 0.55;
  margin-left: 0.25em;
}


/* ══════════════════════════════════════════════════════════════════════
   SITE PAGE SECTIONS  (.page-* family)

   ⚠️ A SHARED VOCABULARY, BECAUSE content_css IS PER PAGE. Each
   site_page carries its own stylesheet and nothing is shared between
   them, so two pages built by two people end up as two design systems --
   which is exactly what happened on Sensorium: a landing page with its
   own tokens, a practitioners page with a bespoke pr-* set, a vendors
   page with no styling at all, and two forms with a third look. The
   normalisation is not "restyle each page", it is "give them one
   vocabulary to be written in".

   These live here, beside .page-wrap and .page-cols, and are generic:
   nothing below names Sensorium, and every colour is a theme token, so a
   white-label install gets its own palette for free.

   ⚠️ A SECTION IS A BAND. The landing rules above make a top-level
   <section> full-bleed with no padding, which is what lets a tinted or
   image section run edge to edge. The measure comes back from a
   .page-wrap INSIDE it:

     <section class="page-section page-section-tint">
       <div class="page-wrap"> ... </div>
     </section>
   ══════════════════════════════════════════════════════════════════════ */

/* ⚠️ TIGHTER THAN IT WAS. clamp(3rem, 7vw, 5.5rem) put up to 11rem
   between one section's last line and the next one's first, which on a
   page of eleven sections reads as eleven separate pages rather than one
   argument. */
.page-section { padding: clamp(2.25rem, 4.5vw, 3.5rem) 0; }

/* ⚠️ A HAIRLINE BETWEEN SECTIONS THAT SHARE A BACKGROUND, and none
   where the background already changes. A divider under a tint edge is a
   second line saying what the colour change has said. */
.page-section + .page-section { border-top: 1px solid var(--c-border, #e2e8f0); }
.page-section-tint + .page-section,
.page-section + .page-section-tint,
.page-section-dark + .page-section,
.page-section + .page-section-dark,
.page-section + .page-cta-band,
.page-band + .page-section { border-top: 0; }
.page-section-tint { background: var(--c-surface-subtle, #f8fafc); }
.page-section-dark {
  background: var(--c-chrome, #17121f);
  color: var(--c-chrome-text, #cbd5e1);
}
.page-section-dark h2,
.page-section-dark h3 { color: var(--c-highlight, #fff); }

/* A small caps label above a heading. */
.page-eyebrow {
  margin: 0 0 0.6rem;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--c-brand, #6366f1);
}
.page-section-dark .page-eyebrow { color: var(--c-brand-alt, #8b5cf6); }

.page-section h2 {
  margin: 0 0 0.75rem;
  font-size: clamp(1.6rem, 3.4vw, 2.4rem);
  line-height: 1.15;
  letter-spacing: -0.015em;
}

/* One step above body copy, for the sentence under a heading. */
.page-lead {
  margin: 0 0 1.25rem;
  font-size: clamp(1.02rem, 1.5vw, 1.15rem);
  line-height: 1.6;
  color: var(--c-text-2, #334155);
  max-width: 62ch;
}
.page-section-dark .page-lead { color: var(--c-chrome-text, #cbd5e1); }
.page-center { text-align: center; }
.page-center .page-lead { margin-left: auto; margin-right: auto; }

/* ── hero ──────────────────────────────────────────────────────────── */
/* ⚠️ THE SCRIM IS NOT OPTIONAL. Text over an uncontrolled photograph
   is a contrast failure waiting for the one image that is bright where
   the words are. The gradient is darkest at the bottom, where the copy
   sits. */
.page-hero {
  position: relative;
  display: grid;
  align-items: end;
  min-height: clamp(320px, 52vh, 560px);
  overflow: hidden;
  /* ⚠️ ONE NUMBER, TUNED IN ONE PLACE. How much comes off the top of
     the hero photograph. Fluid below desktop, where the hero is shorter
     and a flat 120px would be a quarter of it. */
  --page-hero-crop: clamp(40px, 8vw, 120px);
}
/* ⚠️ EXACTLY 120px ON A DESKTOP, not 8vw's 115 at the commonest
   window width. A viewport unit is the right shape for the ramp and the
   wrong thing to leave in charge of the number that was actually asked
   for. */
@media (min-width: 1024px) {
  .page-hero { --page-hero-crop: 120px; }
}
/* ⚠️ A DEEPER CROP FOR ONE HERO, when its subject sits low in the
   frame and the default leaves too much sky or foliage above it. Doubles
   the default at every step of the ramp rather than pinning a flat 240px:
   on a 320px hero at phone width, 240px would leave 80px of photograph.
   
   ⚠️ TWO CLASSES, so it beats the `.page-hero` rule inside the media
   query above on specificity rather than on source order -- the same
   reasoning the .page-wrap overrides rely on, and the reason this is not
   simply an inline --page-hero-crop on the element. */
.page-hero.page-hero-crop-deep { --page-hero-crop: clamp(60px, 16vw, 240px); }
@media (min-width: 1024px) {
  .page-hero.page-hero-crop-deep { --page-hero-crop: 240px; }
}
/* ⚠️ THE TOP 50px IS CUT AT EVERY WIDTH, and object-position alone
   could not do it. With object-fit: cover the crop is whatever overflows
   the box, so a percentage only redistributes what is already spare:
   63% moved the window 52px down at 1440px, 7px at 768px and ZERO on a
   phone, where the image height exactly meets the box and there is
   nothing left to give.
   
   So the image is made 50px TALLER than its cell and pulled up by the
   same 50px. Its bottom still lands on the cell's bottom edge, the top
   50px falls outside, and .page-hero's overflow: hidden clips it. Exact
   and identical at every viewport, which a proportion never is. */
.page-hero > img {
  grid-area: 1 / 1;
  width: 100%;
  height: calc(100% + var(--page-hero-crop));
  margin-top: calc(var(--page-hero-crop) * -1);
  object-fit: cover;
}
/* ⚠️ LIGHTER THAN IT WAS, BECAUSE THE PANEL NOW DOES THE WORK. 78%
   at the foot was sized to make white type readable directly on the
   photograph. With a panel behind the words that job has moved, and
   holding the old value would just be two things darkening the same
   image. This is now only separating the hero from what follows. */
.page-hero-scrim {
  grid-area: 1 / 1;
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--c-scrim, #0f172a) 10%, transparent) 0%,
    color-mix(in srgb, var(--c-scrim, #0f172a) 55%, transparent) 100%);
}
.page-hero-body {
  grid-area: 1 / 1;
  align-self: end;
  width: 100%;
  max-width: var(--page-measure);
  margin: 0 auto;
  padding: clamp(1.5rem, 4vw, 3rem) 1.5rem clamp(2rem, 5vw, 3.5rem);
  color: var(--c-highlight, #fff);
}
/* ⚠️ A PANEL BEHIND THE WORDS, not another layer over the whole
   photograph. The scrim darkens everything equally, which costs the image
   to buy contrast it only needs where the text is; this puts the density
   exactly there and lets the rest of the picture stay a picture.

   ⚠️ THE COLOUR IS --c-chrome-2, A THEME TOKEN. On this install that
   is #1a3d3c, a dark green that belongs to the site; on a white-label
   with an indigo palette it is that install's own dark. Hard-coding a
   green here would put Sensorium's brand in every other site's hero.

   ⚠️ inline-block SO IT HUGS THE TEXT. As a block it would paint a
   1100px bar across the hero whatever the headline's length, which is a
   caption bar and not what was asked for. */
.page-hero-panel {
  display: inline-block;
  max-width: min(100%, 44rem);
  padding: clamp(1rem, 2.4vw, 1.5rem) clamp(1.1rem, 2.8vw, 1.8rem);
  border-radius: 14px;
  background: color-mix(in srgb, var(--c-chrome-2, #241a3d) 78%, transparent);
  border: 1px solid color-mix(in srgb, var(--c-highlight, #fff) 14%, transparent);
  /* Frosts the photograph immediately behind the panel, so a busy patch
     of foliage does not read through the translucency as texture on the
     type. Ignored where unsupported, which costs nothing -- the 78% fill
     is already carrying the contrast. */
  backdrop-filter: blur(4px);
}
.page-hero-panel > :first-child { margin-top: 0; }
.page-hero-panel > :last-child { margin-bottom: 0; }
.page-hero-body h1 {
  margin: 0 0 0.6rem;
  /* ⚠️ THE FLOOR IS WHAT PHONES GET, and 2rem was chosen for how it
     looked on a desktop. Below about 590px the 5.4vw term is smaller than
     the floor, so every phone rendered a flat 32px headline -- two lines
     of it, over a subtitle at its own 1.02rem floor, filling the hero and
     leaving the photograph as a sliver behind the panel. ONLY THE FLOOR MOVES: the
     5.4vw ramp and the 3.4rem cap are untouched, so every size a desktop
     or tablet renders is identical to before and only the screens that
     were pinned to the floor change. The phone gets 24px. */
  font-size: clamp(1.5rem, 5.4vw, 3.4rem);
  line-height: 1.08;
  letter-spacing: -0.02em;
  color: var(--c-highlight, #fff);
}
.page-hero-body .page-eyebrow { color: var(--c-highlight, #fff); opacity: 0.85; }
.page-hero-sub {
  margin: 0 0 1.25rem;
  /* ⚠️ Same trap: 1.7vw does not reach 1.02rem until 960px, so the
     floor was the only value any phone or tablet ever saw. */
  font-size: clamp(0.9rem, 2vw, 1.2rem);
  line-height: 1.55;
  max-width: 54ch;
  opacity: 0.94;
}

/* ── split: copy beside a picture, alternating ─────────────────────── */
/* ⚠️ auto-fit WITH A MINIMUM, not a media query. The same markup
   stacks on a phone with no breakpoint to keep in sync -- the thing a
   generated page most often gets wrong. */
.page-split {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr));
  gap: clamp(1.5rem, 4vw, 3.5rem);
  align-items: center;
}
.page-split > * { min-width: 0; }
/* ⚠️ NO FORCED ASPECT RATIO, and that is the fix for "the images look
   distorted". object-fit: cover never stretches -- it CROPS -- and a 4/3
   frame was cropping ~45% off the two portrait photographs in this set
   (1200x1600 and 1066x1600), slicing the subject out of its own picture.
   Every other image here is 3:2 and was losing a band off both ends.
   A picture keeps the shape it was taken in; the column is centred, so
   rows of different heights sit correctly beside their text. */
.page-split img {
  width: 100%;
  height: auto;
  border-radius: 16px;
  display: block;
}
/* ⚠️ ORDER ONLY WHEN THERE IS ROOM FOR TWO COLUMNS. Reversing at
   every width puts the picture above the words it illustrates on a
   phone, which reads as a caption for the wrong thing. */
@media (min-width: 64rem) {
  .page-split-reverse > :first-child { order: 2; }
}

/* ── tiles: a responsive grid of cards ─────────────────────────────── */
.page-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
  gap: 1rem;
}
.page-tile {
  padding: 1.35rem;
  border-radius: 16px;
  border: 1px solid var(--c-border, #e2e8f0);
  background: var(--c-surface, #fff);
}
.page-tile h3 {
  margin: 0 0 0.4rem;
  font-size: 1.05rem;
  line-height: 1.3;
}
.page-tile p { margin: 0; color: var(--c-text-2, #334155); line-height: 1.55; }
.page-tile-tag {
  display: inline-block;
  margin: 0 0 0.6rem;
  padding: 0.15rem 0.55rem;
  border-radius: 999px;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--c-brand-deep, #4338ca);
  background: var(--c-brand-tint, #f5f3ff);
}
/* The one tile that is the offer, not one of the options. */
.page-tile-feature {
  border-color: var(--c-brand-border, #c7d2fe);
  background: var(--c-brand-tint, #f5f3ff);
}
.page-section-dark .page-tile {
  background: color-mix(in srgb, var(--c-highlight, #fff) 6%, transparent);
  border-color: var(--c-chrome-border, #4b5563);
}
.page-section-dark .page-tile p { color: var(--c-chrome-text, #cbd5e1); }

/* ── a bulleted list that does not look like a document ────────────── */
.page-list { margin: 0; padding: 0; list-style: none; display: grid; gap: 0.7rem; }
.page-list li {
  position: relative;
  padding-left: 1.6rem;
  line-height: 1.55;
  color: var(--c-text-2, #334155);
}
.page-list li::before {
  content: "";
  position: absolute;
  left: 0.15rem;
  top: 0.55em;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--c-brand, #6366f1);
}
.page-section-dark .page-list li { color: var(--c-chrome-text, #cbd5e1); }
.page-list-num { counter-reset: page-step; }
.page-list-num li::before {
  counter-increment: page-step;
  content: counter(page-step);
  top: 0.05em;
  left: 0;
  width: 1.15rem;
  height: 1.15rem;
  border-radius: 50%;
  font-size: 0.68rem;
  font-weight: 700;
  color: var(--c-on-accent, #fff);
  display: grid;
  place-items: center;
}

/* ── gallery ───────────────────────────────────────────────────────── */
.page-gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
  gap: 0.85rem;
}
.page-gallery img {
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: 14px;
  display: block;
}

/* ── callout ───────────────────────────────────────────────────────── */
.page-note {
  padding: 1.1rem 1.35rem;
  border-radius: 14px;
  border: 1px solid var(--c-brand-border, #c7d2fe);
  border-left-width: 4px;
  background: var(--c-brand-tint, #f5f3ff);
  color: var(--c-text-2, #334155);
  line-height: 1.6;
}
.page-note :first-child { margin-top: 0; }
.page-note :last-child { margin-bottom: 0; }

/* ── call to action ────────────────────────────────────────────────── */
.page-cta { text-align: center; }
.page-cta h2 { margin-bottom: 0.5rem; }

.page-btn {
  display: inline-block;
  padding: 0.8rem 1.6rem;
  border-radius: 999px;
  font-weight: 600;
  font-size: 0.98rem;
  text-decoration: none;
  background: var(--c-brand, #6366f1);
  color: var(--c-on-accent, #fff);
  border: 1px solid transparent;
  transition: background 0.15s ease, transform 0.15s ease;
}
.page-btn:hover { background: var(--c-brand-strong, #4f46e5); transform: translateY(-1px); }
/* On a photograph the filled button competes with the image; the ghost
   reads as chrome over it. */
.page-btn-ghost {
  background: color-mix(in srgb, var(--c-highlight, #fff) 14%, transparent);
  border-color: color-mix(in srgb, var(--c-highlight, #fff) 55%, transparent);
  color: var(--c-highlight, #fff);
  backdrop-filter: blur(3px);
}
.page-btn-ghost:hover { background: color-mix(in srgb, var(--c-highlight, #fff) 26%, transparent); }

/* ⚠️ THE SECOND ACTION, ON A LIGHT BACKGROUND. .page-btn-ghost is
   built for sitting over a photograph -- translucent white on white is
   invisible -- so a section that offers two links needs its own quieter
   half. Outlined, so the pair reads as primary and secondary rather than
   as two equal choices. */
.page-btn-quiet {
  background: transparent;
  color: var(--c-brand-deep, #4338ca);
  border-color: var(--c-brand-border, #c7d2fe);
}
.page-btn-quiet:hover {
  background: var(--c-brand-tint, #f5f3ff);
  color: var(--c-brand-deep, #4338ca);
}
/* A row of actions that wraps rather than overflowing on a phone. */
.page-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin: 1.25rem 0 0;
}

/* ⚠️ A BAND'S CHILD STILL NEEDS THE MEASURE. .page-wrap already
   supplies it; this just stops a section's own first/last margin
   collapsing through the band and reopening the gap the padding set. */
.page-section > .page-wrap > :first-child { margin-top: 0; }
.page-section > .page-wrap > :last-child { margin-bottom: 0; }


/* ⚠️ A FOOTNOTE, NOT A CALL TO ACTION. On a page whose job is to
   explain what somebody DOES, the link to apply is a courtesy for the
   small number of readers who turn out to want it -- not the outcome the
   page is driving at. Given .page-btn it becomes a pitch again, which is
   the thing these pages are being moved away from. Quiet, small, and
   last. */
.page-footnote {
  padding: 1.5rem 0 0;
  border-top: 1px solid var(--c-border, #e2e8f0);
  font-size: 0.9rem;
  color: var(--c-text-muted, #64748b);
  line-height: 1.6;
}
.page-footnote a {
  color: var(--c-brand, #6366f1);
  font-weight: 600;
  text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--c-brand, #6366f1) 35%, transparent);
}
.page-footnote a:hover { border-bottom-color: currentColor; }

/* A tighter tile for the condensed "how to take part" block -- it sits
   below the editorial and must not out-weigh it. */
.page-tiles-quiet .page-tile { padding: 1.1rem; background: transparent; }
.page-tiles-quiet .page-tile h3 { font-size: 0.95rem; }
.page-tiles-quiet .page-list li { font-size: 0.9rem; }
.page-tiles-quiet .page-list li::before { top: 0.5em; width: 0.35rem; height: 0.35rem; }


/* ⚠️ A HERO WITH NO PICTURE. Not every page has an image that can
   carry a headline: a photograph with type already burned into it, or one
   that is specific to a single venue, is worse than none -- it competes
   with the h1 and it dates the page. This keeps the hero's type scale and
   drops the image, so a page can lead with words alone without falling
   back to a plain <h1> on white. */
.page-hero-plain {
  min-height: 0;
  background: var(--c-chrome, #17121f);
  color: var(--c-chrome-text, #cbd5e1);
}
.page-hero-plain .page-hero-body {
  padding-top: clamp(2.75rem, 7vw, 4.5rem);
  padding-bottom: clamp(2.75rem, 7vw, 4.5rem);
}
.page-hero-plain .page-hero-sub { opacity: 1; color: var(--c-chrome-text, #cbd5e1); }

/* ── a full-bleed band with the picture BEHIND the words ───────────── */
/* ⚠️ THE SAME GRID STACK AS .page-hero, CENTRED. A background-image
   would have been fewer lines and wrong: an <img> keeps the alt text, is
   discoverable to a screen reader, and can be lazy-loaded and served
   responsively through srcset. Stacking in one grid cell does what
   background-image was wanted for without giving that up.

   ⚠️ AND THE SCRIM IS HEAVIER THAN THE HERO'S. Text sitting in the
   MIDDLE of a photograph has no dark edge to fall back on, so this is an
   even wash rather than a bottom-weighted gradient -- and it is a floor,
   not a preference: the band has to stay readable over an image nobody
   has checked. */
.page-band {
  position: relative;
  display: grid;
  align-items: center;
  /* ⚠️ SIZED BY ITS CONTENT, with a floor rather than a tall fixed
     height. 56vh of photograph for four lines of text was a hole in the
     middle of the page. */
  min-height: clamp(300px, 36vh, 440px);
  overflow: hidden;
}
.page-band > img {
  grid-area: 1 / 1;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* ⚠️ NEARLY OPAQUE, AND 66% WAS NOT ENOUGH. The first pass treated
   the overlay as a tint to take the edge off the photograph; over a busy
   image -- dappled grass, a crowd, bright sky -- a tint leaves light
   patches exactly where a descender or a thin stroke lands, and the
   result is text you can technically read and do not want to.
   
   The wash is the thing being designed here and the photograph is
   texture under it. 86% at the centre, deepening at the edges, which is
   dark enough that white type holds against any frame of any image in
   the library -- checked or not. */
.page-band-scrim {
  grid-area: 1 / 1;
  /* ⚠️ THE TWO LAYERS MULTIPLY, and the first attempt at this forgot
     that. 86% under a 78%-to-92% vignette is 97% at the centre and 99% at
     the edge -- the photograph was gone and the band was a dark rectangle
     with a picture nobody could see behind it. Compositing is
     1-(1-a)(1-b), so the vignette starts at ZERO and the flat wash does
     the work: 82% where the text sits, 90% into the corners. Dark enough
     that white type holds against any frame of any image, light enough
     that it is still visibly a photograph. */
  background:
    radial-gradient(130% 110% at 50% 50%,
      transparent 0%,
      color-mix(in srgb, var(--c-scrim, #0f172a) 45%, transparent) 100%),
    color-mix(in srgb, var(--c-scrim, #0f172a) 82%, transparent);
}
.page-band-body {
  grid-area: 1 / 1;
  position: relative;
  padding-top: clamp(2rem, 4vw, 3rem);
  padding-bottom: clamp(2rem, 4vw, 3rem);
  color: var(--c-highlight, #fff);
  text-align: center;
  /* Type over a photograph needs a little weight behind it even with a
     scrim -- this is the shadow doing the last 5% of the contrast. */
  text-shadow: 0 1px 2px color-mix(in srgb, var(--c-scrim, #0f172a) 55%, transparent);
}
/* ⚠️ BIGGER THAN A SECTION HEADING, NOT SMALLER. The previous pass
   shrank this on the theory that a band is a quiet moment. Wrong twice:
   a full-bleed image band is the most prominent thing on the page, so
   type that shrinks inside it reads as an accident -- and small light
   text on a photograph is the hardest combination to read there is.
   Once the wash above is doing the contrast work, the type can be the
   size the section deserves. */
.page-band-body h2 {
  font-size: clamp(1.9rem, 4vw, 2.8rem);
  line-height: 1.12;
  margin-bottom: 1rem;
}
.page-band-body .page-lead {
  max-width: 60ch;
  font-size: clamp(1.1rem, 1.8vw, 1.3rem);
  line-height: 1.55;
  opacity: 1;
}
.page-band-body .page-list li {
  font-size: clamp(1rem, 1.5vw, 1.12rem);
  line-height: 1.55;
}
.page-band-body h2,
.page-band-body h3 { color: var(--c-highlight, #fff); }
.page-band-body .page-eyebrow { color: var(--c-highlight, #fff); opacity: 0.8; }
.page-band-body .page-lead {
  color: var(--c-highlight, #fff);
  margin-left: auto;
  margin-right: auto;
}
/* The list inside a band centres with its text, so the bullet cannot sit
   in a left gutter the centred lines have moved away from. */
.page-band-body .page-list { max-width: 58ch; margin: 1.75rem auto 0; text-align: left; gap: 0.9rem; }
/* ⚠️ NO OPACITY ON TEXT OVER AN IMAGE. Dimming white type is how the
   first pass lost the contrast the scrim had just bought -- the accent
   belongs on the bullet, not on the words. */
.page-band-body .page-list li { color: var(--c-highlight, #fff); }
.page-band-body .page-list li::before { background: var(--c-brand-alt, #8b5cf6); opacity: 1; }

/* ── the closing call to action ────────────────────────────────────── */
/* ⚠️ THIS ONE IS ALLOWED TO ASK. The footnote treatment is right in
   the middle of an editorial page, where a reader is still finding out
   what happens; at the END of it they have read the whole thing, and a
   muted line is no longer modesty, it is a dead end. */
.page-cta-band {
  /* ⚠️ A GRADIENT ACROSS TWO BRAND STEPS, not a flat fill. Both stops
     are theme tokens, so a white-label install gets its own version of
     the same move rather than an indigo band bolted onto its palette. */
  background: linear-gradient(135deg,
    var(--c-brand-deep, #4338ca) 0%,
    var(--c-brand-strong, #4f46e5) 55%,
    var(--c-brand, #6366f1) 100%);
  color: var(--c-highlight, #fff);
  text-align: center;
  padding: clamp(2.75rem, 5.5vw, 4rem) 0;
}
.page-cta-band h2 { color: var(--c-highlight, #fff); }
.page-cta-band .page-eyebrow { color: var(--c-highlight, #fff); opacity: 0.75; }
.page-cta-band .page-lead {
  color: var(--c-highlight, #fff);
  opacity: 0.92;
  margin-left: auto;
  margin-right: auto;
}
.page-cta-band .page-btn {
  margin-top: 0.5rem;
  background: var(--c-highlight, #fff);
  color: var(--c-brand-deep, #4338ca);
}
.page-cta-band .page-btn:hover {
  background: var(--c-brand-tint, #f5f3ff);
  color: var(--c-brand-deep, #4338ca);
}
.page-cta-note {
  margin: 1rem 0 0;
  font-size: 0.85rem;
  opacity: 0.8;
}


/* ══════════════════════════════════════════════════════════════════════
   A BUILT FORM, ON A PAGE OF ITS OWN

   ⚠️ THIS LIVES HERE AND NOT IN siteFormService. That stylesheet
   ships INSIDE the form, which is pasted onto pages this codebase does
   not own -- it has to stay a self-contained component and must never
   assume a canvas. The canvas is the HOST page's business, and this is
   the host page.

   ⚠️ :has() PICKS THE PAGE OUT, because nothing else can. A signup
   page is a site_page whose content is one <div class="lxf-wrap">; there
   is no per-page body class to hang this on, and content_html cannot set
   one. Where :has() is unsupported the page simply renders as it did
   before -- no canvas, no broken layout.
   ══════════════════════════════════════════════════════════════════════ */

/* ⚠️ LIGHT, NOT DARK. The first version of this was the site's two
   chrome tones, which is right for a band inside a page and wrong for a
   whole page: a full screen of near-black behind a white card makes the
   card glare, and a long form is a lot of minutes to spend looking at
   that. The tints do the work instead -- the page reads as paper with a
   wash over it, and the card still lifts off it. */
.site-page-landing .site-page:has(> .lxf-wrap) {
  background:
    radial-gradient(ellipse 70% 50% at 12% 0%,
      color-mix(in srgb, var(--c-brand, #6366f1) 14%, transparent) 0%, transparent 62%),
    radial-gradient(ellipse 65% 45% at 88% 100%,
      color-mix(in srgb, var(--c-brand-alt, #8b5cf6) 10%, transparent) 0%, transparent 62%),
    linear-gradient(170deg,
      var(--c-brand-tint, #f5f3ff) 0%,
      var(--c-surface-subtle, #f8fafc) 45%,
      var(--c-surface-sunken, #f1f5f9) 100%);
}

/* ⚠️ THE FORM IS NOT A FULL-WIDTH PAGE. 1100px was the fix for a form
   crushed into a 66ch reading column, and it over-corrected: a text input
   a thousand pixels wide is a worse thing to read back what you typed
   into than a narrow one, and the half-width pairs stop looking like a
   pair. 820px keeps two half rows comfortably above their 24rem container
   query and gives the card a shape rather than a stripe.

   ⚠️ AND THE PAGE BREATHES AT BOTH ENDS. The canvas runs edge to edge
   between the site header and footer, so without this the card butts
   against both -- the gradient needs somewhere to actually be seen. */
.site-page-landing .site-page > .lxf-wrap {
  padding-top: clamp(3rem, 7vw, 5rem);
  padding-bottom: clamp(3.5rem, 8vw, 6rem);
  /* ⚠️ THE WRAPPER'S OWN BOX COMES OFF, and this is the affordance the
     form's stylesheet explicitly offers -- its comment says the wrapper is
     a separate element "so a site can delete or restyle the box without
     touching the form inside it".
     
     It ships as a white fill with a border and a 12px radius, which is
     right for a form pasted onto somebody's page with no canvas. Here the
     canvas is the point and .lxf is already the card, so leaving it gave
     the page two nested boxes: a white slab flattening the gradient, with
     a second white card inside it. One card, on the gradient. */
  background: none;
  border: 0;
  border-radius: 0;
}

/* ⚠️ THE FORM BECOMES A CARD, and the rule is four classes deep so
   it beats the form's own `.lxf { }` without touching that file. */
.site-page-landing .site-page > .lxf-wrap > .lxf {
  position: relative;
  background: var(--c-surface, #fff);
  border-radius: 18px;
  padding: clamp(1.5rem, 4vw, 2.75rem) clamp(1.25rem, 3.5vw, 2.5rem);
  box-shadow:
    0 24px 60px color-mix(in srgb, var(--c-shadow, #000) 32%, transparent),
    0 4px 14px color-mix(in srgb, var(--c-shadow, #000) 14%, transparent);
  overflow: hidden;
}
/* The accent rule across the top of the card. ::before rather than a
   border so it can carry a gradient. */
.site-page-landing .site-page > .lxf-wrap > .lxf::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 4px;
  background: linear-gradient(90deg,
    var(--c-brand-deep, #4338ca) 0%,
    var(--c-brand, #6366f1) 55%,
    var(--c-brand-alt, #8b5cf6) 100%);
}

/* ── the head of the form ──────────────────────────────────────────── */
/* ⚠️ FOUR CLASSES, beating `.lxf .lxf-title` at two. The form's own
   1.5rem is right for a form dropped into a page of other content; this
   page IS the form, so its title is the page's title. */
.site-page-landing .site-page > .lxf-wrap .lxf-title {
  margin: 0 0 0.5rem;
  font-size: clamp(1.5rem, 3.4vw, 2.2rem);
  line-height: 1.15;
  letter-spacing: -0.015em;
  color: var(--c-text, #0f172a);
}
.site-page-landing .site-page > .lxf-wrap .lxf-intro {
  margin: 0 auto 1.75rem;
  max-width: 54ch;
  font-size: clamp(0.98rem, 1.4vw, 1.08rem);
  line-height: 1.6;
}
/* A hairline under the head, so the first question does not read as the
   next sentence of the introduction. */
.site-page-landing .site-page > .lxf-wrap .lxf-intro::after {
  content: "";
  display: block;
  width: 3rem;
  height: 2px;
  margin: 1.5rem auto 0;
  border-radius: 2px;
  background: var(--c-brand, #6366f1);
  opacity: 0.6;
}
.site-page-landing .site-page > .lxf-wrap .lxf-logo {
  margin: 0 auto 1.1rem;
}

/* ⚠️ THE STEP COUNTER READS AS A LABEL, not as a question. It is the
   only text in the head that is chrome rather than content. */
.site-page-landing .site-page > .lxf-wrap .lxf-step-label,
.site-page-landing .site-page > .lxf-wrap .lxf-steps {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--c-brand, #6366f1);
}


/* ── the step header inside a built form ───────────────────────────── */
/* ⚠️ THIS IS AUTHORED CONTENT, NOT FORM CHROME. Each step of a built
   form opens with an HTML block the author wrote in the builder, and on
   these forms it holds the step's own header:

     <div class="lxf-html">
       <p>Step 01 of 04 &middot; Identity<br/><br/>
          This becomes your public practitioner profile &mdash; ...</p>
     </div>

   One paragraph, two <br/>. There is no element around "Step 01 of 04"
   to give an eyebrow style to.

   ⚠️ SO ::first-line DOES IT. The <br/> guarantees the label is the
   first formatted line, and ::first-line accepts exactly the properties
   an eyebrow needs -- font, colour, letter-spacing, text-transform. No
   markup changes, which matters here more than usual: a form's
   content_html is REGENERATED from its definition every time somebody
   saves it in the builder, so anything edited into the markup by hand is
   lost on the next save. A stylesheet survives that. */
.site-page-landing .site-page > .lxf-wrap .lxf-step .lxf-rows > .lxf-html:first-child {
  margin-bottom: 1.75rem;
  padding-bottom: 1.5rem;
  border-bottom: 1px solid var(--c-border, #e2e8f0);
  text-align: center;
}
.site-page-landing .site-page > .lxf-wrap .lxf-step .lxf-rows > .lxf-html:first-child p {
  margin: 0 auto;
  max-width: 52ch;
  font-size: clamp(0.98rem, 1.4vw, 1.08rem);
  line-height: 1.65;
  color: var(--c-text-2, #334155);
}
/* The label line. Smaller, spaced and in the brand colour, so it reads as
   a marker rather than as the first sentence of the paragraph under it. */
.site-page-landing .site-page > .lxf-wrap .lxf-step .lxf-rows > .lxf-html:first-child p::first-line {
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--c-brand, #6366f1);
}
/* ⚠️ A NARROW SCREEN CAN WRAP THE LABEL, and ::first-line only ever
   styles the first VISUAL line -- so on a phone "Step 01 of 04 ·
   Identity" could take the eyebrow treatment on line one and the body
   treatment on line two. Tightening the tracking at that width keeps it
   on one line rather than letting it split. */
@media (max-width: 30rem) {
  .site-page-landing .site-page > .lxf-wrap .lxf-step .lxf-rows > .lxf-html:first-child p::first-line {
    font-size: 0.68rem;
    letter-spacing: 0.08em;
  }
}
