@charset "UTF-8";
/*
 * Support Inbox — design system foundation.
 * Compiled by `yarn build:css` (Dart Sass) into app/assets/builds/application.css,
 * then fingerprinted and served by Propshaft. Tokens first, then layout regions,
 * then components. Light tokens on :root; named themes ride [data-theme="…"].
 * System leaves <html> unstamped so prefers-color-scheme picks Default or Midnight.
 *
 * Adding a theme (FRO-155): a catalog entry in Appearance::CATALOG, a `theme-<id>` mixin that
 * starts from `theme-light` or `theme-dark`, and a `[data-theme="<id>"]` line below. Nothing else —
 * the picker's preview, swatches and the contrast check (spec/system/theme_contrast_spec.rb) all
 * read the tokens. Literal colors live ONLY inside `@mixin theme-*` blocks;
 * spec/lib/stylesheet_theme_guardrails_spec.rb fails on one anywhere else.
 */
/* Self-hosted webfont for a theme. Declared globally but fetched lazily: a browser only downloads
   a face once something on the page is set in that family, and only the owning theme's tokens
   name it — so a theme's font loads when that theme is active (or its card is on screen in the
   picker), and never otherwise. The file goes in app/assets/fonts, a Propshaft load-path root, so
   the relative url resolves the way Phosphor's does; CSP font-src is 'self'. */
/* Gotham's faces (FRO-156): open stand-ins for Nooklyn's Hoefler Chronicle Display and Gotham SSm,
   whose cloud.typography kit is domain-locked and may not be copied. Both OFL-1.1; the license
   texts sit beside the files. Libre Caslon Display has one weight, and it is already the hairline
   display cut Genghis sets its headings in at 300 — so it answers every weight rather than letting
   the browser smear a faux bold onto it. */
@font-face {
  font-family: "Libre Caslon Display";
  src: url("/assets/libre-caslon-display-latin-400-normal-25e2304e.woff2") format("woff2");
  font-weight: 300 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Montserrat";
  src: url("/assets/montserrat-latin-400-normal-a441a262.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Montserrat";
  src: url("/assets/montserrat-latin-500-normal-f289678f.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Montserrat";
  src: url("/assets/montserrat-latin-600-normal-806047de.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Montserrat";
  src: url("/assets/montserrat-latin-700-normal-4cbfa40d.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
/* What every theme gets unless it says otherwise: the optional tokens and the formulas derived
   from the primitives. Included at the top of theme-light and theme-dark, so it lands on every
   element that carries data-theme — <html>, and each mini-preview card in the picker — and a
   theme mixin can override any of it simply by setting the token after its base include. */
/* One assignment for both ways Midnight can arrive — an explicit Dark choice, or System on a
   dark OS — so the two selectors cannot drift. Comment / hairline / gradient tokens keep
   their theme-defaults formulas and retint themselves from the primitives below. */
/* Slack-style aubergine: warm plum chrome, pink accent. Built on Midnight so danger /
   success / shadows stay readable on a dark surface. */
/* Slack-style Hoth: icy paper, deep lake-blue accent. Light primitives only — Midnight's
   shadows and danger tones must not leak onto a pale page. */
/* TMS Hub as it actually looks: pale lavender page, white cards, purple-950 ink,
   electric violet accent — the Hub purple ramp, not Frontier blue, and not a dark
   invert of it. */
/* Nooklyn's own building (FRO-156): the near-black pages, white serif headlines, small uppercase
   sans captions and gold of nooklyn.com and Matrix. Values are Nooklyn's Genghis tokens — the dark
   palette, which is Nooklyn's default theme — named in each comment. Built on Midnight so the
   soft tints, comment band and shadows retint from these primitives. */
/* Creamsicle (FRO-158): orange sherbet on vanilla. Light primitives, so Default's danger, success
   and shadows stay tuned for a pale page. Ratios on #fffaf5: text 16.1:1, muted 5.9:1, accent as a
   link 5.0:1, white on the accent 5.2:1. The brighter #f76707 is for non-text fills only — it is
   2.9:1 under white text, so it never goes on a button. */
/* Ember (FRO-158): glowing coals. Built on Midnight so the soft tints, comment band and shadows
   retint from these primitives. Ratios: text 16.4:1, muted 8.3:1, accent 7.1:1, and dark ink on
   the accent 7.1:1 — white on #ff7a1a would be 2.6:1. */
/* Bubblegum (FRO-159): pink on a pale blush. Light primitives, so Default's danger, success and
   shadows stay tuned for a pale page. Ratios on #fff7fb: text 15.5:1, muted 5.8:1, accent as a
   link 5.4:1, white on the accent 5.7:1. The bright #ff8cc6 is for decorative fills only (the
   picker card's bubble) — it is 2.2:1 under white text. */
/* Neon Flamingo (FRO-159): Miami night. Built on Midnight so the soft tints, comment band and
   shadows retint from these primitives. Ratios: text 16.6:1, muted 8.6:1, accent 6.1:1, and dark
   ink on the accent 6.1:1. Cyan is the second neon: the focus ring and links under the pointer. */
:root {
  /* Text and icons drawn on an accent fill (primary button, badges, send spinner). Per theme:
     white fails on gold, phosphor green or a pastel accent. */
  --color-on-accent: #ffffff;
  /* Accent used AS text — links, the selected nav item, counts. A separate token from the fill
     because a bright fill can be unreadable as text on the page (gold on white is 1.6:1). */
  --color-accent-text: var(--color-accent);
  --color-focus-ring: var(--color-accent);
  /* An accent-text link while the pointer is on it. Same as at rest unless a theme has a second
     highlight for it (Neon Flamingo's cyan). */
  --color-link-hover: var(--color-accent-text);
  --color-sidebar: var(--color-surface-sunken);
  --color-sidebar-text: var(--color-text);
  --color-sidebar-muted: var(--color-muted);
  /* The modal and command-palette backdrop. */
  --color-scrim: rgba(16, 24, 40, 0.45);
  /* The thin ring that keeps a user-chosen color (a label, a state dot) from vanishing into the
     surface, and the tick drawn on one. Neither color is a theme color, so neither retints. */
  --color-swatch-ring: rgb(0 0 0 / 12%);
  --color-swatch-check: #ffffff;
  --color-swatch-check-shadow: rgb(0 0 0 / 70%);
  /* Background-image hook for the login page and empty states ONLY — never behind message text or
     the composer. Off under prefers-reduced-motion (see the shared rule below). */
  --theme-decoration: none;
  /* How tall the decoration is drawn under an empty state's copy (it never sits behind the
     words). Zero while there is no decoration, so the empty state keeps its usual height. */
  --theme-decoration-space: 0px;
  /* The picker card's ground. A theme can give its card a swatch of its own here (Creamsicle's
     sherbet diagonal); everything else on the card still reads the theme's real tokens. */
  --theme-preview-ground: var(--color-surface-sunken);
  /* Extra ring on the open conversation's row in the list. None unless a theme asks for one. */
  --conversation-row-current-glow: none;
  /* A halo around the primary button. None unless a theme asks for one; never animated. */
  --btn-primary-glow: none;
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-sans: var(--font-ui);
  /* Headings and display figures. */
  --font-display: var(--font-ui);
  /* The weight app headings are set in. `initial` leaves it unset, so each heading falls back to
     the weight it always had (var(--font-display-weight, 600)) — and a theme preview card does not
     inherit a different theme's weight from <html>. */
  --font-display-weight: initial;
  /* Letter-spacing for the small uppercase captions: sidebar sections, rail headings, group labels. */
  --tracking-caps: 0.04em;
  /* "See all"-style action links: a theme can set them as uppercase captions. */
  --action-link-transform: none;
  --action-link-tracking: normal;
  /* One place in the app needs it: a reply template's {{token}}, which has to be readable character
     for character because it is copied by hand. */
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 14px;
  --radius-xl: 18px;
  --radius-pill: 999px;
  /* The internal-comment surface: the composer in comment mode and the card it produces. Mixed
     from the warning pair rather than picked as three more hexes, so the amber a comment is drawn
     in cannot drift away from the amber every other warning in the app uses. It is its own named
     surface because a comment is neither half of the correspondence — --warning-soft alone has no
     edge or band to build a bordered, banded box out of. */
  --color-comment-soft: var(--color-warning-soft);
  --color-comment-band: color-mix(in srgb, var(--color-warning) 8%, var(--color-warning-soft));
  --color-comment-band-strong: color-mix(in srgb, var(--color-warning) 16%, var(--color-warning-soft));
  --color-comment-border: color-mix(in srgb, var(--color-warning) 35%, var(--color-warning-soft));
  /* Softer than --color-border, for rules *inside* a bordered card. A full-strength border
     repeated on every table row and stat divider is what makes a dense panel look boxy. */
  --color-hairline: color-mix(in srgb, var(--color-border) 55%, transparent);
  /* Chart fills fade toward the baseline so a tall bar does not read as a solid slab.
     Built from the accent/success pair rather than new hexes, so a chart cannot drift
     away from the blue and green the rest of the app uses for in/out.
     The fade stops at 70%: a bar's height IS the datum, and fading its base to near-white
     made short bars read shorter than they are. */
  --gradient-accent: linear-gradient(180deg, var(--color-accent), color-mix(in srgb, var(--color-accent) 70%, transparent));
  --gradient-success: linear-gradient(180deg, var(--color-success), color-mix(in srgb, var(--color-success) 70%, transparent));
  /* The edges that used to be light-only hexes. Derived so a named theme only has to retint the
     primitives. */
  --color-accent-hover: color-mix(in srgb, var(--color-accent) 86%, #000000);
  --color-danger-edge: color-mix(in srgb, var(--color-danger) 28%, var(--color-danger-soft));
  --color-success-edge: color-mix(in srgb, var(--color-success) 28%, var(--color-success-soft));
  --color-accent-edge: color-mix(in srgb, var(--color-accent) 28%, var(--color-accent-soft));
  --color-surface: #ffffff;
  --color-surface-sunken: #f6f7f9;
  --color-surface-raised: #ffffff;
  --color-border: #e3e6ea;
  --color-border-strong: #cdd2d9;
  --color-text: #1b1f24;
  --color-muted: #6b7280;
  --color-accent: #2f6feb;
  --color-accent-soft: #eaf1fe;
  --color-danger: #b42318;
  --color-danger-soft: #fef3f2;
  --color-success: #067647;
  --color-success-soft: #ecfdf3;
  --color-warning: #b54708;
  --color-warning-soft: #fffaeb;
  --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06);
  --shadow-md: 0 4px 16px rgba(16, 24, 40, 0.10);
  --shadow-lg: 0 1px 2px rgba(16, 24, 40, 0.04), 0 8px 24px -8px rgba(16, 24, 40, 0.10);
  color-scheme: light;
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --text-sm: 13px;
  --text-base: 14px;
  --text-lg: 16px;
  --text-xl: 20px;
  /* Display sizes for the public explainer and the analytics report, where a figure has to
     outrank the label above it. The features hero has to hold a Chatwoot-scale headline
     without inventing a second type ramp. */
  --text-2xl: 28px;
  --text-display: 40px;
  /* Large figures set at their default tracking read loose. Only worth applying at
     --text-xl and up; at body sizes it closes the letters up. */
  --tracking-tight: -0.02em;
}

/* System (no data-theme) follows the OS. Any stamped theme owns the tokens completely —
   otherwise a dark OS would overlay Midnight on Aubergine / TMS / Hoth. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    /* Text and icons drawn on an accent fill (primary button, badges, send spinner). Per theme:
       white fails on gold, phosphor green or a pastel accent. */
    --color-on-accent: #ffffff;
    /* Accent used AS text — links, the selected nav item, counts. A separate token from the fill
       because a bright fill can be unreadable as text on the page (gold on white is 1.6:1). */
    --color-accent-text: var(--color-accent);
    --color-focus-ring: var(--color-accent);
    /* An accent-text link while the pointer is on it. Same as at rest unless a theme has a second
       highlight for it (Neon Flamingo's cyan). */
    --color-link-hover: var(--color-accent-text);
    --color-sidebar: var(--color-surface-sunken);
    --color-sidebar-text: var(--color-text);
    --color-sidebar-muted: var(--color-muted);
    /* The modal and command-palette backdrop. */
    --color-scrim: rgba(16, 24, 40, 0.45);
    /* The thin ring that keeps a user-chosen color (a label, a state dot) from vanishing into the
       surface, and the tick drawn on one. Neither color is a theme color, so neither retints. */
    --color-swatch-ring: rgb(0 0 0 / 12%);
    --color-swatch-check: #ffffff;
    --color-swatch-check-shadow: rgb(0 0 0 / 70%);
    /* Background-image hook for the login page and empty states ONLY — never behind message text or
       the composer. Off under prefers-reduced-motion (see the shared rule below). */
    --theme-decoration: none;
    /* How tall the decoration is drawn under an empty state's copy (it never sits behind the
       words). Zero while there is no decoration, so the empty state keeps its usual height. */
    --theme-decoration-space: 0px;
    /* The picker card's ground. A theme can give its card a swatch of its own here (Creamsicle's
       sherbet diagonal); everything else on the card still reads the theme's real tokens. */
    --theme-preview-ground: var(--color-surface-sunken);
    /* Extra ring on the open conversation's row in the list. None unless a theme asks for one. */
    --conversation-row-current-glow: none;
    /* A halo around the primary button. None unless a theme asks for one; never animated. */
    --btn-primary-glow: none;
    --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    --font-sans: var(--font-ui);
    /* Headings and display figures. */
    --font-display: var(--font-ui);
    /* The weight app headings are set in. `initial` leaves it unset, so each heading falls back to
       the weight it always had (var(--font-display-weight, 600)) — and a theme preview card does not
       inherit a different theme's weight from <html>. */
    --font-display-weight: initial;
    /* Letter-spacing for the small uppercase captions: sidebar sections, rail headings, group labels. */
    --tracking-caps: 0.04em;
    /* "See all"-style action links: a theme can set them as uppercase captions. */
    --action-link-transform: none;
    --action-link-tracking: normal;
    /* One place in the app needs it: a reply template's {{token}}, which has to be readable character
       for character because it is copied by hand. */
    --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    --radius-sm: 6px;
    --radius-md: 10px;
    --radius-lg: 14px;
    --radius-xl: 18px;
    --radius-pill: 999px;
    /* The internal-comment surface: the composer in comment mode and the card it produces. Mixed
       from the warning pair rather than picked as three more hexes, so the amber a comment is drawn
       in cannot drift away from the amber every other warning in the app uses. It is its own named
       surface because a comment is neither half of the correspondence — --warning-soft alone has no
       edge or band to build a bordered, banded box out of. */
    --color-comment-soft: var(--color-warning-soft);
    --color-comment-band: color-mix(in srgb, var(--color-warning) 8%, var(--color-warning-soft));
    --color-comment-band-strong: color-mix(in srgb, var(--color-warning) 16%, var(--color-warning-soft));
    --color-comment-border: color-mix(in srgb, var(--color-warning) 35%, var(--color-warning-soft));
    /* Softer than --color-border, for rules *inside* a bordered card. A full-strength border
       repeated on every table row and stat divider is what makes a dense panel look boxy. */
    --color-hairline: color-mix(in srgb, var(--color-border) 55%, transparent);
    /* Chart fills fade toward the baseline so a tall bar does not read as a solid slab.
       Built from the accent/success pair rather than new hexes, so a chart cannot drift
       away from the blue and green the rest of the app uses for in/out.
       The fade stops at 70%: a bar's height IS the datum, and fading its base to near-white
       made short bars read shorter than they are. */
    --gradient-accent: linear-gradient(180deg, var(--color-accent), color-mix(in srgb, var(--color-accent) 70%, transparent));
    --gradient-success: linear-gradient(180deg, var(--color-success), color-mix(in srgb, var(--color-success) 70%, transparent));
    /* The edges that used to be light-only hexes. Derived so a named theme only has to retint the
       primitives. */
    --color-accent-hover: color-mix(in srgb, var(--color-accent) 86%, #000000);
    --color-danger-edge: color-mix(in srgb, var(--color-danger) 28%, var(--color-danger-soft));
    --color-success-edge: color-mix(in srgb, var(--color-success) 28%, var(--color-success-soft));
    --color-accent-edge: color-mix(in srgb, var(--color-accent) 28%, var(--color-accent-soft));
    --color-surface: #1c2128;
    --color-surface-sunken: #111418;
    --color-surface-raised: #252b33;
    --color-border: #2e343c;
    --color-border-strong: #3d4450;
    --color-text: #f2f4f6;
    --color-muted: #9aa3af;
    --color-accent: #4c8dff;
    --color-accent-soft: color-mix(in srgb, var(--color-accent) 18%, var(--color-surface));
    /* White on #4c8dff is 3.2:1; the sunken ink is 5.8:1. */
    --color-on-accent: #111418;
    --color-danger: #f97066;
    --color-danger-soft: color-mix(in srgb, var(--color-danger) 16%, var(--color-surface));
    --color-success: #3bbf7a;
    --color-success-soft: color-mix(in srgb, var(--color-success) 16%, var(--color-surface));
    --color-warning: #f79009;
    --color-warning-soft: color-mix(in srgb, var(--color-warning) 16%, var(--color-surface));
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
    --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.45);
    --shadow-lg: 0 1px 2px rgba(0, 0, 0, 0.35), 0 8px 24px -8px rgba(0, 0, 0, 0.55);
    color-scheme: dark;
  }
}
/* Not :root-only on purpose: the picker stamps data-theme on each card's mini-preview, so a card
   paints from the real tokens of the theme it shows rather than from a hand-copied swatch. */
[data-theme=light] {
  /* Text and icons drawn on an accent fill (primary button, badges, send spinner). Per theme:
     white fails on gold, phosphor green or a pastel accent. */
  --color-on-accent: #ffffff;
  /* Accent used AS text — links, the selected nav item, counts. A separate token from the fill
     because a bright fill can be unreadable as text on the page (gold on white is 1.6:1). */
  --color-accent-text: var(--color-accent);
  --color-focus-ring: var(--color-accent);
  /* An accent-text link while the pointer is on it. Same as at rest unless a theme has a second
     highlight for it (Neon Flamingo's cyan). */
  --color-link-hover: var(--color-accent-text);
  --color-sidebar: var(--color-surface-sunken);
  --color-sidebar-text: var(--color-text);
  --color-sidebar-muted: var(--color-muted);
  /* The modal and command-palette backdrop. */
  --color-scrim: rgba(16, 24, 40, 0.45);
  /* The thin ring that keeps a user-chosen color (a label, a state dot) from vanishing into the
     surface, and the tick drawn on one. Neither color is a theme color, so neither retints. */
  --color-swatch-ring: rgb(0 0 0 / 12%);
  --color-swatch-check: #ffffff;
  --color-swatch-check-shadow: rgb(0 0 0 / 70%);
  /* Background-image hook for the login page and empty states ONLY — never behind message text or
     the composer. Off under prefers-reduced-motion (see the shared rule below). */
  --theme-decoration: none;
  /* How tall the decoration is drawn under an empty state's copy (it never sits behind the
     words). Zero while there is no decoration, so the empty state keeps its usual height. */
  --theme-decoration-space: 0px;
  /* The picker card's ground. A theme can give its card a swatch of its own here (Creamsicle's
     sherbet diagonal); everything else on the card still reads the theme's real tokens. */
  --theme-preview-ground: var(--color-surface-sunken);
  /* Extra ring on the open conversation's row in the list. None unless a theme asks for one. */
  --conversation-row-current-glow: none;
  /* A halo around the primary button. None unless a theme asks for one; never animated. */
  --btn-primary-glow: none;
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-sans: var(--font-ui);
  /* Headings and display figures. */
  --font-display: var(--font-ui);
  /* The weight app headings are set in. `initial` leaves it unset, so each heading falls back to
     the weight it always had (var(--font-display-weight, 600)) — and a theme preview card does not
     inherit a different theme's weight from <html>. */
  --font-display-weight: initial;
  /* Letter-spacing for the small uppercase captions: sidebar sections, rail headings, group labels. */
  --tracking-caps: 0.04em;
  /* "See all"-style action links: a theme can set them as uppercase captions. */
  --action-link-transform: none;
  --action-link-tracking: normal;
  /* One place in the app needs it: a reply template's {{token}}, which has to be readable character
     for character because it is copied by hand. */
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 14px;
  --radius-xl: 18px;
  --radius-pill: 999px;
  /* The internal-comment surface: the composer in comment mode and the card it produces. Mixed
     from the warning pair rather than picked as three more hexes, so the amber a comment is drawn
     in cannot drift away from the amber every other warning in the app uses. It is its own named
     surface because a comment is neither half of the correspondence — --warning-soft alone has no
     edge or band to build a bordered, banded box out of. */
  --color-comment-soft: var(--color-warning-soft);
  --color-comment-band: color-mix(in srgb, var(--color-warning) 8%, var(--color-warning-soft));
  --color-comment-band-strong: color-mix(in srgb, var(--color-warning) 16%, var(--color-warning-soft));
  --color-comment-border: color-mix(in srgb, var(--color-warning) 35%, var(--color-warning-soft));
  /* Softer than --color-border, for rules *inside* a bordered card. A full-strength border
     repeated on every table row and stat divider is what makes a dense panel look boxy. */
  --color-hairline: color-mix(in srgb, var(--color-border) 55%, transparent);
  /* Chart fills fade toward the baseline so a tall bar does not read as a solid slab.
     Built from the accent/success pair rather than new hexes, so a chart cannot drift
     away from the blue and green the rest of the app uses for in/out.
     The fade stops at 70%: a bar's height IS the datum, and fading its base to near-white
     made short bars read shorter than they are. */
  --gradient-accent: linear-gradient(180deg, var(--color-accent), color-mix(in srgb, var(--color-accent) 70%, transparent));
  --gradient-success: linear-gradient(180deg, var(--color-success), color-mix(in srgb, var(--color-success) 70%, transparent));
  /* The edges that used to be light-only hexes. Derived so a named theme only has to retint the
     primitives. */
  --color-accent-hover: color-mix(in srgb, var(--color-accent) 86%, #000000);
  --color-danger-edge: color-mix(in srgb, var(--color-danger) 28%, var(--color-danger-soft));
  --color-success-edge: color-mix(in srgb, var(--color-success) 28%, var(--color-success-soft));
  --color-accent-edge: color-mix(in srgb, var(--color-accent) 28%, var(--color-accent-soft));
  --color-surface: #ffffff;
  --color-surface-sunken: #f6f7f9;
  --color-surface-raised: #ffffff;
  --color-border: #e3e6ea;
  --color-border-strong: #cdd2d9;
  --color-text: #1b1f24;
  --color-muted: #6b7280;
  --color-accent: #2f6feb;
  --color-accent-soft: #eaf1fe;
  --color-danger: #b42318;
  --color-danger-soft: #fef3f2;
  --color-success: #067647;
  --color-success-soft: #ecfdf3;
  --color-warning: #b54708;
  --color-warning-soft: #fffaeb;
  --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06);
  --shadow-md: 0 4px 16px rgba(16, 24, 40, 0.10);
  --shadow-lg: 0 1px 2px rgba(16, 24, 40, 0.04), 0 8px 24px -8px rgba(16, 24, 40, 0.10);
  color-scheme: light;
}

[data-theme=dark] {
  /* Text and icons drawn on an accent fill (primary button, badges, send spinner). Per theme:
     white fails on gold, phosphor green or a pastel accent. */
  --color-on-accent: #ffffff;
  /* Accent used AS text — links, the selected nav item, counts. A separate token from the fill
     because a bright fill can be unreadable as text on the page (gold on white is 1.6:1). */
  --color-accent-text: var(--color-accent);
  --color-focus-ring: var(--color-accent);
  /* An accent-text link while the pointer is on it. Same as at rest unless a theme has a second
     highlight for it (Neon Flamingo's cyan). */
  --color-link-hover: var(--color-accent-text);
  --color-sidebar: var(--color-surface-sunken);
  --color-sidebar-text: var(--color-text);
  --color-sidebar-muted: var(--color-muted);
  /* The modal and command-palette backdrop. */
  --color-scrim: rgba(16, 24, 40, 0.45);
  /* The thin ring that keeps a user-chosen color (a label, a state dot) from vanishing into the
     surface, and the tick drawn on one. Neither color is a theme color, so neither retints. */
  --color-swatch-ring: rgb(0 0 0 / 12%);
  --color-swatch-check: #ffffff;
  --color-swatch-check-shadow: rgb(0 0 0 / 70%);
  /* Background-image hook for the login page and empty states ONLY — never behind message text or
     the composer. Off under prefers-reduced-motion (see the shared rule below). */
  --theme-decoration: none;
  /* How tall the decoration is drawn under an empty state's copy (it never sits behind the
     words). Zero while there is no decoration, so the empty state keeps its usual height. */
  --theme-decoration-space: 0px;
  /* The picker card's ground. A theme can give its card a swatch of its own here (Creamsicle's
     sherbet diagonal); everything else on the card still reads the theme's real tokens. */
  --theme-preview-ground: var(--color-surface-sunken);
  /* Extra ring on the open conversation's row in the list. None unless a theme asks for one. */
  --conversation-row-current-glow: none;
  /* A halo around the primary button. None unless a theme asks for one; never animated. */
  --btn-primary-glow: none;
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-sans: var(--font-ui);
  /* Headings and display figures. */
  --font-display: var(--font-ui);
  /* The weight app headings are set in. `initial` leaves it unset, so each heading falls back to
     the weight it always had (var(--font-display-weight, 600)) — and a theme preview card does not
     inherit a different theme's weight from <html>. */
  --font-display-weight: initial;
  /* Letter-spacing for the small uppercase captions: sidebar sections, rail headings, group labels. */
  --tracking-caps: 0.04em;
  /* "See all"-style action links: a theme can set them as uppercase captions. */
  --action-link-transform: none;
  --action-link-tracking: normal;
  /* One place in the app needs it: a reply template's {{token}}, which has to be readable character
     for character because it is copied by hand. */
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 14px;
  --radius-xl: 18px;
  --radius-pill: 999px;
  /* The internal-comment surface: the composer in comment mode and the card it produces. Mixed
     from the warning pair rather than picked as three more hexes, so the amber a comment is drawn
     in cannot drift away from the amber every other warning in the app uses. It is its own named
     surface because a comment is neither half of the correspondence — --warning-soft alone has no
     edge or band to build a bordered, banded box out of. */
  --color-comment-soft: var(--color-warning-soft);
  --color-comment-band: color-mix(in srgb, var(--color-warning) 8%, var(--color-warning-soft));
  --color-comment-band-strong: color-mix(in srgb, var(--color-warning) 16%, var(--color-warning-soft));
  --color-comment-border: color-mix(in srgb, var(--color-warning) 35%, var(--color-warning-soft));
  /* Softer than --color-border, for rules *inside* a bordered card. A full-strength border
     repeated on every table row and stat divider is what makes a dense panel look boxy. */
  --color-hairline: color-mix(in srgb, var(--color-border) 55%, transparent);
  /* Chart fills fade toward the baseline so a tall bar does not read as a solid slab.
     Built from the accent/success pair rather than new hexes, so a chart cannot drift
     away from the blue and green the rest of the app uses for in/out.
     The fade stops at 70%: a bar's height IS the datum, and fading its base to near-white
     made short bars read shorter than they are. */
  --gradient-accent: linear-gradient(180deg, var(--color-accent), color-mix(in srgb, var(--color-accent) 70%, transparent));
  --gradient-success: linear-gradient(180deg, var(--color-success), color-mix(in srgb, var(--color-success) 70%, transparent));
  /* The edges that used to be light-only hexes. Derived so a named theme only has to retint the
     primitives. */
  --color-accent-hover: color-mix(in srgb, var(--color-accent) 86%, #000000);
  --color-danger-edge: color-mix(in srgb, var(--color-danger) 28%, var(--color-danger-soft));
  --color-success-edge: color-mix(in srgb, var(--color-success) 28%, var(--color-success-soft));
  --color-accent-edge: color-mix(in srgb, var(--color-accent) 28%, var(--color-accent-soft));
  --color-surface: #1c2128;
  --color-surface-sunken: #111418;
  --color-surface-raised: #252b33;
  --color-border: #2e343c;
  --color-border-strong: #3d4450;
  --color-text: #f2f4f6;
  --color-muted: #9aa3af;
  --color-accent: #4c8dff;
  --color-accent-soft: color-mix(in srgb, var(--color-accent) 18%, var(--color-surface));
  /* White on #4c8dff is 3.2:1; the sunken ink is 5.8:1. */
  --color-on-accent: #111418;
  --color-danger: #f97066;
  --color-danger-soft: color-mix(in srgb, var(--color-danger) 16%, var(--color-surface));
  --color-success: #3bbf7a;
  --color-success-soft: color-mix(in srgb, var(--color-success) 16%, var(--color-surface));
  --color-warning: #f79009;
  --color-warning-soft: color-mix(in srgb, var(--color-warning) 16%, var(--color-surface));
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
  --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.45);
  --shadow-lg: 0 1px 2px rgba(0, 0, 0, 0.35), 0 8px 24px -8px rgba(0, 0, 0, 0.55);
  color-scheme: dark;
}

[data-theme=aubergine] {
  /* Text and icons drawn on an accent fill (primary button, badges, send spinner). Per theme:
     white fails on gold, phosphor green or a pastel accent. */
  --color-on-accent: #ffffff;
  /* Accent used AS text — links, the selected nav item, counts. A separate token from the fill
     because a bright fill can be unreadable as text on the page (gold on white is 1.6:1). */
  --color-accent-text: var(--color-accent);
  --color-focus-ring: var(--color-accent);
  /* An accent-text link while the pointer is on it. Same as at rest unless a theme has a second
     highlight for it (Neon Flamingo's cyan). */
  --color-link-hover: var(--color-accent-text);
  --color-sidebar: var(--color-surface-sunken);
  --color-sidebar-text: var(--color-text);
  --color-sidebar-muted: var(--color-muted);
  /* The modal and command-palette backdrop. */
  --color-scrim: rgba(16, 24, 40, 0.45);
  /* The thin ring that keeps a user-chosen color (a label, a state dot) from vanishing into the
     surface, and the tick drawn on one. Neither color is a theme color, so neither retints. */
  --color-swatch-ring: rgb(0 0 0 / 12%);
  --color-swatch-check: #ffffff;
  --color-swatch-check-shadow: rgb(0 0 0 / 70%);
  /* Background-image hook for the login page and empty states ONLY — never behind message text or
     the composer. Off under prefers-reduced-motion (see the shared rule below). */
  --theme-decoration: none;
  /* How tall the decoration is drawn under an empty state's copy (it never sits behind the
     words). Zero while there is no decoration, so the empty state keeps its usual height. */
  --theme-decoration-space: 0px;
  /* The picker card's ground. A theme can give its card a swatch of its own here (Creamsicle's
     sherbet diagonal); everything else on the card still reads the theme's real tokens. */
  --theme-preview-ground: var(--color-surface-sunken);
  /* Extra ring on the open conversation's row in the list. None unless a theme asks for one. */
  --conversation-row-current-glow: none;
  /* A halo around the primary button. None unless a theme asks for one; never animated. */
  --btn-primary-glow: none;
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-sans: var(--font-ui);
  /* Headings and display figures. */
  --font-display: var(--font-ui);
  /* The weight app headings are set in. `initial` leaves it unset, so each heading falls back to
     the weight it always had (var(--font-display-weight, 600)) — and a theme preview card does not
     inherit a different theme's weight from <html>. */
  --font-display-weight: initial;
  /* Letter-spacing for the small uppercase captions: sidebar sections, rail headings, group labels. */
  --tracking-caps: 0.04em;
  /* "See all"-style action links: a theme can set them as uppercase captions. */
  --action-link-transform: none;
  --action-link-tracking: normal;
  /* One place in the app needs it: a reply template's {{token}}, which has to be readable character
     for character because it is copied by hand. */
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 14px;
  --radius-xl: 18px;
  --radius-pill: 999px;
  /* The internal-comment surface: the composer in comment mode and the card it produces. Mixed
     from the warning pair rather than picked as three more hexes, so the amber a comment is drawn
     in cannot drift away from the amber every other warning in the app uses. It is its own named
     surface because a comment is neither half of the correspondence — --warning-soft alone has no
     edge or band to build a bordered, banded box out of. */
  --color-comment-soft: var(--color-warning-soft);
  --color-comment-band: color-mix(in srgb, var(--color-warning) 8%, var(--color-warning-soft));
  --color-comment-band-strong: color-mix(in srgb, var(--color-warning) 16%, var(--color-warning-soft));
  --color-comment-border: color-mix(in srgb, var(--color-warning) 35%, var(--color-warning-soft));
  /* Softer than --color-border, for rules *inside* a bordered card. A full-strength border
     repeated on every table row and stat divider is what makes a dense panel look boxy. */
  --color-hairline: color-mix(in srgb, var(--color-border) 55%, transparent);
  /* Chart fills fade toward the baseline so a tall bar does not read as a solid slab.
     Built from the accent/success pair rather than new hexes, so a chart cannot drift
     away from the blue and green the rest of the app uses for in/out.
     The fade stops at 70%: a bar's height IS the datum, and fading its base to near-white
     made short bars read shorter than they are. */
  --gradient-accent: linear-gradient(180deg, var(--color-accent), color-mix(in srgb, var(--color-accent) 70%, transparent));
  --gradient-success: linear-gradient(180deg, var(--color-success), color-mix(in srgb, var(--color-success) 70%, transparent));
  /* The edges that used to be light-only hexes. Derived so a named theme only has to retint the
     primitives. */
  --color-accent-hover: color-mix(in srgb, var(--color-accent) 86%, #000000);
  --color-danger-edge: color-mix(in srgb, var(--color-danger) 28%, var(--color-danger-soft));
  --color-success-edge: color-mix(in srgb, var(--color-success) 28%, var(--color-success-soft));
  --color-accent-edge: color-mix(in srgb, var(--color-accent) 28%, var(--color-accent-soft));
  --color-surface: #1c2128;
  --color-surface-sunken: #111418;
  --color-surface-raised: #252b33;
  --color-border: #2e343c;
  --color-border-strong: #3d4450;
  --color-text: #f2f4f6;
  --color-muted: #9aa3af;
  --color-accent: #4c8dff;
  --color-accent-soft: color-mix(in srgb, var(--color-accent) 18%, var(--color-surface));
  /* White on #4c8dff is 3.2:1; the sunken ink is 5.8:1. */
  --color-on-accent: #111418;
  --color-danger: #f97066;
  --color-danger-soft: color-mix(in srgb, var(--color-danger) 16%, var(--color-surface));
  --color-success: #3bbf7a;
  --color-success-soft: color-mix(in srgb, var(--color-success) 16%, var(--color-surface));
  --color-warning: #f79009;
  --color-warning-soft: color-mix(in srgb, var(--color-warning) 16%, var(--color-surface));
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
  --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.45);
  --shadow-lg: 0 1px 2px rgba(0, 0, 0, 0.35), 0 8px 24px -8px rgba(0, 0, 0, 0.55);
  color-scheme: dark;
  --color-surface: #3f1242;
  --color-surface-sunken: #2a0c2d;
  --color-surface-raised: #4a154b;
  --color-border: #5c2a5e;
  --color-border-strong: #7a3d7c;
  --color-text: #f7eef7;
  --color-muted: #c9a8cb;
  --color-accent: #e01e5a;
  --color-on-accent: #ffffff;
  /* The #e01e5a fill as link text on plum is 3.3:1. */
  --color-accent-text: #ff6b9a;
  /* Midnight's #f97066 on its own plum-tinted soft (an alert toast) is 4.2:1. */
  --color-danger: #ff8077;
}

[data-theme=hoth] {
  /* Text and icons drawn on an accent fill (primary button, badges, send spinner). Per theme:
     white fails on gold, phosphor green or a pastel accent. */
  --color-on-accent: #ffffff;
  /* Accent used AS text — links, the selected nav item, counts. A separate token from the fill
     because a bright fill can be unreadable as text on the page (gold on white is 1.6:1). */
  --color-accent-text: var(--color-accent);
  --color-focus-ring: var(--color-accent);
  /* An accent-text link while the pointer is on it. Same as at rest unless a theme has a second
     highlight for it (Neon Flamingo's cyan). */
  --color-link-hover: var(--color-accent-text);
  --color-sidebar: var(--color-surface-sunken);
  --color-sidebar-text: var(--color-text);
  --color-sidebar-muted: var(--color-muted);
  /* The modal and command-palette backdrop. */
  --color-scrim: rgba(16, 24, 40, 0.45);
  /* The thin ring that keeps a user-chosen color (a label, a state dot) from vanishing into the
     surface, and the tick drawn on one. Neither color is a theme color, so neither retints. */
  --color-swatch-ring: rgb(0 0 0 / 12%);
  --color-swatch-check: #ffffff;
  --color-swatch-check-shadow: rgb(0 0 0 / 70%);
  /* Background-image hook for the login page and empty states ONLY — never behind message text or
     the composer. Off under prefers-reduced-motion (see the shared rule below). */
  --theme-decoration: none;
  /* How tall the decoration is drawn under an empty state's copy (it never sits behind the
     words). Zero while there is no decoration, so the empty state keeps its usual height. */
  --theme-decoration-space: 0px;
  /* The picker card's ground. A theme can give its card a swatch of its own here (Creamsicle's
     sherbet diagonal); everything else on the card still reads the theme's real tokens. */
  --theme-preview-ground: var(--color-surface-sunken);
  /* Extra ring on the open conversation's row in the list. None unless a theme asks for one. */
  --conversation-row-current-glow: none;
  /* A halo around the primary button. None unless a theme asks for one; never animated. */
  --btn-primary-glow: none;
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-sans: var(--font-ui);
  /* Headings and display figures. */
  --font-display: var(--font-ui);
  /* The weight app headings are set in. `initial` leaves it unset, so each heading falls back to
     the weight it always had (var(--font-display-weight, 600)) — and a theme preview card does not
     inherit a different theme's weight from <html>. */
  --font-display-weight: initial;
  /* Letter-spacing for the small uppercase captions: sidebar sections, rail headings, group labels. */
  --tracking-caps: 0.04em;
  /* "See all"-style action links: a theme can set them as uppercase captions. */
  --action-link-transform: none;
  --action-link-tracking: normal;
  /* One place in the app needs it: a reply template's {{token}}, which has to be readable character
     for character because it is copied by hand. */
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 14px;
  --radius-xl: 18px;
  --radius-pill: 999px;
  /* The internal-comment surface: the composer in comment mode and the card it produces. Mixed
     from the warning pair rather than picked as three more hexes, so the amber a comment is drawn
     in cannot drift away from the amber every other warning in the app uses. It is its own named
     surface because a comment is neither half of the correspondence — --warning-soft alone has no
     edge or band to build a bordered, banded box out of. */
  --color-comment-soft: var(--color-warning-soft);
  --color-comment-band: color-mix(in srgb, var(--color-warning) 8%, var(--color-warning-soft));
  --color-comment-band-strong: color-mix(in srgb, var(--color-warning) 16%, var(--color-warning-soft));
  --color-comment-border: color-mix(in srgb, var(--color-warning) 35%, var(--color-warning-soft));
  /* Softer than --color-border, for rules *inside* a bordered card. A full-strength border
     repeated on every table row and stat divider is what makes a dense panel look boxy. */
  --color-hairline: color-mix(in srgb, var(--color-border) 55%, transparent);
  /* Chart fills fade toward the baseline so a tall bar does not read as a solid slab.
     Built from the accent/success pair rather than new hexes, so a chart cannot drift
     away from the blue and green the rest of the app uses for in/out.
     The fade stops at 70%: a bar's height IS the datum, and fading its base to near-white
     made short bars read shorter than they are. */
  --gradient-accent: linear-gradient(180deg, var(--color-accent), color-mix(in srgb, var(--color-accent) 70%, transparent));
  --gradient-success: linear-gradient(180deg, var(--color-success), color-mix(in srgb, var(--color-success) 70%, transparent));
  /* The edges that used to be light-only hexes. Derived so a named theme only has to retint the
     primitives. */
  --color-accent-hover: color-mix(in srgb, var(--color-accent) 86%, #000000);
  --color-danger-edge: color-mix(in srgb, var(--color-danger) 28%, var(--color-danger-soft));
  --color-success-edge: color-mix(in srgb, var(--color-success) 28%, var(--color-success-soft));
  --color-accent-edge: color-mix(in srgb, var(--color-accent) 28%, var(--color-accent-soft));
  --color-surface: #ffffff;
  --color-surface-sunken: #f6f7f9;
  --color-surface-raised: #ffffff;
  --color-border: #e3e6ea;
  --color-border-strong: #cdd2d9;
  --color-text: #1b1f24;
  --color-muted: #6b7280;
  --color-accent: #2f6feb;
  --color-accent-soft: #eaf1fe;
  --color-danger: #b42318;
  --color-danger-soft: #fef3f2;
  --color-success: #067647;
  --color-success-soft: #ecfdf3;
  --color-warning: #b54708;
  --color-warning-soft: #fffaeb;
  --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06);
  --shadow-md: 0 4px 16px rgba(16, 24, 40, 0.10);
  --shadow-lg: 0 1px 2px rgba(16, 24, 40, 0.04), 0 8px 24px -8px rgba(16, 24, 40, 0.10);
  color-scheme: light;
  --color-surface: #f7f9fc;
  --color-surface-sunken: #e8eef5;
  --color-surface-raised: #ffffff;
  --color-border: #d0dae6;
  --color-border-strong: #b7c4d4;
  --color-text: #1b2838;
  --color-muted: #5b6b7c;
  --color-accent: #0b5cab;
  --color-accent-soft: #d7e6f6;
}

[data-theme=tms] {
  /* Text and icons drawn on an accent fill (primary button, badges, send spinner). Per theme:
     white fails on gold, phosphor green or a pastel accent. */
  --color-on-accent: #ffffff;
  /* Accent used AS text — links, the selected nav item, counts. A separate token from the fill
     because a bright fill can be unreadable as text on the page (gold on white is 1.6:1). */
  --color-accent-text: var(--color-accent);
  --color-focus-ring: var(--color-accent);
  /* An accent-text link while the pointer is on it. Same as at rest unless a theme has a second
     highlight for it (Neon Flamingo's cyan). */
  --color-link-hover: var(--color-accent-text);
  --color-sidebar: var(--color-surface-sunken);
  --color-sidebar-text: var(--color-text);
  --color-sidebar-muted: var(--color-muted);
  /* The modal and command-palette backdrop. */
  --color-scrim: rgba(16, 24, 40, 0.45);
  /* The thin ring that keeps a user-chosen color (a label, a state dot) from vanishing into the
     surface, and the tick drawn on one. Neither color is a theme color, so neither retints. */
  --color-swatch-ring: rgb(0 0 0 / 12%);
  --color-swatch-check: #ffffff;
  --color-swatch-check-shadow: rgb(0 0 0 / 70%);
  /* Background-image hook for the login page and empty states ONLY — never behind message text or
     the composer. Off under prefers-reduced-motion (see the shared rule below). */
  --theme-decoration: none;
  /* How tall the decoration is drawn under an empty state's copy (it never sits behind the
     words). Zero while there is no decoration, so the empty state keeps its usual height. */
  --theme-decoration-space: 0px;
  /* The picker card's ground. A theme can give its card a swatch of its own here (Creamsicle's
     sherbet diagonal); everything else on the card still reads the theme's real tokens. */
  --theme-preview-ground: var(--color-surface-sunken);
  /* Extra ring on the open conversation's row in the list. None unless a theme asks for one. */
  --conversation-row-current-glow: none;
  /* A halo around the primary button. None unless a theme asks for one; never animated. */
  --btn-primary-glow: none;
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-sans: var(--font-ui);
  /* Headings and display figures. */
  --font-display: var(--font-ui);
  /* The weight app headings are set in. `initial` leaves it unset, so each heading falls back to
     the weight it always had (var(--font-display-weight, 600)) — and a theme preview card does not
     inherit a different theme's weight from <html>. */
  --font-display-weight: initial;
  /* Letter-spacing for the small uppercase captions: sidebar sections, rail headings, group labels. */
  --tracking-caps: 0.04em;
  /* "See all"-style action links: a theme can set them as uppercase captions. */
  --action-link-transform: none;
  --action-link-tracking: normal;
  /* One place in the app needs it: a reply template's {{token}}, which has to be readable character
     for character because it is copied by hand. */
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 14px;
  --radius-xl: 18px;
  --radius-pill: 999px;
  /* The internal-comment surface: the composer in comment mode and the card it produces. Mixed
     from the warning pair rather than picked as three more hexes, so the amber a comment is drawn
     in cannot drift away from the amber every other warning in the app uses. It is its own named
     surface because a comment is neither half of the correspondence — --warning-soft alone has no
     edge or band to build a bordered, banded box out of. */
  --color-comment-soft: var(--color-warning-soft);
  --color-comment-band: color-mix(in srgb, var(--color-warning) 8%, var(--color-warning-soft));
  --color-comment-band-strong: color-mix(in srgb, var(--color-warning) 16%, var(--color-warning-soft));
  --color-comment-border: color-mix(in srgb, var(--color-warning) 35%, var(--color-warning-soft));
  /* Softer than --color-border, for rules *inside* a bordered card. A full-strength border
     repeated on every table row and stat divider is what makes a dense panel look boxy. */
  --color-hairline: color-mix(in srgb, var(--color-border) 55%, transparent);
  /* Chart fills fade toward the baseline so a tall bar does not read as a solid slab.
     Built from the accent/success pair rather than new hexes, so a chart cannot drift
     away from the blue and green the rest of the app uses for in/out.
     The fade stops at 70%: a bar's height IS the datum, and fading its base to near-white
     made short bars read shorter than they are. */
  --gradient-accent: linear-gradient(180deg, var(--color-accent), color-mix(in srgb, var(--color-accent) 70%, transparent));
  --gradient-success: linear-gradient(180deg, var(--color-success), color-mix(in srgb, var(--color-success) 70%, transparent));
  /* The edges that used to be light-only hexes. Derived so a named theme only has to retint the
     primitives. */
  --color-accent-hover: color-mix(in srgb, var(--color-accent) 86%, #000000);
  --color-danger-edge: color-mix(in srgb, var(--color-danger) 28%, var(--color-danger-soft));
  --color-success-edge: color-mix(in srgb, var(--color-success) 28%, var(--color-success-soft));
  --color-accent-edge: color-mix(in srgb, var(--color-accent) 28%, var(--color-accent-soft));
  --color-surface: #ffffff;
  --color-surface-sunken: #f6f7f9;
  --color-surface-raised: #ffffff;
  --color-border: #e3e6ea;
  --color-border-strong: #cdd2d9;
  --color-text: #1b1f24;
  --color-muted: #6b7280;
  --color-accent: #2f6feb;
  --color-accent-soft: #eaf1fe;
  --color-danger: #b42318;
  --color-danger-soft: #fef3f2;
  --color-success: #067647;
  --color-success-soft: #ecfdf3;
  --color-warning: #b54708;
  --color-warning-soft: #fffaeb;
  --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06);
  --shadow-md: 0 4px 16px rgba(16, 24, 40, 0.10);
  --shadow-lg: 0 1px 2px rgba(16, 24, 40, 0.04), 0 8px 24px -8px rgba(16, 24, 40, 0.10);
  color-scheme: light;
  --color-surface: #ffffff;
  --color-surface-sunken: #f6f6ff;
  --color-surface-raised: #ffffff;
  --color-border: #e8e6f4;
  --color-border-strong: #ceccfb;
  --color-text: #1a1c43;
  --color-muted: #6b6880;
  --color-accent: #6259fe;
  --color-accent-soft: #efeefe;
}

[data-theme=gotham] {
  /* Text and icons drawn on an accent fill (primary button, badges, send spinner). Per theme:
     white fails on gold, phosphor green or a pastel accent. */
  --color-on-accent: #ffffff;
  /* Accent used AS text — links, the selected nav item, counts. A separate token from the fill
     because a bright fill can be unreadable as text on the page (gold on white is 1.6:1). */
  --color-accent-text: var(--color-accent);
  --color-focus-ring: var(--color-accent);
  /* An accent-text link while the pointer is on it. Same as at rest unless a theme has a second
     highlight for it (Neon Flamingo's cyan). */
  --color-link-hover: var(--color-accent-text);
  --color-sidebar: var(--color-surface-sunken);
  --color-sidebar-text: var(--color-text);
  --color-sidebar-muted: var(--color-muted);
  /* The modal and command-palette backdrop. */
  --color-scrim: rgba(16, 24, 40, 0.45);
  /* The thin ring that keeps a user-chosen color (a label, a state dot) from vanishing into the
     surface, and the tick drawn on one. Neither color is a theme color, so neither retints. */
  --color-swatch-ring: rgb(0 0 0 / 12%);
  --color-swatch-check: #ffffff;
  --color-swatch-check-shadow: rgb(0 0 0 / 70%);
  /* Background-image hook for the login page and empty states ONLY — never behind message text or
     the composer. Off under prefers-reduced-motion (see the shared rule below). */
  --theme-decoration: none;
  /* How tall the decoration is drawn under an empty state's copy (it never sits behind the
     words). Zero while there is no decoration, so the empty state keeps its usual height. */
  --theme-decoration-space: 0px;
  /* The picker card's ground. A theme can give its card a swatch of its own here (Creamsicle's
     sherbet diagonal); everything else on the card still reads the theme's real tokens. */
  --theme-preview-ground: var(--color-surface-sunken);
  /* Extra ring on the open conversation's row in the list. None unless a theme asks for one. */
  --conversation-row-current-glow: none;
  /* A halo around the primary button. None unless a theme asks for one; never animated. */
  --btn-primary-glow: none;
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-sans: var(--font-ui);
  /* Headings and display figures. */
  --font-display: var(--font-ui);
  /* The weight app headings are set in. `initial` leaves it unset, so each heading falls back to
     the weight it always had (var(--font-display-weight, 600)) — and a theme preview card does not
     inherit a different theme's weight from <html>. */
  --font-display-weight: initial;
  /* Letter-spacing for the small uppercase captions: sidebar sections, rail headings, group labels. */
  --tracking-caps: 0.04em;
  /* "See all"-style action links: a theme can set them as uppercase captions. */
  --action-link-transform: none;
  --action-link-tracking: normal;
  /* One place in the app needs it: a reply template's {{token}}, which has to be readable character
     for character because it is copied by hand. */
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 14px;
  --radius-xl: 18px;
  --radius-pill: 999px;
  /* The internal-comment surface: the composer in comment mode and the card it produces. Mixed
     from the warning pair rather than picked as three more hexes, so the amber a comment is drawn
     in cannot drift away from the amber every other warning in the app uses. It is its own named
     surface because a comment is neither half of the correspondence — --warning-soft alone has no
     edge or band to build a bordered, banded box out of. */
  --color-comment-soft: var(--color-warning-soft);
  --color-comment-band: color-mix(in srgb, var(--color-warning) 8%, var(--color-warning-soft));
  --color-comment-band-strong: color-mix(in srgb, var(--color-warning) 16%, var(--color-warning-soft));
  --color-comment-border: color-mix(in srgb, var(--color-warning) 35%, var(--color-warning-soft));
  /* Softer than --color-border, for rules *inside* a bordered card. A full-strength border
     repeated on every table row and stat divider is what makes a dense panel look boxy. */
  --color-hairline: color-mix(in srgb, var(--color-border) 55%, transparent);
  /* Chart fills fade toward the baseline so a tall bar does not read as a solid slab.
     Built from the accent/success pair rather than new hexes, so a chart cannot drift
     away from the blue and green the rest of the app uses for in/out.
     The fade stops at 70%: a bar's height IS the datum, and fading its base to near-white
     made short bars read shorter than they are. */
  --gradient-accent: linear-gradient(180deg, var(--color-accent), color-mix(in srgb, var(--color-accent) 70%, transparent));
  --gradient-success: linear-gradient(180deg, var(--color-success), color-mix(in srgb, var(--color-success) 70%, transparent));
  /* The edges that used to be light-only hexes. Derived so a named theme only has to retint the
     primitives. */
  --color-accent-hover: color-mix(in srgb, var(--color-accent) 86%, #000000);
  --color-danger-edge: color-mix(in srgb, var(--color-danger) 28%, var(--color-danger-soft));
  --color-success-edge: color-mix(in srgb, var(--color-success) 28%, var(--color-success-soft));
  --color-accent-edge: color-mix(in srgb, var(--color-accent) 28%, var(--color-accent-soft));
  --color-surface: #1c2128;
  --color-surface-sunken: #111418;
  --color-surface-raised: #252b33;
  --color-border: #2e343c;
  --color-border-strong: #3d4450;
  --color-text: #f2f4f6;
  --color-muted: #9aa3af;
  --color-accent: #4c8dff;
  --color-accent-soft: color-mix(in srgb, var(--color-accent) 18%, var(--color-surface));
  /* White on #4c8dff is 3.2:1; the sunken ink is 5.8:1. */
  --color-on-accent: #111418;
  --color-danger: #f97066;
  --color-danger-soft: color-mix(in srgb, var(--color-danger) 16%, var(--color-surface));
  --color-success: #3bbf7a;
  --color-success-soft: color-mix(in srgb, var(--color-success) 16%, var(--color-surface));
  --color-warning: #f79009;
  --color-warning-soft: color-mix(in srgb, var(--color-warning) 16%, var(--color-surface));
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
  --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.45);
  --shadow-lg: 0 1px 2px rgba(0, 0, 0, 0.35), 0 8px 24px -8px rgba(0, 0, 0, 0.55);
  color-scheme: dark;
  --color-surface: #111111; /* surface */
  --color-surface-sunken: #000000; /* page-bg, which is also the sidebar */
  --color-surface-raised: #222222; /* surface-raised */
  --color-border: #333333; /* divider */
  --color-border-strong: #404040; /* hairline */
  --color-text: #ffffff; /* text-primary */
  --color-muted: #b3b3b3; /* text-muted, rgba(255 255 255 / 0.7) flattened on surface */
  --color-accent: #ffc03a; /* $genghis-accent */
  --color-accent-hover: #ffcf66; /* accent-light: gold lightens on hover, it does not dim */
  /* Dark ink on gold, never white — white on #ffc03a is 1.6:1. */
  --color-on-accent: #111111;
  --color-danger: #e86a5a; /* danger-light */
  --color-success: #2ed9a8; /* success-light */
  /* warning-light: orange, so a warning cannot be mistaken for the gold accent. */
  --color-warning: #ff9500;
  --font-display: "Libre Caslon Display", Georgia, serif;
  --font-ui: "Montserrat", Verdana, sans-serif;
  /* Genghis display-1 / heading-* weight. */
  --font-display-weight: 300;
  /* Genghis heading-5 / heading-6. */
  --tracking-caps: 1.5px;
  --action-link-transform: uppercase;
  --action-link-tracking: 1.5px;
  /* Thin architectural line art — an arcade of arches and a stair — in the divider grey. */
  --theme-decoration: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 240 64' fill='none' stroke='%23333333' stroke-width='1' stroke-linecap='square'%3E%3Cpath d='M0 63.5h240'/%3E%3Cpath d='M12 63.5V34a14 14 0 0 1 28 0v29.5M48 63.5V34a14 14 0 0 1 28 0v29.5M84 63.5V34a14 14 0 0 1 28 0v29.5'/%3E%3Cpath d='M6 63.5V18h112v45.5M6 12h112'/%3E%3Cpath d='M132 63.5h12v-8h12v-8h12v-8h12v-8h12v-8h12v-8h12v-8h12'/%3E%3Cpath d='M138 55.5l84-56'/%3E%3C/svg%3E");
  --theme-decoration-space: 40px;
}

[data-theme=creamsicle] {
  /* Text and icons drawn on an accent fill (primary button, badges, send spinner). Per theme:
     white fails on gold, phosphor green or a pastel accent. */
  --color-on-accent: #ffffff;
  /* Accent used AS text — links, the selected nav item, counts. A separate token from the fill
     because a bright fill can be unreadable as text on the page (gold on white is 1.6:1). */
  --color-accent-text: var(--color-accent);
  --color-focus-ring: var(--color-accent);
  /* An accent-text link while the pointer is on it. Same as at rest unless a theme has a second
     highlight for it (Neon Flamingo's cyan). */
  --color-link-hover: var(--color-accent-text);
  --color-sidebar: var(--color-surface-sunken);
  --color-sidebar-text: var(--color-text);
  --color-sidebar-muted: var(--color-muted);
  /* The modal and command-palette backdrop. */
  --color-scrim: rgba(16, 24, 40, 0.45);
  /* The thin ring that keeps a user-chosen color (a label, a state dot) from vanishing into the
     surface, and the tick drawn on one. Neither color is a theme color, so neither retints. */
  --color-swatch-ring: rgb(0 0 0 / 12%);
  --color-swatch-check: #ffffff;
  --color-swatch-check-shadow: rgb(0 0 0 / 70%);
  /* Background-image hook for the login page and empty states ONLY — never behind message text or
     the composer. Off under prefers-reduced-motion (see the shared rule below). */
  --theme-decoration: none;
  /* How tall the decoration is drawn under an empty state's copy (it never sits behind the
     words). Zero while there is no decoration, so the empty state keeps its usual height. */
  --theme-decoration-space: 0px;
  /* The picker card's ground. A theme can give its card a swatch of its own here (Creamsicle's
     sherbet diagonal); everything else on the card still reads the theme's real tokens. */
  --theme-preview-ground: var(--color-surface-sunken);
  /* Extra ring on the open conversation's row in the list. None unless a theme asks for one. */
  --conversation-row-current-glow: none;
  /* A halo around the primary button. None unless a theme asks for one; never animated. */
  --btn-primary-glow: none;
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-sans: var(--font-ui);
  /* Headings and display figures. */
  --font-display: var(--font-ui);
  /* The weight app headings are set in. `initial` leaves it unset, so each heading falls back to
     the weight it always had (var(--font-display-weight, 600)) — and a theme preview card does not
     inherit a different theme's weight from <html>. */
  --font-display-weight: initial;
  /* Letter-spacing for the small uppercase captions: sidebar sections, rail headings, group labels. */
  --tracking-caps: 0.04em;
  /* "See all"-style action links: a theme can set them as uppercase captions. */
  --action-link-transform: none;
  --action-link-tracking: normal;
  /* One place in the app needs it: a reply template's {{token}}, which has to be readable character
     for character because it is copied by hand. */
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 14px;
  --radius-xl: 18px;
  --radius-pill: 999px;
  /* The internal-comment surface: the composer in comment mode and the card it produces. Mixed
     from the warning pair rather than picked as three more hexes, so the amber a comment is drawn
     in cannot drift away from the amber every other warning in the app uses. It is its own named
     surface because a comment is neither half of the correspondence — --warning-soft alone has no
     edge or band to build a bordered, banded box out of. */
  --color-comment-soft: var(--color-warning-soft);
  --color-comment-band: color-mix(in srgb, var(--color-warning) 8%, var(--color-warning-soft));
  --color-comment-band-strong: color-mix(in srgb, var(--color-warning) 16%, var(--color-warning-soft));
  --color-comment-border: color-mix(in srgb, var(--color-warning) 35%, var(--color-warning-soft));
  /* Softer than --color-border, for rules *inside* a bordered card. A full-strength border
     repeated on every table row and stat divider is what makes a dense panel look boxy. */
  --color-hairline: color-mix(in srgb, var(--color-border) 55%, transparent);
  /* Chart fills fade toward the baseline so a tall bar does not read as a solid slab.
     Built from the accent/success pair rather than new hexes, so a chart cannot drift
     away from the blue and green the rest of the app uses for in/out.
     The fade stops at 70%: a bar's height IS the datum, and fading its base to near-white
     made short bars read shorter than they are. */
  --gradient-accent: linear-gradient(180deg, var(--color-accent), color-mix(in srgb, var(--color-accent) 70%, transparent));
  --gradient-success: linear-gradient(180deg, var(--color-success), color-mix(in srgb, var(--color-success) 70%, transparent));
  /* The edges that used to be light-only hexes. Derived so a named theme only has to retint the
     primitives. */
  --color-accent-hover: color-mix(in srgb, var(--color-accent) 86%, #000000);
  --color-danger-edge: color-mix(in srgb, var(--color-danger) 28%, var(--color-danger-soft));
  --color-success-edge: color-mix(in srgb, var(--color-success) 28%, var(--color-success-soft));
  --color-accent-edge: color-mix(in srgb, var(--color-accent) 28%, var(--color-accent-soft));
  --color-surface: #ffffff;
  --color-surface-sunken: #f6f7f9;
  --color-surface-raised: #ffffff;
  --color-border: #e3e6ea;
  --color-border-strong: #cdd2d9;
  --color-text: #1b1f24;
  --color-muted: #6b7280;
  --color-accent: #2f6feb;
  --color-accent-soft: #eaf1fe;
  --color-danger: #b42318;
  --color-danger-soft: #fef3f2;
  --color-success: #067647;
  --color-success-soft: #ecfdf3;
  --color-warning: #b54708;
  --color-warning-soft: #fffaeb;
  --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06);
  --shadow-md: 0 4px 16px rgba(16, 24, 40, 0.10);
  --shadow-lg: 0 1px 2px rgba(16, 24, 40, 0.04), 0 8px 24px -8px rgba(16, 24, 40, 0.10);
  color-scheme: light;
  --color-surface: #fffaf5;
  --color-surface-sunken: #fff1e6;
  --color-surface-raised: #ffffff;
  --color-border: #f6dcc8;
  --color-border-strong: #eec3a3;
  --color-text: #2b1a10;
  --color-muted: #7a5c4a;
  --color-accent: #c2410c;
  --color-accent-soft: #ffe8d6;
  --color-on-accent: #ffffff;
  /* Browner than Default's #b54708, so a warning cannot be mistaken for the orange accent. */
  --color-warning: #a14b00;
  --theme-preview-ground: linear-gradient(135deg, #ffe8d6, #fffaf5);
}

[data-theme=ember] {
  /* Text and icons drawn on an accent fill (primary button, badges, send spinner). Per theme:
     white fails on gold, phosphor green or a pastel accent. */
  --color-on-accent: #ffffff;
  /* Accent used AS text — links, the selected nav item, counts. A separate token from the fill
     because a bright fill can be unreadable as text on the page (gold on white is 1.6:1). */
  --color-accent-text: var(--color-accent);
  --color-focus-ring: var(--color-accent);
  /* An accent-text link while the pointer is on it. Same as at rest unless a theme has a second
     highlight for it (Neon Flamingo's cyan). */
  --color-link-hover: var(--color-accent-text);
  --color-sidebar: var(--color-surface-sunken);
  --color-sidebar-text: var(--color-text);
  --color-sidebar-muted: var(--color-muted);
  /* The modal and command-palette backdrop. */
  --color-scrim: rgba(16, 24, 40, 0.45);
  /* The thin ring that keeps a user-chosen color (a label, a state dot) from vanishing into the
     surface, and the tick drawn on one. Neither color is a theme color, so neither retints. */
  --color-swatch-ring: rgb(0 0 0 / 12%);
  --color-swatch-check: #ffffff;
  --color-swatch-check-shadow: rgb(0 0 0 / 70%);
  /* Background-image hook for the login page and empty states ONLY — never behind message text or
     the composer. Off under prefers-reduced-motion (see the shared rule below). */
  --theme-decoration: none;
  /* How tall the decoration is drawn under an empty state's copy (it never sits behind the
     words). Zero while there is no decoration, so the empty state keeps its usual height. */
  --theme-decoration-space: 0px;
  /* The picker card's ground. A theme can give its card a swatch of its own here (Creamsicle's
     sherbet diagonal); everything else on the card still reads the theme's real tokens. */
  --theme-preview-ground: var(--color-surface-sunken);
  /* Extra ring on the open conversation's row in the list. None unless a theme asks for one. */
  --conversation-row-current-glow: none;
  /* A halo around the primary button. None unless a theme asks for one; never animated. */
  --btn-primary-glow: none;
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-sans: var(--font-ui);
  /* Headings and display figures. */
  --font-display: var(--font-ui);
  /* The weight app headings are set in. `initial` leaves it unset, so each heading falls back to
     the weight it always had (var(--font-display-weight, 600)) — and a theme preview card does not
     inherit a different theme's weight from <html>. */
  --font-display-weight: initial;
  /* Letter-spacing for the small uppercase captions: sidebar sections, rail headings, group labels. */
  --tracking-caps: 0.04em;
  /* "See all"-style action links: a theme can set them as uppercase captions. */
  --action-link-transform: none;
  --action-link-tracking: normal;
  /* One place in the app needs it: a reply template's {{token}}, which has to be readable character
     for character because it is copied by hand. */
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 14px;
  --radius-xl: 18px;
  --radius-pill: 999px;
  /* The internal-comment surface: the composer in comment mode and the card it produces. Mixed
     from the warning pair rather than picked as three more hexes, so the amber a comment is drawn
     in cannot drift away from the amber every other warning in the app uses. It is its own named
     surface because a comment is neither half of the correspondence — --warning-soft alone has no
     edge or band to build a bordered, banded box out of. */
  --color-comment-soft: var(--color-warning-soft);
  --color-comment-band: color-mix(in srgb, var(--color-warning) 8%, var(--color-warning-soft));
  --color-comment-band-strong: color-mix(in srgb, var(--color-warning) 16%, var(--color-warning-soft));
  --color-comment-border: color-mix(in srgb, var(--color-warning) 35%, var(--color-warning-soft));
  /* Softer than --color-border, for rules *inside* a bordered card. A full-strength border
     repeated on every table row and stat divider is what makes a dense panel look boxy. */
  --color-hairline: color-mix(in srgb, var(--color-border) 55%, transparent);
  /* Chart fills fade toward the baseline so a tall bar does not read as a solid slab.
     Built from the accent/success pair rather than new hexes, so a chart cannot drift
     away from the blue and green the rest of the app uses for in/out.
     The fade stops at 70%: a bar's height IS the datum, and fading its base to near-white
     made short bars read shorter than they are. */
  --gradient-accent: linear-gradient(180deg, var(--color-accent), color-mix(in srgb, var(--color-accent) 70%, transparent));
  --gradient-success: linear-gradient(180deg, var(--color-success), color-mix(in srgb, var(--color-success) 70%, transparent));
  /* The edges that used to be light-only hexes. Derived so a named theme only has to retint the
     primitives. */
  --color-accent-hover: color-mix(in srgb, var(--color-accent) 86%, #000000);
  --color-danger-edge: color-mix(in srgb, var(--color-danger) 28%, var(--color-danger-soft));
  --color-success-edge: color-mix(in srgb, var(--color-success) 28%, var(--color-success-soft));
  --color-accent-edge: color-mix(in srgb, var(--color-accent) 28%, var(--color-accent-soft));
  --color-surface: #1c2128;
  --color-surface-sunken: #111418;
  --color-surface-raised: #252b33;
  --color-border: #2e343c;
  --color-border-strong: #3d4450;
  --color-text: #f2f4f6;
  --color-muted: #9aa3af;
  --color-accent: #4c8dff;
  --color-accent-soft: color-mix(in srgb, var(--color-accent) 18%, var(--color-surface));
  /* White on #4c8dff is 3.2:1; the sunken ink is 5.8:1. */
  --color-on-accent: #111418;
  --color-danger: #f97066;
  --color-danger-soft: color-mix(in srgb, var(--color-danger) 16%, var(--color-surface));
  --color-success: #3bbf7a;
  --color-success-soft: color-mix(in srgb, var(--color-success) 16%, var(--color-surface));
  --color-warning: #f79009;
  --color-warning-soft: color-mix(in srgb, var(--color-warning) 16%, var(--color-surface));
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
  --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.45);
  --shadow-lg: 0 1px 2px rgba(0, 0, 0, 0.35), 0 8px 24px -8px rgba(0, 0, 0, 0.55);
  color-scheme: dark;
  --color-surface: #1a1210;
  --color-surface-sunken: #120b09;
  --color-surface-raised: #241915;
  --color-border: #3a2a23;
  --color-border-strong: #4d382e;
  --color-text: #fbefe7;
  --color-muted: #c4a898;
  --color-accent: #ff7a1a;
  /* An ember brightens when it is stirred; darkening it would also thin the ink on it. */
  --color-accent-hover: color-mix(in srgb, var(--color-accent) 86%, #ffffff);
  --color-on-accent: #1a1210;
  --color-warning: #fdb022;
  /* The open conversation's row glows faintly from inside, so the list's overflow cannot clip it
     and it cannot spill onto the next row. Static, so there is no motion to turn off. */
  --conversation-row-current-glow: inset 0 0 12px color-mix(in srgb, var(--color-accent) 25%, transparent);
}

[data-theme=bubblegum] {
  /* Text and icons drawn on an accent fill (primary button, badges, send spinner). Per theme:
     white fails on gold, phosphor green or a pastel accent. */
  --color-on-accent: #ffffff;
  /* Accent used AS text — links, the selected nav item, counts. A separate token from the fill
     because a bright fill can be unreadable as text on the page (gold on white is 1.6:1). */
  --color-accent-text: var(--color-accent);
  --color-focus-ring: var(--color-accent);
  /* An accent-text link while the pointer is on it. Same as at rest unless a theme has a second
     highlight for it (Neon Flamingo's cyan). */
  --color-link-hover: var(--color-accent-text);
  --color-sidebar: var(--color-surface-sunken);
  --color-sidebar-text: var(--color-text);
  --color-sidebar-muted: var(--color-muted);
  /* The modal and command-palette backdrop. */
  --color-scrim: rgba(16, 24, 40, 0.45);
  /* The thin ring that keeps a user-chosen color (a label, a state dot) from vanishing into the
     surface, and the tick drawn on one. Neither color is a theme color, so neither retints. */
  --color-swatch-ring: rgb(0 0 0 / 12%);
  --color-swatch-check: #ffffff;
  --color-swatch-check-shadow: rgb(0 0 0 / 70%);
  /* Background-image hook for the login page and empty states ONLY — never behind message text or
     the composer. Off under prefers-reduced-motion (see the shared rule below). */
  --theme-decoration: none;
  /* How tall the decoration is drawn under an empty state's copy (it never sits behind the
     words). Zero while there is no decoration, so the empty state keeps its usual height. */
  --theme-decoration-space: 0px;
  /* The picker card's ground. A theme can give its card a swatch of its own here (Creamsicle's
     sherbet diagonal); everything else on the card still reads the theme's real tokens. */
  --theme-preview-ground: var(--color-surface-sunken);
  /* Extra ring on the open conversation's row in the list. None unless a theme asks for one. */
  --conversation-row-current-glow: none;
  /* A halo around the primary button. None unless a theme asks for one; never animated. */
  --btn-primary-glow: none;
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-sans: var(--font-ui);
  /* Headings and display figures. */
  --font-display: var(--font-ui);
  /* The weight app headings are set in. `initial` leaves it unset, so each heading falls back to
     the weight it always had (var(--font-display-weight, 600)) — and a theme preview card does not
     inherit a different theme's weight from <html>. */
  --font-display-weight: initial;
  /* Letter-spacing for the small uppercase captions: sidebar sections, rail headings, group labels. */
  --tracking-caps: 0.04em;
  /* "See all"-style action links: a theme can set them as uppercase captions. */
  --action-link-transform: none;
  --action-link-tracking: normal;
  /* One place in the app needs it: a reply template's {{token}}, which has to be readable character
     for character because it is copied by hand. */
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 14px;
  --radius-xl: 18px;
  --radius-pill: 999px;
  /* The internal-comment surface: the composer in comment mode and the card it produces. Mixed
     from the warning pair rather than picked as three more hexes, so the amber a comment is drawn
     in cannot drift away from the amber every other warning in the app uses. It is its own named
     surface because a comment is neither half of the correspondence — --warning-soft alone has no
     edge or band to build a bordered, banded box out of. */
  --color-comment-soft: var(--color-warning-soft);
  --color-comment-band: color-mix(in srgb, var(--color-warning) 8%, var(--color-warning-soft));
  --color-comment-band-strong: color-mix(in srgb, var(--color-warning) 16%, var(--color-warning-soft));
  --color-comment-border: color-mix(in srgb, var(--color-warning) 35%, var(--color-warning-soft));
  /* Softer than --color-border, for rules *inside* a bordered card. A full-strength border
     repeated on every table row and stat divider is what makes a dense panel look boxy. */
  --color-hairline: color-mix(in srgb, var(--color-border) 55%, transparent);
  /* Chart fills fade toward the baseline so a tall bar does not read as a solid slab.
     Built from the accent/success pair rather than new hexes, so a chart cannot drift
     away from the blue and green the rest of the app uses for in/out.
     The fade stops at 70%: a bar's height IS the datum, and fading its base to near-white
     made short bars read shorter than they are. */
  --gradient-accent: linear-gradient(180deg, var(--color-accent), color-mix(in srgb, var(--color-accent) 70%, transparent));
  --gradient-success: linear-gradient(180deg, var(--color-success), color-mix(in srgb, var(--color-success) 70%, transparent));
  /* The edges that used to be light-only hexes. Derived so a named theme only has to retint the
     primitives. */
  --color-accent-hover: color-mix(in srgb, var(--color-accent) 86%, #000000);
  --color-danger-edge: color-mix(in srgb, var(--color-danger) 28%, var(--color-danger-soft));
  --color-success-edge: color-mix(in srgb, var(--color-success) 28%, var(--color-success-soft));
  --color-accent-edge: color-mix(in srgb, var(--color-accent) 28%, var(--color-accent-soft));
  --color-surface: #ffffff;
  --color-surface-sunken: #f6f7f9;
  --color-surface-raised: #ffffff;
  --color-border: #e3e6ea;
  --color-border-strong: #cdd2d9;
  --color-text: #1b1f24;
  --color-muted: #6b7280;
  --color-accent: #2f6feb;
  --color-accent-soft: #eaf1fe;
  --color-danger: #b42318;
  --color-danger-soft: #fef3f2;
  --color-success: #067647;
  --color-success-soft: #ecfdf3;
  --color-warning: #b54708;
  --color-warning-soft: #fffaeb;
  --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06);
  --shadow-md: 0 4px 16px rgba(16, 24, 40, 0.10);
  --shadow-lg: 0 1px 2px rgba(16, 24, 40, 0.04), 0 8px 24px -8px rgba(16, 24, 40, 0.10);
  color-scheme: light;
  --color-surface: #fff7fb;
  --color-surface-sunken: #ffe9f3;
  --color-surface-raised: #ffffff;
  --color-border: #f9cfe2;
  --color-border-strong: #f2aecd;
  --color-text: #3a1029;
  --color-muted: #8a5070;
  --color-accent: #c2255c;
  --color-accent-soft: #ffe0ee;
  --color-on-accent: #ffffff;
  /* Softer, chewier corners. */
  --radius-md: 12px;
  --radius-lg: 16px;
  /* A tiny bubble, with a glint, floating in the card's bottom corner. */
  --theme-preview-ground: radial-gradient(circle at 86% 78%, #ffffff 0 1.5px, #ff8cc6 2px 6px, transparent 6.5px), #ffe9f3;
}

[data-theme=neon-flamingo] {
  /* Text and icons drawn on an accent fill (primary button, badges, send spinner). Per theme:
     white fails on gold, phosphor green or a pastel accent. */
  --color-on-accent: #ffffff;
  /* Accent used AS text — links, the selected nav item, counts. A separate token from the fill
     because a bright fill can be unreadable as text on the page (gold on white is 1.6:1). */
  --color-accent-text: var(--color-accent);
  --color-focus-ring: var(--color-accent);
  /* An accent-text link while the pointer is on it. Same as at rest unless a theme has a second
     highlight for it (Neon Flamingo's cyan). */
  --color-link-hover: var(--color-accent-text);
  --color-sidebar: var(--color-surface-sunken);
  --color-sidebar-text: var(--color-text);
  --color-sidebar-muted: var(--color-muted);
  /* The modal and command-palette backdrop. */
  --color-scrim: rgba(16, 24, 40, 0.45);
  /* The thin ring that keeps a user-chosen color (a label, a state dot) from vanishing into the
     surface, and the tick drawn on one. Neither color is a theme color, so neither retints. */
  --color-swatch-ring: rgb(0 0 0 / 12%);
  --color-swatch-check: #ffffff;
  --color-swatch-check-shadow: rgb(0 0 0 / 70%);
  /* Background-image hook for the login page and empty states ONLY — never behind message text or
     the composer. Off under prefers-reduced-motion (see the shared rule below). */
  --theme-decoration: none;
  /* How tall the decoration is drawn under an empty state's copy (it never sits behind the
     words). Zero while there is no decoration, so the empty state keeps its usual height. */
  --theme-decoration-space: 0px;
  /* The picker card's ground. A theme can give its card a swatch of its own here (Creamsicle's
     sherbet diagonal); everything else on the card still reads the theme's real tokens. */
  --theme-preview-ground: var(--color-surface-sunken);
  /* Extra ring on the open conversation's row in the list. None unless a theme asks for one. */
  --conversation-row-current-glow: none;
  /* A halo around the primary button. None unless a theme asks for one; never animated. */
  --btn-primary-glow: none;
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-sans: var(--font-ui);
  /* Headings and display figures. */
  --font-display: var(--font-ui);
  /* The weight app headings are set in. `initial` leaves it unset, so each heading falls back to
     the weight it always had (var(--font-display-weight, 600)) — and a theme preview card does not
     inherit a different theme's weight from <html>. */
  --font-display-weight: initial;
  /* Letter-spacing for the small uppercase captions: sidebar sections, rail headings, group labels. */
  --tracking-caps: 0.04em;
  /* "See all"-style action links: a theme can set them as uppercase captions. */
  --action-link-transform: none;
  --action-link-tracking: normal;
  /* One place in the app needs it: a reply template's {{token}}, which has to be readable character
     for character because it is copied by hand. */
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 14px;
  --radius-xl: 18px;
  --radius-pill: 999px;
  /* The internal-comment surface: the composer in comment mode and the card it produces. Mixed
     from the warning pair rather than picked as three more hexes, so the amber a comment is drawn
     in cannot drift away from the amber every other warning in the app uses. It is its own named
     surface because a comment is neither half of the correspondence — --warning-soft alone has no
     edge or band to build a bordered, banded box out of. */
  --color-comment-soft: var(--color-warning-soft);
  --color-comment-band: color-mix(in srgb, var(--color-warning) 8%, var(--color-warning-soft));
  --color-comment-band-strong: color-mix(in srgb, var(--color-warning) 16%, var(--color-warning-soft));
  --color-comment-border: color-mix(in srgb, var(--color-warning) 35%, var(--color-warning-soft));
  /* Softer than --color-border, for rules *inside* a bordered card. A full-strength border
     repeated on every table row and stat divider is what makes a dense panel look boxy. */
  --color-hairline: color-mix(in srgb, var(--color-border) 55%, transparent);
  /* Chart fills fade toward the baseline so a tall bar does not read as a solid slab.
     Built from the accent/success pair rather than new hexes, so a chart cannot drift
     away from the blue and green the rest of the app uses for in/out.
     The fade stops at 70%: a bar's height IS the datum, and fading its base to near-white
     made short bars read shorter than they are. */
  --gradient-accent: linear-gradient(180deg, var(--color-accent), color-mix(in srgb, var(--color-accent) 70%, transparent));
  --gradient-success: linear-gradient(180deg, var(--color-success), color-mix(in srgb, var(--color-success) 70%, transparent));
  /* The edges that used to be light-only hexes. Derived so a named theme only has to retint the
     primitives. */
  --color-accent-hover: color-mix(in srgb, var(--color-accent) 86%, #000000);
  --color-danger-edge: color-mix(in srgb, var(--color-danger) 28%, var(--color-danger-soft));
  --color-success-edge: color-mix(in srgb, var(--color-success) 28%, var(--color-success-soft));
  --color-accent-edge: color-mix(in srgb, var(--color-accent) 28%, var(--color-accent-soft));
  --color-surface: #1c2128;
  --color-surface-sunken: #111418;
  --color-surface-raised: #252b33;
  --color-border: #2e343c;
  --color-border-strong: #3d4450;
  --color-text: #f2f4f6;
  --color-muted: #9aa3af;
  --color-accent: #4c8dff;
  --color-accent-soft: color-mix(in srgb, var(--color-accent) 18%, var(--color-surface));
  /* White on #4c8dff is 3.2:1; the sunken ink is 5.8:1. */
  --color-on-accent: #111418;
  --color-danger: #f97066;
  --color-danger-soft: color-mix(in srgb, var(--color-danger) 16%, var(--color-surface));
  --color-success: #3bbf7a;
  --color-success-soft: color-mix(in srgb, var(--color-success) 16%, var(--color-surface));
  --color-warning: #f79009;
  --color-warning-soft: color-mix(in srgb, var(--color-warning) 16%, var(--color-surface));
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
  --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.45);
  --shadow-lg: 0 1px 2px rgba(0, 0, 0, 0.35), 0 8px 24px -8px rgba(0, 0, 0, 0.55);
  color-scheme: dark;
  --color-surface: #1b0f1a;
  --color-surface-sunken: #120812;
  --color-surface-raised: #261526;
  --color-border: #3d2240;
  --color-border-strong: #55305a;
  --color-text: #fdeefa;
  --color-muted: #cfa3c8;
  --color-accent: #ff4fa3;
  /* Neon brightens under the pointer; darkening it would also thin the ink on it. */
  --color-accent-hover: color-mix(in srgb, var(--color-accent) 86%, #ffffff);
  --color-on-accent: #1b0f1a;
  --color-focus-ring: #2de2e6;
  --color-link-hover: #2de2e6;
  /* A static neon halo on the primary button. Never flickers (out of scope for accessibility). */
  --btn-primary-glow: 0 0 12px color-mix(in srgb, var(--color-accent) 40%, transparent);
}

/* The one reduced-motion switch for every theme. A theme's animation or decoration needs no
   reduced-motion rule of its own: this turns all of it off. Durations go to ~0 rather than
   `animation: none` so an animation that ends in a visible state (thread-loading-raise) still
   lands there, and animationend/transitionend still fire for anything waiting on them. */
@media (prefers-reduced-motion: reduce) {
  :root, [data-theme] {
    --theme-decoration: none !important;
    --theme-decoration-space: 0px !important;
  }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
* {
  box-sizing: border-box;
}

html, body {
  height: 100%;
}

body {
  margin: 0;
  font-family: var(--font-ui);
  font-size: var(--text-base);
  color: var(--color-text);
  background: var(--color-surface-sunken);
  -webkit-font-smoothing: antialiased;
}

/* A theme's display face is for the app's own headings. A customer's email and the reply being
   written are correspondence, not chrome, so their headings stay in the body face. */
:where(h1, h2, h3, h4, h5, h6) {
  font-family: var(--font-display);
  font-weight: var(--font-display-weight, bold);
}

:where(.message-card__body, .composer__editor) :where(h1, h2, h3, h4, h5, h6) {
  font-family: inherit;
  font-weight: bold;
}

a {
  color: inherit;
  text-decoration: none;
}

:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 2px;
}

/* ---------- Toasts ---------- */
/* Bottom-left, and stacked: a state action streams one in on every transition, so two can
   easily be on screen at once and the second must not land on top of the first.
   Not top-right, where it used to sit: that is where the thread header's action row is, and
   the toast now carries an Undo button, so it can no longer let clicks pass through itself
   to get out of that row's way. */
.toasts {
  position: fixed;
  bottom: var(--space-4);
  left: var(--space-4);
  z-index: 50;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2);
  /* The container is as tall as the corner it occupies whether or not anything is in it, so it
     lets clicks through; each toast takes them back for its own box. */
  pointer-events: none;
}

/* display: contents so streamed toasts and a redirect's flash stack in one column despite
   arriving in two containers — the inner one is turbo-permanent, because a broadcast morph
   must not wipe a toast whose Undo the reader is reaching for, and a permanent element cannot
   also be the one a redirect re-renders its flash into. */
.toasts__stack {
  display: contents;
}

.toast {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  max-width: 22rem;
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  box-shadow: var(--shadow-md);
  pointer-events: auto;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.2s ease, transform 0.2s ease;
}

.toast--shown {
  opacity: 1;
  transform: none;
}

.toast--dismissing {
  opacity: 0;
  transform: translateY(8px);
}

.toast--alert {
  background: var(--color-danger-soft);
  color: var(--color-danger);
  border: 1px solid var(--color-danger-edge);
}

.toast--notice {
  background: var(--color-success-soft);
  color: var(--color-success);
  border: 1px solid var(--color-success-edge);
}

.toast__message {
  flex: 1;
}

/* Two buttons plus a sentence do not fit the one-button width, and a toast whose message wraps to
   three lines to make room reads as a fault rather than as an offer. */
.toast--offering {
  max-width: 30rem;
}

.toast__undo-form, .toast__offer-form {
  margin: 0;
}

.toast__undo, .toast__offer {
  appearance: none;
  border: 0;
  background: transparent;
  padding: 0;
  font: inherit;
  font-weight: 600;
  color: inherit;
  text-decoration: underline;
  cursor: pointer;
  white-space: nowrap;
}

/* The notice and its Undo are the point; sliding into place is not. toast_controller removes
   the element on a timer either way, so dropping the transition costs nothing but the movement. */
@media (prefers-reduced-motion: reduce) {
  .toast {
    transition: none;
  }
}
/* ---------- Buttons & fields ---------- */
.btn {
  appearance: none;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-4);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  background: var(--color-surface-sunken);
  color: var(--color-text);
}

.btn--primary {
  background: var(--color-accent);
  color: var(--color-on-accent);
  width: 100%;
  box-shadow: var(--btn-primary-glow);
}

.btn--primary:hover {
  background: var(--color-accent-hover);
}

.btn--ghost {
  background: transparent;
  border-color: var(--color-border-strong);
  color: var(--color-muted);
  padding: var(--space-1) var(--space-3);
  font-size: var(--text-sm);
}

.btn--ghost:hover {
  color: var(--color-text);
  background: var(--color-surface-sunken);
}

.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.btn--primary:disabled:hover {
  background: var(--color-accent);
}

.field {
  display: block;
  margin-bottom: var(--space-4);
}

.field__label {
  display: block;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-muted);
  margin-bottom: var(--space-1);
}

.field__input {
  width: 100%;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  font: inherit;
  background: var(--color-surface);
  color: var(--color-text);
}

.field__input:focus {
  border-color: var(--color-accent);
  outline: none;
  box-shadow: 0 0 0 3px var(--color-accent-soft);
}

/* A note under an input — the rule it has to satisfy, not an error. Sits inside the .field label
   so it stays tied to the control it describes. */
.field__hint {
  display: block;
  margin-top: var(--space-1);
  font-size: 12px;
  color: var(--color-muted);
}

/* The app's one native control. Everywhere else a "pick one" is the .dropdown popover, but that
   is position:absolute inside .modal{overflow:hidden} and a scrolling .settings__detail, so a
   list long enough to matter gets clipped. A native select's list is painted by the browser
   outside the document, which no ancestor can clip — and it brings keyboard nav and type-ahead
   with it. The cost is the UA chrome, stripped here so it reads as a .field__input with a caret. */
.field__select {
  appearance: none;
  padding: var(--space-2) var(--space-6) var(--space-2) var(--space-3);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  font: inherit;
  background-color: var(--color-surface);
  color: var(--color-text);
  cursor: pointer;
  /* The caret the appearance:none reset removed, redrawn in the muted token rather than left to
     the platform's own arrow. */
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-2) center;
  background-size: 14px;
}

.field__select:hover {
  border-color: var(--color-accent);
}

.field__select:focus {
  border-color: var(--color-accent);
  outline: none;
  box-shadow: 0 0 0 3px var(--color-accent-soft);
}

/* ---------- Login ---------- */
.login {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-5);
  /* --theme-decoration paints here and on .empty-state only (FRO-155): chrome a reader passes
     through, never behind message text or the composer. Anchored under the card, not tiled. */
  background-image: var(--theme-decoration);
  background-repeat: no-repeat;
  background-position: center bottom var(--space-6);
  background-size: auto 96px;
}

.login__card {
  width: 100%;
  max-width: 360px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
  box-shadow: var(--shadow-sm);
}

.login__title {
  margin: 0;
  font-size: var(--text-xl);
}

.login__subtitle {
  margin: var(--space-1) 0 var(--space-5);
  color: var(--color-muted);
}

.login__hint {
  margin: var(--space-4) 0 0;
  font-size: 12px;
  color: var(--color-muted);
  text-align: center;
}

.login__hint + .login__hint {
  margin-top: var(--space-2);
}

/* The global `a` rule paints links in body colour with no underline, which on a card whose only
   other text is muted prose leaves nothing saying this one is clickable. */
.login__link {
  color: var(--color-accent-text);
  font-weight: 600;
}

.login__link:hover {
  color: var(--color-link-hover);
  text-decoration: underline;
}

/* In the card, unlike the flash toast, this is not a transient notice but the whole explanation of
   why the visitor is still on this screen — a rejected token most of all. Same palette as
   .flash--alert so the two read as one language. */
.login__error {
  margin: 0 0 var(--space-4);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-danger-edge);
  border-radius: var(--radius-md);
  background: var(--color-danger-soft);
  color: var(--color-danger);
  font-size: var(--text-sm);
}

.login .field__input[readonly] {
  background: var(--color-surface-sunken);
  color: var(--color-muted);
}

/* Rails wraps a failed field in <div class="field_with_errors">. The login card stacks its
   fields in normal flow, so the extra block sizes to the same width the input already had —
   unlike .settings / .add-user, which have to take the wrapper out of the flex row. */
/* ---------- App shell (three regions) ---------- */
/* The rail's width is a custom property so collapsing overrides one value rather than restating
   the whole track list — which is also why the narrow-viewport rule below, setting
   grid-template-columns outright, wins over the collapsed state without needing to know it. */
/* The list is 390px because the tab strip now carries counts as well as words, and 340px could not
   hold both: four labels leave 39px inside the header's padding, and three counts need 79px at the
   cap. Measured rather than guessed, and asserted — see the fitting group in
   spec/system/tab_counts_spec.rb. */
/* FRO-138: every inbox panel width is a custom property the server renders inline from the user's
   saved layout and panel-resize writes while dragging. The *-track properties are what the grid
   actually uses: each clamps its *-width into the panel's range (InboxLayout::PANELS — keep the
   numbers in step), so a bad value cannot break the layout, and the collapse classes override the
   track rather than the width, so reopening restores the width the person had. The inline style
   would outrank any class rule that set --sidebar-width itself.
   The rail gives way to the viewport first, then the list, so the thread keeps 420px wherever the
   minimums allow; clamp() puts a panel's minimum above that when they do not. */
.app {
  --sidebar-width: 240px;
  --list-width: 390px;
  --rail-width: 300px;
  --sidebar-track: clamp(200px, var(--sidebar-width), 360px);
  --rail-track: clamp(260px, var(--rail-width), min(480px, 100vw - var(--sidebar-track) - 390px - 420px));
  --list-track: clamp(390px, var(--list-width), min(640px, 100vw - var(--sidebar-track) - var(--rail-track) - 420px));
  display: grid;
  grid-template-columns: var(--sidebar-track) var(--list-track) minmax(0, 1fr);
  height: 100vh;
  height: 100dvh;
  background: var(--color-surface);
}

/* Inbox only. Report and settings are their own shells and keep their own overflow.
   Grid items default to min-height: auto — the content size — which is what lets a tall
   list or thread grow this row past the viewport and start the document scrolling.
   overflow: hidden on the grid, and min-height: 0 on its children, is what keeps `.app`
   at the viewport and forces each pane to clip into its own scrollport. */
.app:not(.app--report):not(.app--settings) {
  height: 100%;
  max-height: 100%;
  overflow: hidden;
  /* Implicit rows min at content size, which is the leak: a tall pane grows the
     one row past 100vh and the document becomes the scrollport. */
  grid-template-rows: minmax(0, 1fr);
}

.app:not(.app--report):not(.app--settings) > * {
  min-height: 0;
  max-height: 100%;
}

/* Belt for the same leak: if any child still paints past `.app`, the window itself
   must not become the scrollport. Narrow viewports drop this — they stack the panes
   and scroll the document on purpose. */
@media (min-width: 901px) {
  html:has(.app:not(.app--report):not(.app--settings)),
  html:has(.app:not(.app--report):not(.app--settings)) body {
    overflow: clip;
    max-height: 100%;
  }
}
/* Analytics is a report, not a queue, so it drops the 390px list track. The rail's width stays
   a custom property, which is what lets collapsing and the narrow-viewport rule keep working
   without knowing this variant exists. */
.app--report {
  grid-template-columns: var(--sidebar-track) 1fr;
}

/* Settings drops it for the same reason: a rail of sections and the one section you opened. */
.app--settings {
  grid-template-columns: var(--sidebar-track) 1fr;
}

@media (prefers-reduced-motion: no-preference) {
  .app {
    transition: grid-template-columns 0.15s ease;
  }
}
/* ---------- Panel resize handles (FRO-138) ----------
   Absolutely positioned so they take no grid track: each sits on the seam it resizes, found from the
   same track properties the grid uses. A 7px hit area around a 1px line that lights up on hover,
   focus and drag. Only above the stacking breakpoint — below it the panes are full-width blocks and
   there is nothing to resize (see the 900px block). */
.app--inbox {
  position: relative;
}

#conversation {
  position: relative;
}

.panel-resize {
  position: absolute;
  top: 0;
  bottom: 0;
  z-index: 3;
  width: 7px;
  margin-left: -3px;
  cursor: col-resize;
  touch-action: none;
  background: transparent;
}

.panel-resize::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 3px;
  width: 1px;
  background: transparent;
  transition: background-color 0.1s ease;
}

.panel-resize:hover::after,
.panel-resize:focus-visible::after,
.app.is-resizing .panel-resize:focus::after {
  width: 2px;
  left: 2.5px;
  background: var(--color-accent);
}

.panel-resize:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: -2px;
}

.panel-resize--sidebar {
  left: var(--sidebar-track);
}

.panel-resize--list {
  left: calc(var(--sidebar-track) + var(--list-track));
}

/* Inside the conversation frame, on the rail's left edge. */
.panel-resize--rail {
  left: auto;
  margin-left: 0;
  right: calc(var(--rail-track) - 4px);
}

/* While dragging: the track follows the pointer with no easing, text does not select, and the
   cursor stays col-resize even when the pointer outruns the 7px handle. */
.app.is-resizing {
  transition: none;
  user-select: none;
  cursor: col-resize;
}

.app.is-resizing * {
  cursor: col-resize !important;
}

/* ---------- Icons ---------- */
/* Phosphor glyphs are text, so the size has to drive both the box and the font — one custom
   property keeps them in step, and every override below only has to restate `--icon-size`.
   The explicit box is what stops a slow font load from shifting the layout: the space is
   reserved whether or not the glyph has arrived.
   Keep the display non-inline — `transform` (used by .sidebar__chevron) does not apply to
   inline elements. */
.icon {
  --icon-size: 18px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--icon-size);
  height: var(--icon-size);
  font-size: var(--icon-size);
  line-height: 1;
  flex: 0 0 auto;
  vertical-align: middle;
}

/* ---------- Sidebar ---------- */
.sidebar {
  display: flex;
  flex-direction: column;
  height: 100%;
  border-right: 1px solid var(--color-border);
  background: var(--color-sidebar);
  /* A theme may give the rail its own ground (a dark rail on a light page). Rebinding the two ink
     tokens here, rather than repainting every sidebar rule, keeps each row drawing from
     --color-text / --color-muted and still reads on that ground. Not a cycle: the sidebar tokens
     are inherited from the theme, not declared on this element. */
  --color-text: var(--color-sidebar-text);
  --color-muted: var(--color-sidebar-muted);
  color: var(--color-text);
  overflow: hidden; /* the nav region scrolls, not the whole rail */
}

/* Header: avatar + name + collapse */
.sidebar__header {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-4);
  border-bottom: 1px solid var(--color-border);
}

.sidebar__identity {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
  flex: 1;
  border-radius: var(--radius-md);
}

.sidebar__identity:hover {
  background: var(--color-surface);
}

.user-avatar {
  flex: none;
  display: block;
  width: var(--user-avatar-size, 32px);
  height: var(--user-avatar-size, 32px);
  border-radius: 50%;
  overflow: hidden;
  background: var(--color-surface-sunken);
}

.user-avatar svg {
  display: block;
  width: 100%;
  height: 100%;
}

.sidebar__name {
  flex: 1;
  font-weight: 700;
  font-size: var(--text-lg);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A <button>, so the native chrome has to be reset — background, border and font all come
   from the UA otherwise. */
.sidebar__collapse-toggle {
  flex: 0 0 auto;
  display: inline-flex;
  padding: var(--space-1);
  border-radius: var(--radius-md);
  color: var(--color-muted);
  appearance: none;
  border: 0;
  background: transparent;
  font: inherit;
  cursor: pointer;
}

.sidebar__collapse-toggle:hover {
  color: var(--color-text);
  background: var(--color-surface);
}

/* One glyph for both directions, turned around when the rail is shut — the same rotate-on-state
   the project chevron uses. */
.sidebar__collapse-icon {
  transition: transform 0.15s ease;
}

/* Scrolling nav region */
.sidebar__nav {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--space-3) var(--space-3) var(--space-4);
}

/* Nav rows: Inbox, Spam, and project children */
.sidebar__nav-item {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.sidebar__nav-item:hover {
  background: var(--color-accent-soft);
}

.sidebar__nav-item[aria-current=true] {
  background: var(--color-accent-soft);
  color: var(--color-accent-text);
  font-weight: 600;
}

/* Compose sits above Inbox as a primary action, not another destination row.
   .btn--primary is width:100% for the + Project form; that is the right answer here too. */
.sidebar__compose {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
  text-decoration: none;
}

.sidebar__compose .sidebar__nav-icon {
  color: inherit;
}

.sidebar__compose[aria-current=true] {
  box-shadow: 0 0 0 2px var(--color-accent-edge);
}

.sidebar__nav-icon {
  color: var(--color-muted);
}

.sidebar__nav-item[aria-current=true] .sidebar__nav-icon {
  color: var(--color-accent-text);
}

.sidebar__badge {
  margin-left: auto;
  min-width: 1.25em;
  padding: 0 6px;
  border-radius: 999px;
  /* Informational by default. The rail is already --color-surface-sunken, so a sunken
     fill would vanish; surface + muted is a quiet chip on both the rail and a selected
     accent-soft row, and does not inherit the row's accent text. */
  background: var(--color-surface);
  color: var(--color-muted);
  font-size: 11px;
  font-weight: 700;
  line-height: 1.45;
  text-align: center;
}

.sidebar__badge--actionable {
  background: var(--color-accent);
  color: var(--color-on-accent);
}

.sidebar__section-label {
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--color-muted);
  margin: var(--space-4) var(--space-3) var(--space-2);
}

.sidebar__projects {
  list-style: none;
  margin: 0;
  padding: 0;
}

.sidebar__project {
  margin: 0;
}

.sidebar__inbox {
  margin: 0;
}

/* Project / Inbox disclosure toggle — a <button> reset to read like a nav row */
.sidebar__project-toggle,
.sidebar__inbox-toggle {
  appearance: none;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--color-text);
  font: inherit;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}

.sidebar__project-toggle:hover,
.sidebar__inbox-toggle:hover {
  background: var(--color-accent-soft);
}

/* Wraps the initials and the name so a rename can stream both back as one node (and carry the
   tooltip with them). flex:1 keeps it filling the row the way those two spans did on their own —
   which is also what keeps the title's hover target the whole label, not just the text. */
.sidebar__project-label,
.sidebar__inbox-label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 1;
  min-width: 0;
}

.sidebar__project-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Stands in for the project's name in the collapsed rail, where there is no room for text.
   Always in the DOM — the row is also injected by create.turbo_stream.erb, which knows nothing
   about the rail's state — so it is the CSS that decides which of the two shows.
   Borrows the avatar's treatment so the two read as one family. */
.sidebar__project-initials {
  display: none;
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  place-items: center;
  background: var(--color-accent-soft);
  color: var(--color-accent-text);
  font-size: 11px;
  font-weight: 700;
}

/* Rotates on open — which needs .icon to keep a non-inline display. */
.sidebar__chevron {
  color: var(--color-muted);
  transition: transform 0.15s ease;
}

.sidebar__project.is-open .sidebar__chevron,
.sidebar__inbox.is-open .sidebar__chevron {
  transform: rotate(180deg);
}

.sidebar__project-children,
.sidebar__inbox-children {
  list-style: none;
  margin: var(--space-1) 0 var(--space-2);
  padding-left: var(--space-4);
}

.sidebar__view {
  margin: 0;
  position: relative;
}

.sidebar__view:has(.sidebar__view-menu-toggle[aria-expanded=true]) {
  z-index: 3;
}

.sidebar__view-row {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  position: relative;
}

.sidebar__view-row .sidebar__nav-item {
  flex: 1;
  min-width: 0;
}

.sidebar__view-menu-toggle {
  appearance: none;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-muted);
  cursor: pointer;
  opacity: 0;
}

.sidebar__view-row:hover .sidebar__view-menu-toggle,
.sidebar__view-row:focus-within .sidebar__view-menu-toggle,
.sidebar__view-menu-toggle[aria-expanded=true] {
  opacity: 1;
}

.sidebar__view-menu-toggle:hover {
  color: var(--color-text);
  background: var(--color-accent-soft);
}

.sidebar__view-menu {
  right: 0;
  left: auto;
  min-width: 140px;
  z-index: 21;
}

.sidebar__view-delete {
  margin: 0;
  display: contents;
}

.sidebar__view-delete .dropdown__option {
  width: 100%;
}

.sidebar__view-rename {
  padding: 0 var(--space-3) var(--space-2);
}

.sidebar__view-rename-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.inbox-view-form__menu {
  min-width: 220px;
  padding: var(--space-2);
}

.inbox-view-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* An <li> in the projects list now, so it takes the same vertical rhythm as the rows it sits
   between instead of a paragraph's default margins. */
.sidebar__empty {
  color: var(--color-muted);
  font-size: var(--text-sm);
  padding: var(--space-2) var(--space-3);
}

/* "+ Project" — the last row of the projects list above, which is why it lives here rather than
   with the footer it used to sit in. */
.sidebar__new-project {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: 0;
}

/* Reads as a list row, not a button chip: it sits among the project toggles now, so it borrows
   their geometry wholesale. It carries no .btn, which means the native <button> chrome has to be
   reset here — background, border and font all come from the UA otherwise. Accent-coloured
   because it is the one row in the list that acts rather than navigates; the "Project" label
   carries that meaning too, so the colour is never the only signal. */
.sidebar__new-project-toggle {
  appearance: none;
  width: 100%;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--color-accent-text);
  font: inherit;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}

.sidebar__new-project-toggle:hover {
  background: var(--color-accent-soft);
}

.sidebar__new-project-toggle .sidebar__nav-icon {
  color: var(--color-accent-text);
}

/* Only the form is a flex column; the fields wrapper stays display-neutral so the
   `hidden` attribute (display:none) still collapses it when the disclosure is closed. */
.sidebar__new-project-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.sidebar__new-project-fields[hidden] {
  display: none;
}

.sidebar__form-error {
  margin: 0;
  font-size: 12px;
  color: var(--color-danger);
}

/* Footer: account only */
.sidebar__footer {
  border-top: 1px solid var(--color-border);
  padding: var(--space-3);
}

.sidebar__account {
  display: flex;
  align-items: center;
}

/* .btn is a bare <button>, so it stays inline-block and `gap` alone would be inert — the glyph
   would sit flush against its label. The display change is what makes the gap real, which is why
   .sidebar__new-project-toggle sets one too for the same icon+label pairing. */
.sidebar__account .btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

/* ---------- Sidebar, collapsed to an icon rail ----------
   Everything here is keyed off ONE class on .app, which the server renders from the cookie and
   the sidebar-collapse controller toggles. The markup is identical in both states; only these
   rules differ. That is what lets the toggle flip a class instead of re-rendering, and what lets
   a turbo-stream prepend a project row without knowing which state the rail is in.

   Scoped above the stacking breakpoint on purpose: below it the rail is a full-width block on top
   of the page, and centred lone icons across 100% of the viewport would be nonsense. */
@media (min-width: 901px) {
  .app.is-sidebar-collapsed {
    --sidebar-track: 56px;
  }
  /* The way back out is the first thing in the rail, so a collapsed sidebar can never trap. */
  .app.is-sidebar-collapsed .sidebar__header {
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-2);
  }
  .app.is-sidebar-collapsed .sidebar__identity {
    flex: none;
    justify-content: center;
  }
  .app.is-sidebar-collapsed .sidebar__collapse-toggle {
    order: -1;
  }
  .app.is-sidebar-collapsed .sidebar__collapse-icon {
    transform: rotate(180deg);
  }
  .app.is-sidebar-collapsed .sidebar__nav {
    padding: var(--space-2);
  }
  .app.is-sidebar-collapsed .sidebar__nav-item,
  .app.is-sidebar-collapsed .sidebar__project-toggle,
  .app.is-sidebar-collapsed .sidebar__inbox-toggle,
  .app.is-sidebar-collapsed .sidebar__new-project-toggle,
  .app.is-sidebar-collapsed .sidebar__compose {
    justify-content: center;
    padding: var(--space-2);
  }
  /* These spans ARE the accessible name of the control they sit in, so they are hidden from the
     eye but left in the accessibility tree — display:none would leave a screen reader announcing
     an unlabelled icon. */
  .app.is-sidebar-collapsed .sidebar__nav-label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  /* Text nothing takes its name from, and controls that cannot work at this width. The project
     name goes with them because its button falls back to the title attribute. */
  .app.is-sidebar-collapsed :is(.sidebar__name,
  .sidebar__section-label,
  .sidebar__project-name,
  .sidebar__chevron,
  .sidebar__empty,
  .sidebar__new-project-fields) {
    display: none;
  }
  .app.is-sidebar-collapsed .sidebar__project-initials {
    display: grid;
  }
  /* Unread count becomes a dot on the icon, because the rail has no room for a number.
     Surface-on-sunken is too faint at 8px; the muted token itself keeps the default
     visible and still distinct from the accent fill. */
  .app.is-sidebar-collapsed .sidebar__badge {
    position: absolute;
    top: 4px;
    right: 4px;
    min-width: 8px;
    height: 8px;
    padding: 0;
    font-size: 0;
    line-height: 0;
    background: var(--color-muted);
  }
  .app.is-sidebar-collapsed .sidebar__badge--actionable {
    background: var(--color-accent);
  }
  /* The label is down to just the badge here, so it must stop claiming the row's width or the
     toggle's justify-content:center would have nothing left to centre. */
  .app.is-sidebar-collapsed .sidebar__project-label,
  .app.is-sidebar-collapsed .sidebar__inbox-label {
    flex: 0 0 auto;
  }
  /* Carries the grouping the hidden PROJECTS, REMOVED and SETTINGS captions used to. */
  .app.is-sidebar-collapsed :is(.sidebar__projects, .sidebar__terminal, .sidebar__sections) {
    border-top: 1px solid var(--color-border);
    margin-top: var(--space-2);
    padding-top: var(--space-2);
  }
  .app.is-sidebar-collapsed .sidebar__project-children,
  .app.is-sidebar-collapsed .sidebar__inbox-children {
    padding-left: 0;
  }
  .app.is-sidebar-collapsed .sidebar__footer {
    padding: var(--space-2);
  }
  /* Sign out is the footer's only row now, so it centres like every other glyph in the rail.
     It used to get that for free from `flex-direction: column` + the base `align-items: center`,
     which only made sense while the email sat above it. */
  .app.is-sidebar-collapsed .sidebar__account {
    justify-content: center;
  }
}
/* ---------- Conversation list ---------- */
.app__list {
  border-right: 1px solid var(--color-border);
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--color-surface);
}

.list__header {
  position: sticky;
  top: 0;
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  padding: var(--space-4);
  z-index: 1;
}

.list__heading {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  margin-top: var(--space-3);
  min-width: 0;
}

.list__title {
  margin: 0;
  min-width: 0;
  font-size: var(--text-lg);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.list__header > .list__title:first-child {
  margin-top: 0;
}

.list__refresh {
  appearance: none;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-muted);
  cursor: pointer;
}

.list__refresh .icon {
  --icon-size: 14px;
}

.list__refresh:hover {
  color: var(--color-text);
}

/* An analytics drill-through list's way back to the page, the refresh control's size and weight. */
.list__back {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: var(--radius-sm);
  color: var(--color-muted);
}

.list__back .icon {
  --icon-size: 14px;
}

.list__back:hover {
  color: var(--color-text);
}

.list__back:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 2px;
}

.list__refresh:disabled {
  opacity: 0.55;
  cursor: default;
}

.list__refresh.is-busy,
.list__refresh.is-cooling {
  color: var(--color-text);
}

.list__refresh.is-busy .icon {
  animation: list-refresh-spin 0.7s linear infinite;
}

@keyframes list-refresh-spin {
  to {
    transform: rotate(360deg);
  }
}
@media (prefers-reduced-motion: reduce) {
  .list__refresh.is-busy .icon {
    animation: none;
  }
}
/* Four tabs, three of them carrying a number as well as a word — space-between with a floor on the
   gap rather than a fixed gap, so the strip spends whatever room the labels leave and never widens
   past the header it sits in. */
.list__tabs {
  display: flex;
  justify-content: space-between;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.list__tab {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-1);
  color: var(--color-muted);
  font-size: var(--text-sm);
  font-weight: 600;
  padding-bottom: var(--space-1);
  border-bottom: 2px solid transparent;
  /* The label is two words' worth of one thing; wrapping it mid-tab would make the strip two rows
     tall and push the whole list down. */
  white-space: nowrap;
}

.list__tab:hover {
  color: var(--color-text);
}

.list__tab[aria-current=true] {
  color: var(--color-text);
  border-bottom-color: var(--color-accent);
}

/* A plain number rather than the sidebar's filled pill: three pills across one strip reads as three
   alerts, and none of these is a notification — it is the size of a queue. Tabular figures so a
   count ticking 9 → 10 → 11 under a write does not shift the tabs beside it. */
.list__tab-count {
  font-size: 11px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--color-muted);
}

.list__tab[aria-current=true] .list__tab-count {
  color: var(--color-accent-text);
}

/* ---------- List toolbar (search / filter / sort) ---------- */
.list__toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.list__toolbar[hidden] {
  display: none;
}

.list__toolbar-form {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 1;
  min-width: 0;
}

/* ---------- Bulk bar (stands in for the toolbar while rows are ticked) ---------- */
/* In the toolbar's place rather than below it: .list__header is sticky, and a second row arriving
   would push the whole list down on every tick — a list that jumps under the pointer mid-selection
   is how a run of shift-clicks lands on the wrong rows. Same height as what it replaces, because
   both are a row of .list__tool-buttons. */
.list__bulk-bar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* Re-asserted so the `hidden` attribute wins over `display: flex` — the same trap .dropdown__menu
   documents above. */
.list__bulk-bar[hidden] {
  display: none;
}

.list__bulk-count {
  flex: 1;
  min-width: 0;
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 600;
  white-space: nowrap;
}

.list__bulk-actions {
  display: flex;
  align-items: center;
  gap: var(--space-1);
}

.list__bulk-action-form {
  margin: 0;
}

/* Taller than a two-option sort menu once a project has more than a handful of members, so cap
   and scroll the way .filter-menu does — otherwise a 12-person roster shears at .app__list. */
.list__bulk-assign-menu {
  min-width: 220px;
  max-height: 60vh;
  overflow-y: auto;
}

/* More actions (FRO-150) sits first in the bar, so its short menu opens rightwards, into the column,
   rather than from the trigger's right edge out past the list. */
.list__bulk-more .list__bulk-more-menu {
  left: 0;
  right: auto;
}

/* The bulk Move picker is fixed and held inside .app__list by typeahead_controller, which publishes
   the room it has as --typeahead-room; capped by that and by a readable width, rows ellipsize. */
.list__bulk-move-menu {
  min-width: 220px;
  max-width: min(320px, var(--typeahead-room, calc(100vw - 16px)));
  max-height: 60vh;
  overflow-y: auto;
}

.list__search {
  position: relative;
  flex: 1;
  min-width: 0;
}

.list__search-icon {
  position: absolute;
  left: var(--space-2);
  top: 50%;
  transform: translateY(-50%);
  color: var(--color-muted);
  pointer-events: none;
}

.list__search-input {
  padding-left: calc(var(--space-5) + var(--space-1));
}

.list__tool-button {
  position: relative;
  appearance: none;
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--color-border-strong);
  background: var(--color-surface);
  color: var(--color-muted);
  border-radius: var(--radius-md);
  padding: var(--space-2);
  cursor: pointer;
}

.list__tool-button:hover {
  color: var(--color-text);
}

.list__tool-button[aria-expanded=true] {
  color: var(--color-accent-text);
  border-color: var(--color-accent);
}

/* Active-filter dot on the funnel — hidden unless a non-default filter is set. */
.list__tool-button__dot {
  display: none;
  position: absolute;
  top: 3px;
  right: 3px;
  width: 7px;
  height: 7px;
  border-radius: 999px;
  background: var(--color-accent);
  border: 1px solid var(--color-surface);
}

.list__tool-button__dot.is-active {
  display: block;
}

/* ---------- Dropdown menu (filter / sort) ---------- */
.dropdown {
  position: relative;
}

.dropdown__menu {
  position: absolute;
  right: 0;
  top: calc(100% + var(--space-1));
  z-index: 20;
  min-width: 168px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-1);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
}

/* Re-assert so the `hidden` attribute wins over `display:flex` (the slice-1 trap). */
.dropdown__menu[hidden] {
  display: none;
}

/* A menu whose rows come from the server holds them in a frame (see remote_options_controller).
   `display: contents` keeps that invisible: the rows stay flex items of the menu, so a fetched
   option sits in the column exactly where a rendered one would. */
.dropdown__menu > turbo-frame {
  display: contents;
}

.dropdown__option {
  appearance: none;
  border: 0;
  background: transparent;
  text-align: left;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  font: inherit;
  color: var(--color-text);
  cursor: pointer;
  white-space: nowrap;
  position: relative;
}

.dropdown__option:hover,
.dropdown__option.is-active {
  background: var(--color-accent-soft);
}

.dropdown__option[aria-checked=true] {
  color: var(--color-accent-text);
  font-weight: 600;
  background: var(--color-accent-soft);
}

/* Re-assert so the `hidden` attribute wins over `display:flex` on --checkable (the slice-1 trap). */
.dropdown__option[hidden] {
  display: none;
}

/* ---------- Timestamp popover (UTC / local) ---------- */
/* The same panel recipe as .dropdown__menu — surface, hairline, --radius-md, --shadow-md — but
   fixed rather than absolute, and parented to the body. Every timestamp it serves sits inside a
   container that sets overflow (.app__list, .thread__scroll, .thread-info), so an absolutely
   positioned panel would be sheared at the very edge it opens against. The controller writes
   top/left from the trigger's viewport rect. */
.time-popover {
  position: fixed;
  /* Over .dropdown__menu (20) and the .flash toast (50), under .modal-overlay (100) — no
     timestamp is drawn inside the settings modal, so this must never cover one. */
  z-index: 60;
  /* Purely informational, and two of the three triggers sit inside a row's navigation link. If
     the panel could take a pointer it would swallow the click that opens the thread — the same
     reason .flash gives up its own pointer events. */
  pointer-events: none;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-1) var(--space-3);
  /* A long IANA zone (America/Argentina/ComodRivadavia) wraps inside the panel instead of
     widening it past a narrow viewport. */
  max-width: min(260px, 100vw - 16px);
  padding: var(--space-2) var(--space-3);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
}

/* Re-assert so the `hidden` attribute wins over `display:grid` — the same slice-1 trap
   .dropdown__menu[hidden] guards against. */
.time-popover[hidden] {
  display: none;
}

/* Mirrors .filter-menu__label, so these captions read as the same system as every other
   grouping label in the app. */
.time-popover__label {
  margin: 0;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--color-muted);
}

.time-popover__label[hidden] {
  display: none;
}

.time-popover__value {
  margin: 0;
  font-size: var(--text-sm);
  white-space: nowrap;
}

.time-popover__value[hidden] {
  display: none;
}

/* Qualifies the local line rather than standing as a third fact, so it sits under it in the
   value column. */
.time-popover__zone {
  grid-column: 2;
  margin: 0;
  font-size: 11px;
  color: var(--color-muted);
}

/* Snooze duration menu. The same panel recipe as .dropdown__menu, but fixed and parented
   to the body so .app__list's overflow cannot shear it — see .time-popover above. */
.snooze-popover.dropdown__menu {
  position: fixed;
  top: auto;
  right: auto;
  /* Over the hover cluster (2) and the list, under .modal-overlay (100). */
  z-index: 60;
  min-width: 220px;
}

.snooze-popover[hidden] {
  display: none;
}

.snooze-popover__custom {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-2);
  min-width: 252px;
}

.snooze-popover__custom[hidden] {
  display: none;
}

.snooze-popover__field {
  margin-bottom: 0;
}

.snooze-popover__error {
  margin: 0;
  font-size: 12px;
  color: var(--color-danger);
}

.snooze-popover__error[hidden] {
  display: none;
}

.snooze-popover__actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2);
}

/* .btn--primary is full-width for the sidebar's + Project form; here it shares a row with Back. */
.snooze-popover__actions .btn--primary {
  width: auto;
}

/* datetime-local keeps UA chrome for the calendar, the way .field__select keeps a native list —
   both escape this fixed panel. appearance:none + the field tokens make the closed control
   read as the rest of the app rather than a platform text box. */
.snooze-popover input[type=datetime-local].field__input {
  appearance: none;
  min-width: 0;
}

.snooze-popover input[type=datetime-local].field__input::-webkit-calendar-picker-indicator {
  cursor: pointer;
  opacity: 0.65;
}

/* Reaction glyph palette. Fixed and parented to the body for the reason the two panels above are:
   every note it serves sits inside .thread__scroll, which sets overflow. */
.reaction-palette.dropdown__menu {
  position: fixed;
  top: auto;
  right: auto;
  z-index: 60;
  padding: var(--space-2);
}

.reaction-palette[hidden] {
  display: none;
}

/* Per-message ⋯ menu. Fixed and parented to the body for the reason the two
   panels above are: every card it serves sits inside .thread__scroll. */
.message-menu.dropdown__menu {
  position: fixed;
  top: auto;
  right: auto;
  z-index: 60;
  min-width: 220px;
}

.message-menu[hidden] {
  display: none;
}

.message-menu .dropdown__option {
  display: block;
  width: 100%;
  box-sizing: border-box;
  text-decoration: none;
}

/* Same trap .dropdown__option[hidden] exists to catch: display:block here is
   more specific than the attribute, so a Slack or view-only card would paint
   Reply all / Forward as if they were offered. */
.message-menu .dropdown__option[hidden] {
  display: none;
}

/* Contact card behind a name on a message. Fixed and parented to the body for
   the reason the panels above are: every trigger sits inside .thread__scroll. */
.contact-popover.dropdown__menu {
  position: fixed;
  top: auto;
  right: auto;
  z-index: 60;
  min-width: 220px;
  max-width: min(320px, 100vw - 16px);
  padding: var(--space-2);
  gap: var(--space-1);
}

.contact-popover[hidden] {
  display: none;
}

.contact-popover__header {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
  padding: var(--space-1) var(--space-2) var(--space-2);
}

.contact-popover__initials {
  flex: none;
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: var(--color-accent-soft);
  color: var(--color-accent-text);
  font-size: 12px;
  font-weight: 700;
}

.contact-popover__identity {
  min-width: 0;
}

.contact-popover__name {
  margin: 0;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.contact-popover__email {
  margin: 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.contact-popover__email[hidden] {
  display: none;
}

.contact-popover__copy {
  min-width: 0;
}

.contact-popover__copy[hidden] {
  display: none;
}

.contact-popover__action {
  display: flex;
  width: 100%;
  box-sizing: border-box;
}

.contact-popover__compose {
  margin-top: var(--space-1);
}

.contact-popover__compose[hidden] {
  display: none;
}

.contact-popover .btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Authored for <button>, so appearance/font/border have to be set or the UA
   chrome stays — the same trap .copy-id__button documents. */
.contact-popover-trigger {
  appearance: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  font: inherit;
  color: inherit;
  cursor: pointer;
  text-align: inherit;
}

.contact-popover-trigger:hover,
.contact-popover-trigger:focus-visible {
  color: var(--color-accent-text);
  text-decoration: underline;
}

.contact-popover-trigger:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 2px;
}

.message-card__from.contact-popover-trigger {
  font-weight: 600;
}

/* Auto-filled, so how many glyphs sit on a row is this rule's answer and the controller measures it
   off the layout rather than being told — the same arrangement .composer__emoji-grid has. Narrower
   than the composer's palette because this one floats over a card instead of spanning a pane. */
.reaction-palette__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(32px, 1fr));
  gap: 2px;
  width: min(296px, 100vw - 16px);
  max-height: 40vh;
  overflow-y: auto;
}

.reaction-palette__option {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  /* The glyph is the content, so it is sized like a glyph rather than like body text. */
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}

.reaction-palette__option:hover {
  background: var(--color-comment-band-strong);
}

.reaction-palette__option:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: -2px;
}

/* Sectioned filter popover (Status / Type / Priority) — taller than a plain menu, so cap + scroll. */
.filter-menu {
  min-width: 184px;
  max-height: 60vh;
  overflow-y: auto;
}

.filter-menu__section {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.filter-menu__label {
  margin: var(--space-1) var(--space-2) 2px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--color-muted);
}

.filter-menu__hint {
  margin: 2px var(--space-2) var(--space-1);
  font-size: 12px;
  color: var(--color-muted);
}

.conversation-rows {
  list-style: none;
  margin: 0;
  padding: 0;
}

.conversation-row {
  position: relative;
  border-bottom: 1px solid var(--color-border);
}

.conversation-row__link {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas: "badges source" "contact time" "subject count" "snippet snippet";
  gap: var(--space-1) var(--space-2);
  /* The left padding clears the selection gutter below rather than the row's own edge: a checkbox
     overlaying this content would cover the start of the sender and the subject, which is the one
     thing the cluster on the right never has to do. */
  padding: var(--space-3) var(--space-4) var(--space-3) calc(var(--space-4) + var(--space-5));
}

.conversation-row__link:hover {
  background: var(--color-surface-sunken);
}

.conversation-row:hover .conversation-row__link,
.conversation-row:focus-within .conversation-row__link {
  background: var(--color-surface-sunken);
}

.conversation-row__link[aria-current=true] {
  background: var(--color-accent-soft);
  box-shadow: var(--conversation-row-current-glow);
}

.conversation-row:hover .conversation-row__link[aria-current=true],
.conversation-row:focus-within .conversation-row__link[aria-current=true] {
  background: var(--color-accent-soft);
}

.conversation-row__contact {
  grid-area: contact;
  color: var(--color-muted);
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.conversation-row__subject {
  grid-area: subject;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.conversation-row__snippet {
  grid-area: snippet;
  color: var(--color-muted);
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.conversation-row__time {
  grid-area: time;
  color: var(--color-muted);
  font-size: 12px;
  white-space: nowrap;
}

/* The shared badge stripped to coloured text here — a row already carries three lines, and a run of
   pills on top of them competes with the subject for attention. Only the chrome is dropped; the
   .badge--* variant still owns the colour, so the strip and the pane's chips stay one language.
   Wrapping rather than scrolling: several types must push the sender down, never widen the list or
   clip a type off the right edge, and the wider gap is what separates them once the pills are gone. */
/* align-items: uniform 11px text needed no cross-axis rule; a fixed-height glyph beside it does. */
.conversation-row__badges {
  grid-area: badges;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0 var(--space-3);
  min-width: 0;
}

.conversation-row__badges .badge {
  display: block;
  padding: 0;
  border: 0;
  background: none;
  font-size: 11px;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The glyph stands for the value on its own here, so this wrapper carries the accessible name.
   It also pulls back from the strip's shared gap: --space-3 is what two type NAMES need between
   them to read as separate words, and a 14px mark set against the first of them needs less than
   that or it drifts away from the row it labels. Expressed as the difference between the two
   tokens rather than a bare -4px, so it still tracks the scale if the gap ever changes. */
.conversation-row__priority {
  display: inline-flex;
  margin-right: calc(var(--space-2) - var(--space-3));
}

/* The source mark, alone in the badge row's right-hand cell. Muted and sized to the 11px badge text
   beside it rather than to the row's body copy: it labels the thread, it is not a thing to read
   first, and at the badges' own weight it would out-shout the types it sits level with.
   Both self-alignments are load-bearing. justify-self, because the right-hand column is sized by
   the timestamp under it — left to stretch, the mark sits at the left edge of an 80px cell, which
   is nowhere near the corner it is meant to occupy. align-self, because the badge strip wraps: the
   mark stays level with the first line of types rather than floating to the middle of three. */
.conversation-row__source {
  grid-area: source;
  justify-self: end;
  align-self: start;
  display: inline-flex;
  color: var(--color-muted);
  font-size: 15px;
}

.conversation-row__count {
  grid-area: count;
  align-self: center;
  color: var(--color-accent-text);
  font-weight: 700;
  font-size: var(--text-sm);
  text-align: right;
  white-space: nowrap;
}

/* Selection gutter: sibling of the row link, in the space its left padding reserves. Hidden at rest
   and revealed on hover and :focus-within like the cluster on the right — and pinned for EVERY row
   while a selection exists, because a gutter that vanished between two clicks would turn extending a
   selection into a hunt for where the last box was. A ticked box stays visible on its own account,
   which is what a selection scrolled past has to look like. */
.conversation-row__select {
  position: absolute;
  left: var(--space-4);
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  width: 15px;
  height: 15px;
  margin: 0;
  /* accent-color rather than a rebuilt control: the native checkbox is the one input in this app that
     already reads correctly at this size, and all it wanted was the app's own blue. */
  accent-color: var(--color-accent);
  cursor: pointer;
  opacity: 0;
  /* Out of the way of the pointer while it is invisible, as the cluster on the right also is. Without
     it the gutter would swallow clicks meant for the row's link — which reaches under here, since the
     gutter is the link's own padding — and ticking a thread would be what "clicking nothing" did. */
  pointer-events: none;
}

.conversation-row:hover .conversation-row__select,
.conversation-row:focus-within .conversation-row__select,
.conversation-row__select:checked,
.list-scope.is-selecting .conversation-row__select {
  opacity: 1;
  pointer-events: auto;
}

/* A ticked row keeps looking ticked once the pointer leaves it, which is the only way a selection
   built one click at a time stays legible. The same wash the open thread's row gets: a row can be
   both, and there is nothing useful to say by drawing them differently. */
.conversation-row:has(.conversation-row__select:checked) .conversation-row__link {
  background: var(--color-accent-soft);
}

/* Hover cluster: sibling of the row link, stacked above it on the right edge. Hidden at rest
   (including from pointer events) so the timestamp underneath still receives hover for the
   time popover; revealed on row hover AND :focus-within so a keyboard user reaching the
   buttons actually sees them. */
.conversation-row__actions {
  position: absolute;
  top: 50%;
  right: var(--space-3);
  transform: translateY(-50%);
  z-index: 2;
  display: flex;
  gap: var(--space-1);
  padding-left: var(--space-2);
  background: var(--color-surface);
  opacity: 0;
  pointer-events: none;
}

.conversation-row:hover .conversation-row__actions,
.conversation-row:focus-within .conversation-row__actions {
  opacity: 1;
  pointer-events: auto;
  background: var(--color-surface-sunken);
}

.conversation-row:hover:has(.conversation-row__link[aria-current=true]) .conversation-row__actions,
.conversation-row:focus-within:has(.conversation-row__link[aria-current=true]) .conversation-row__actions {
  background: var(--color-accent-soft);
}

.conversation-row__action-form {
  margin: 0;
}

.conversation-row__action {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-muted);
  font: inherit;
  cursor: pointer;
}

.conversation-row__action:hover {
  color: var(--color-text);
  background: var(--color-surface-sunken);
}

.conversation-row__action .icon {
  --icon-size: 14px;
}

/* A notification row carries no checkbox — the list is a feed, not a queue you clear — so it
   takes its left padding back rather than indenting every row past an empty gutter. */
.notification-row .conversation-row__link {
  grid-template-areas: "contact time" "subject subject" "snippet snippet";
  padding-left: var(--space-4);
}

.notification-row--unread .conversation-row__subject {
  font-weight: 600;
}

/* A second destination on one row (FRO-152's paused-forwarding alert): its own link under the row's,
   because a link cannot sit inside the link that opens the thread. */
.notification-row__secondary {
  display: block;
  padding: 0 var(--space-4) var(--space-3);
  color: var(--color-accent-text);
  font-size: var(--text-sm);
  text-decoration: underline;
}

.notification-row__secondary:hover {
  color: var(--color-text);
}

/* ---------- Conversation thread ---------- */
/* position: relative for .thread-loading below, which covers the pane rather than sitting inside
   the frame it covers — see conversations/_pane_loading. */
.app__thread {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  height: 100%;
  overflow: hidden;
  background: var(--color-surface-sunken);
}

/* The turbo-frame fills the pane and lays out its two columns: the thread and the info rail. The
   grid lives here rather than on .app because a list row navigates THIS frame, and a frame
   navigation swaps only the frame it targets — a rail outside it would keep describing the thread
   the reader just left.
   `auto` on the second track so the "Select a conversation" empty state (a lone child) reserves no
   dead column; minmax(0,1fr) on the row so the thread's inner scroll region sizes to the frame
   instead of to its own content. */
#conversation {
  flex: 1;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: minmax(0, 1fr);
}

/* No info rail on compose — span the pane the way the empty state does. */
#conversation > .compose {
  grid-column: 1/-1;
  min-width: 0;
  min-height: 0;
}

/* turbo_stream_from renders a real element. In `#conversation`'s grid it would take
   a track and wrap the rail; in the body it is `display: inline` and still makes a
   line box — 12px after a viewport-tall `.app`, which unlocks the document scroll.
   It draws nothing, so it gets no box. */
turbo-cable-stream-source {
  display: none;
}

/* ---------- The pane while it is fetching a thread ---------- */
/* Not there at all at rest: visibility rather than opacity alone, so a screen reader is not
   walking a skeleton laid over the thread that is actually on screen, and the live region inside
   it only speaks when the cover genuinely goes up. thread_pane_controller owns the class. */
.thread-loading {
  position: absolute;
  inset: 0;
  z-index: 1;
  visibility: hidden;
  opacity: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-5);
  background: var(--color-surface-sunken);
}

/* Delayed, and the delay is the feature: a short thread opens inside it and never flashes a
   skeleton, while a long one has something to show before the ~150 ms at which a click stops
   feeling connected to its answer. A CSS delay rather than a setTimeout, so clicking four rows in
   a second leaves no timers to cancel. `forwards` and not `backwards` — the cover must stay down
   for the whole delay, which is the half of this that does the work. */
.app__thread.is-loading .thread-loading {
  animation: thread-loading-raise 120ms ease-out 100ms forwards;
}

@keyframes thread-loading-raise {
  from {
    visibility: visible;
    opacity: 0;
  }
  to {
    visibility: visible;
    opacity: 1;
  }
}
.thread-loading__card {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-4) var(--space-5);
}

.thread-loading__line {
  height: 10px;
  border-radius: var(--radius-pill);
  background: var(--color-border);
}

.thread-loading__line--head {
  width: 40%;
  background: var(--color-border-strong);
}

.thread-loading__line--short {
  width: 60%;
}

/* The one sentence a screen reader gets. Same recipe as .composer__status — the app has no
   visually-hidden utility and each place that needs one spells it out. */
.thread-loading__label {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

@media (prefers-reduced-motion: no-preference) {
  .thread-loading__line {
    animation: thread-loading-pulse 1.4s ease-in-out infinite;
  }
}
@keyframes thread-loading-pulse {
  50% {
    opacity: 0.4;
  }
}
/* The fade goes, the delay stays: the delay is not decoration, it is what keeps a fast thread
   from flashing a skeleton at all. */
@media (prefers-reduced-motion: reduce) {
  .app__thread.is-loading .thread-loading {
    animation-duration: 1ms;
  }
}
/* The thread column: fixed header + scrolling messages + pinned comment bar. */
.thread {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
}

.thread__header {
  flex-shrink: 0;
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  padding: var(--space-4) var(--space-5);
}

.thread__subject {
  margin: 0;
  font-size: var(--text-xl);
  min-width: 0;
}

/* Subject and the Participants trigger share the title line: the roster is a control, and under
   the subject it would sit in the strip of live status and read as another thing that changes on
   its own. align-items:flex-start so a subject that wraps to two lines keeps the button on the
   first, beside the title rather than floating at its middle. */
.thread__title-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
}

/* ---------- Participants popover (thread header) ---------- */
/* Nested rather than parented to the body, unlike the contact card and the ⋯ menu: those open
   from cards inside .thread__scroll, which shears a descendant panel. .thread__header is outside
   it, so an absolute .dropdown__menu is safe here.
   The trigger follows .list__tool-button's recipe — hairline box, muted glyph, accent when
   expanded — without borrowing its class, whose name belongs to the list toolbar. */
.thread-participants {
  position: relative;
  flex-shrink: 0;
}

.thread-participants__trigger {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-muted);
  font: inherit;
  font-size: var(--text-sm);
  padding: var(--space-2);
  cursor: pointer;
}

.thread-participants__trigger:hover {
  color: var(--color-text);
}

.thread-participants__trigger[aria-expanded=true] {
  color: var(--color-accent-text);
  border-color: var(--color-accent);
}

/* Tabular figures so the count does not shift the button's width as people join the thread. */
.thread-participants__count {
  font-variant-numeric: tabular-nums;
}

/* The panel is a .dropdown__menu and inherits its surface, hairline and shadow. What it adds is a
   width the two-line rows need and a ceiling on the list, which grows with the thread. */
.thread-participants__panel {
  width: 320px;
  gap: var(--space-1);
  padding: var(--space-2);
}

.thread-participants__heading {
  margin: 0;
  padding: var(--space-1) var(--space-2);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-muted);
}

.thread-participants__list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: 320px;
  overflow-y: auto;
  /* Padding, cancelled by the margin, so the global :focus-visible ring — 2px at 2px offset — has
     room inside the scroll box. overflow-y:auto clips on BOTH axes, so without it the Subscribe
     button's ring would be shaved at the panel's edge. */
  padding: var(--space-1);
  margin: calc(var(--space-1) * -1);
}

.thread-participants__person {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2);
  border-radius: var(--radius-sm);
}

.thread-participants__identity {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-width: 0;
}

/* A long name truncates — there is nothing to lose but characters of one word, and a name that
   wrapped would push the status line around. */
.thread-participants__name {
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The status wraps rather than truncating, because every fact on it is load-bearing: clipped to
   one line, "Not subscribed · Assignee · Viewing now" loses the presence the row exists to show. */
.thread-participants__status {
  color: var(--color-muted);
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
}

.thread-participants__toggle-form {
  flex-shrink: 0;
}

/* .btn's padding is sized for a full-width action; in a roster row it is a trailing control, so
   it shrink-wraps and loses the block width --primary sets (in case the variant ever changes). */
.thread-participants__toggle {
  width: auto;
  white-space: nowrap;
}

/* Says who can OPEN the thread, which is a different set from the roster above — hence the rule
   separating them. */
.thread-participants__footer {
  margin: 0;
  padding: var(--space-2);
  border-top: 1px solid var(--color-border);
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.thread-viewers {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-2);
}

.thread-viewers[hidden] {
  display: none;
}

.thread-viewers__person {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-width: 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.thread-viewers__person[hidden] {
  display: none;
}

.thread-viewers__name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.thread-reply-collision {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-warning-soft);
  color: var(--color-text);
  font-size: var(--text-sm);
}

.thread-reply-collision[hidden] {
  display: none;
}

.thread-reply-collision__icon {
  --icon-size: 14px;
  flex: none;
  color: var(--color-warning);
}

.thread-reply-collision__person {
  display: none;
}

.thread-reply-collision__name {
  color: var(--color-text);
  font-weight: 600;
}

.thread-reply-collision__actions {
  margin-left: auto;
  display: flex;
  gap: var(--space-2);
}

.thread-reply-collision__actions[hidden] {
  display: none;
}

.thread-draft-handoff {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-accent-soft);
  color: var(--color-text);
  font-size: var(--text-sm);
}

.thread-draft-handoff__icon {
  --icon-size: 14px;
  flex: none;
  color: var(--color-accent-text);
}

.thread-draft-handoff__copy {
  flex: 1;
}

.thread-draft-handoff .btn {
  margin-left: auto;
}

.thread-duplicate-nudge {
  margin-top: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-accent-soft);
  border: 1px solid var(--color-accent-edge);
  color: var(--color-text);
  font-size: var(--text-sm);
}

.thread-duplicate-nudge__head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.thread-duplicate-nudge__icon {
  --icon-size: 14px;
  flex: none;
  color: var(--color-accent-text);
}

.thread-duplicate-nudge__title {
  flex: 1;
  font-weight: 600;
}

.thread-duplicate-nudge__dismiss-form {
  margin: 0;
}

.thread-duplicate-nudge__list {
  display: grid;
  gap: var(--space-2);
  margin: var(--space-2) 0 0;
  padding: 0;
  list-style: none;
}

.thread-duplicate-nudge__candidate {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.thread-duplicate-nudge__subject {
  flex: 1 1 12rem;
  min-width: 0;
  overflow-wrap: break-word;
}

.thread-duplicate-nudge__meta {
  color: var(--color-muted);
  white-space: nowrap;
}

.thread-duplicate-nudge__actions {
  display: flex;
  gap: var(--space-1);
  flex: none;
}

.thread-duplicate-nudge__mark {
  margin: 0;
}

.scheduled-outbound {
  margin-top: var(--space-3);
  padding: var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-accent-soft);
  border: 1px solid var(--color-accent-edge);
  color: var(--color-text);
  font-size: var(--text-sm);
}

.scheduled-outbound__status {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.scheduled-outbound__icon {
  --icon-size: 14px;
  flex: none;
  color: var(--color-accent-text);
}

.scheduled-outbound__copy {
  flex: 1;
}

.scheduled-outbound__undo {
  margin: 0 0 0 auto;
}

.scheduled-outbound__undo[hidden] {
  display: none;
}

.scheduled-outbound__recipients,
.scheduled-outbound__excerpt {
  margin: var(--space-2) 0 0;
  color: var(--color-muted);
}

.scheduled-outbound--compose {
  margin-bottom: var(--space-3);
}

.scheduled-outbound--failed {
  background: var(--color-danger-soft);
  border-color: var(--color-danger-edge);
}

.scheduled-outbound--failed .scheduled-outbound__icon {
  color: var(--color-danger);
}

.scheduled-outbound__reason {
  margin: var(--space-2) 0 0;
  color: var(--color-danger);
}

.scheduled-outbound__reason[hidden] {
  display: none;
}

/* ---------- Conversation info rail (the thread's 4th pane) ---------- */
/* Surface rather than the pane's sunken background, so it reads as its own region beside the
   thread — the same treatment the conversation list gets. */
.thread-info {
  /* The track, not the width: .app owns both, because this rail is swapped with every thread. */
  width: var(--rail-track);
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  padding: var(--space-4);
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--color-surface);
  border-left: 1px solid var(--color-border);
}

.thread-info__section {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* The rail's collapse toggle (FRO-138): top-right while open; collapsed, the rail is a 40px strip
   holding only this button, its glyph turned to point back out. Mirrors .sidebar__collapse-toggle. */
.thread-info__collapse-toggle {
  align-self: flex-end;
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  margin: calc(var(--space-2) * -1) calc(var(--space-2) * -1) calc(var(--space-3) * -1) 0;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm, 6px);
  background: transparent;
  color: var(--color-muted);
  cursor: pointer;
  flex: none;
}

.thread-info__collapse-toggle:hover {
  background: var(--color-surface-sunken);
  color: var(--color-text);
}

.thread-info__collapse-toggle:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 1px;
}

@media (min-width: 901px) {
  .app.is-rail-collapsed {
    --rail-track: 40px;
  }
  .app.is-rail-collapsed .thread-info {
    padding: var(--space-2) 0;
    align-items: center;
    overflow: hidden;
  }
  .app.is-rail-collapsed .thread-info > :not(.thread-info__collapse-toggle) {
    display: none;
  }
  .app.is-rail-collapsed .thread-info__collapse-toggle {
    align-self: center;
    margin: 0;
  }
  .app.is-rail-collapsed .thread-info__collapse-icon {
    transform: rotate(180deg);
  }
}
/* Mirrors .filter-menu__label, so a section caption here reads as the same system as the one
   above the toolbar's filter groups. */
.thread-info__heading {
  margin: 0;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--color-muted);
}

/* Triage. The two pickers are buttons, not inputs, so the captions above them are spans — laid
   out as their own row so a wrapped set of chips never collides with the label beside it. */
/* The assignee sits outside the triage form (its own resource), so the wrapper carries the same
   gap the form does — otherwise it would butt straight up against Priority. */
.thread-info__fields {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.thread-info__form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.thread-info__field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  align-items: flex-start;
}

.thread-info__label {
  font-size: var(--text-sm);
  color: var(--color-muted);
}

/* The summary. The one thing in the rail that wraps on purpose — every other value here is a single
   line clipped to an ellipsis, and a summary cut off mid-sentence would be worth less than no
   summary. Full contrast rather than muted: it is the section a reader came for, not a caption on
   one. break-word because the prose is the client's, and theirs is where a pasted stack trace or an
   unbroken URL turns up — .message-card__body guards the same content for the same reason. */
.thread-info__summary {
  margin: 0;
  font-size: var(--text-sm);
  line-height: 1.5;
  word-break: break-word;
}

/* Details. Ticket and the Linear peek stay on the first glance; arrival facts and Linear meta
   sit behind disclosures so the rail stays short. */
.thread-info__group {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.thread-info__group-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.thread-info__group-peek {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  min-width: 0;
  margin-left: auto;
}

.thread-info__group-label {
  color: var(--color-muted);
  font-size: var(--text-sm);
  font-weight: 600;
}

/* Button-as-caption: authored for <button>, so appearance and font have to be reset or the
   UA chrome stays. Same caret-swap the quoted-body disclosure uses. */
.thread-info__disclosure-toggle {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: var(--color-muted);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
}

.thread-info__disclosure-toggle:hover {
  color: var(--color-text);
}

.thread-info__disclosure-caret {
  --icon-size: 14px;
  flex: none;
}

.thread-info__disclosure.is-open .thread-info__disclosure-caret {
  transform: rotate(180deg);
}

.thread-info__disclosure-label--hide {
  display: none;
}

.thread-info__disclosure.is-open .thread-info__disclosure-label--show {
  display: none;
}

.thread-info__disclosure.is-open .thread-info__disclosure-label--hide {
  display: inline;
}

/* A grid rather than the <dl>'s default block flow, so the terms share one column and
   the values line up against it. [hidden] has to be re-asserted: display:grid would
   otherwise keep a closed disclosure painted. */
.thread-info__rows {
  margin: 0;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-2) var(--space-3);
  align-items: baseline;
}

.thread-info__rows[hidden] {
  display: none;
}

.thread-info__term {
  color: var(--color-muted);
  font-size: var(--text-sm);
}

/* The padding is not decoration: `overflow: hidden` (for the ellipsis) also shears anything drawn
   OUTSIDE a child's box, and the project link's focus ring is drawn there. The negative margin
   gives the ring its 2px back without moving the text off the term column's baseline grid. */
.thread-info__value {
  margin: 0 calc(var(--space-1) * -1);
  padding: var(--space-1);
  min-width: 0;
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.thread-info__muted {
  color: var(--color-muted);
  font-size: var(--text-sm);
}

/* The workflow state's colour, as Linear holds it. A dot rather than a badge: the rows around it
   are quiet text, and a pill here would out-shout the ticket key above it. Decorative — the state's
   name sits beside it, so the colour is never the only thing carrying the meaning. */
.thread-info__state-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  margin-right: var(--space-2);
  border-radius: 999px;
  background: var(--state-color);
  /* Keeps a pale state colour from vanishing into the surface. */
  box-shadow: inset 0 0 0 1px var(--color-swatch-ring);
}

.thread-info__link {
  color: var(--color-accent-text);
  font-weight: 600;
}

.thread-info__link:hover {
  color: var(--color-link-hover);
  text-decoration: underline;
}

/* Linear-style ID chip: the identifier is the button, a copy icon beside it, brief Copied
   on the label. Authored for <button>, so appearance/font/border have to be set or the UA
   chrome stays. Cousin of .thread__linear, not a reuse: that chip is a link that leaves
   the app, and this one must not navigate. */
.copy-id {
  position: relative;
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-1);
  min-width: 0;
}

.copy-id__button {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  max-width: 100%;
  margin: 0;
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: 1.2;
  cursor: pointer;
}

.copy-id__button:hover {
  border-color: var(--color-accent);
  color: var(--color-accent-text);
}

.copy-id__button.is-on {
  border-color: var(--color-accent);
  color: var(--color-accent-text);
}

.copy-id__icon {
  --icon-size: 14px;
  flex: none;
}

.copy-id__label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Cannot be display:none — select() needs a focusable field. Revealed only when the
   clipboard write did not land, so the agent can still ⌘C the value. */
.copy-id__value {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
}

.copy-id__value.is-revealed {
  position: static;
  width: 100%;
  height: auto;
  clip: auto;
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: var(--text-sm);
}

/* Success is already on the button label. The live region stays for AT; only the
   no-clipboard hint is painted, because that is the one instruction the button
   does not already carry. */
.copy-id__status {
  font-size: 12px;
  color: var(--color-muted);
}

.copy-id__status:empty {
  display: none;
}

.copy-id__status:not(.is-hint) {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
}

.thread-info__linear-id {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-width: 0;
}

/* Icon-only sibling of the copy chip. Authored for <a>; the border/padding match the chip
   so the pair reads as one control. */
.thread-info__linear-open {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  padding: var(--space-1);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  color: var(--color-text);
  background: var(--color-surface);
}

.thread-info__linear-open:hover {
  border-color: var(--color-accent);
  color: var(--color-accent-text);
}

.thread-info__linear-open-icon {
  --icon-size: 14px;
}

/* Sender. Tighter than the section's own gap: the name and the address are one block of two lines,
   not two separate facts. Both truncate — an address is the longest plausible string in the rail. */
.thread-info__sender {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.thread-info__sender-name {
  margin: 0;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.thread-info__sender-email {
  margin: 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The standing "this sender is never judged" line. Muted and small like the address above it —
   it is a fact about the sender, not a control, and the row it replaces is a text action. */
.thread-info__sender-note {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  padding: var(--space-1) 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.thread-info__sender-note .icon {
  --icon-size: 14px;
  flex-shrink: 0;
}

/* A borderline spam verdict waiting on a person (conversations/_spam_review_notice). Warning-toned
   like the row's Possible spam badge, so the two read as one question. */
.thread-info__spam-review {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-warning);
  border-radius: var(--radius-sm);
  background: var(--color-warning-soft);
}

.thread-info__spam-review .thread-info__sender-note {
  color: var(--color-warning);
}

.thread-info__sender-picker {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.thread-info__sender-menu {
  min-width: 220px;
  max-width: min(360px, 100vw - 16px);
  max-height: 60vh;
  overflow-y: auto;
}

.thread-info__sender-option {
  display: flex;
  flex-direction: column;
  min-width: 0;
  gap: 1px;
}

.thread-info__sender-option-name {
  overflow: hidden;
  text-overflow: ellipsis;
}

.thread-info__sender-option-email {
  color: var(--color-muted);
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
}

.thread-info__sender-error {
  margin: 0;
  color: var(--color-danger);
  font-size: var(--text-sm);
}

.thread-info__sublabel {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--color-muted);
}

/* The Customer section (FRO-141). The slot is always rendered so Change sender can replace it by
   id; contents, so an empty one is not a gap in the rail's flex column. The frame inside carries
   the section's own column, since the heading and Refresh live in it. */
.thread-info__slot {
  display: contents;
}

.thread-info__section > turbo-frame {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
}

.customer-lookup__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}

.customer-lookup__refresh {
  padding: 0;
  font-size: var(--text-sm);
}

.customer-lookup {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
}

.customer-lookup__status,
.customer-lookup__updated {
  margin: 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.customer-lookup__updated {
  font-size: 11px;
}

.customer-card {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

.customer-card__source {
  margin: 0;
  color: var(--color-muted);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
}

.customer-card__title {
  margin: 0;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.customer-card__title--link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  color: var(--color-text);
  text-decoration: none;
}

.customer-card__title--link span {
  overflow: hidden;
  text-overflow: ellipsis;
}

.customer-card__title--link:hover span {
  text-decoration: underline;
}

.customer-card__title--link:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 2px;
  border-radius: var(--radius-sm, 4px);
}

.customer-card__open {
  --icon-size: 12px;
  flex-shrink: 0;
  color: var(--color-muted);
}

.customer-card__subtitle {
  margin: 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.customer-card__facts {
  margin: var(--space-1) 0 0;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 2px var(--space-3);
  font-size: var(--text-sm);
}

.customer-card__facts dt {
  color: var(--color-muted);
}

.customer-card__facts dd {
  margin: 0;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Store orders (FRO-137). The order the thread names sits first and is set apart by a tinted edge,
   not a louder card: it is the same kind of fact as the cards under it. */
.customer-card--referenced {
  border-left: 3px solid var(--color-border-strong);
}

.customer-card--matched {
  border-left-color: var(--color-success);
}

.customer-card--linked {
  border-left-color: var(--color-accent);
}

.customer-card--unmatched {
  border-left-color: var(--color-warning);
}

.customer-card__headline {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  min-width: 0;
}

.customer-card__badge {
  flex-shrink: 0;
  padding: 0 var(--space-2);
  font-size: 11px;
}

.customer-card__action {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  margin-top: var(--space-1);
  color: var(--color-accent-text);
  font-size: var(--text-sm);
  text-decoration: none;
}

.customer-card__action:hover span {
  text-decoration: underline;
}

.customer-card__action:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 2px;
  border-radius: var(--radius-sm, 4px);
}

.customer-card__unlink {
  align-self: flex-start;
  margin-top: var(--space-1);
  padding: 0;
  font-size: var(--text-sm);
}

/* The sender as the system's own staff (FRO-153): an accent edge and a badge, so an agent never
   reads as one more lease. Former or suspended staff are greyed, not hidden. */
.customer-card--agent {
  border-left: 3px solid var(--color-accent);
  background: var(--color-accent-soft);
}

.customer-card--agent .customer-card__headline {
  justify-content: flex-start;
  flex-wrap: wrap;
  gap: var(--space-1);
}

/* "3 active leases · last lease 14 Aug 2026" is one fact and too long for the rail: wrap it. */
.customer-card--agent .customer-card__facts dd {
  white-space: normal;
}

.customer-card--inactive {
  border-left-color: var(--color-border-strong);
  background: var(--color-surface-sunken);
}

.customer-card--inactive .customer-card__title,
.customer-card--inactive .customer-card__facts dd {
  color: var(--color-muted);
}

.customer-lookup__subheading {
  margin: var(--space-1) 0 0;
  color: var(--color-muted);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
}

.customer-lookup__link-order > summary {
  list-style: none;
  cursor: pointer;
  font-size: var(--text-sm);
}

.customer-lookup__link-order > summary::-webkit-details-marker {
  display: none;
}

.customer-lookup__link-form {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.customer-lookup__link-form .field__input {
  flex: 1;
  min-width: 0;
}

.customer-lookup__error {
  margin: var(--space-1) 0 0;
  color: var(--color-danger);
  font-size: var(--text-sm);
}

/* Their other threads. The negative margin lets a row's hover bleed to the rail's padding edge
   without the rows themselves sitting flush against it. */
.thread-info__threads {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

.thread-info__thread-link {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--space-2);
  align-items: baseline;
  padding: var(--space-2);
  margin: 0 calc(var(--space-2) * -1);
  border-radius: var(--radius-sm);
}

.thread-info__thread-link:hover {
  background: var(--color-surface-sunken);
}

.thread-info__thread-subject {
  min-width: 0;
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.thread-info__thread-time {
  color: var(--color-muted);
  font-size: 12px;
  white-space: nowrap;
}

.thread-info__more {
  font-size: var(--text-sm);
  color: var(--color-accent-text);
  font-weight: 600;
}

/* "See all"-style actions, wherever they sit, take the theme's action-link casing (FRO-156). */
:is(.thread-info__more, .trend__see-all, .command-palette__option--see-all .command-palette__option-label) {
  text-transform: var(--action-link-transform);
  letter-spacing: var(--action-link-tracking);
}

.thread-info__more:hover {
  color: var(--color-link-hover);
  text-decoration: underline;
}

/* .empty-state's --space-6 padding and centering are built for a whole pane; in a 300px rail they
   read as a hole rather than an answer. */
.thread-info__empty {
  margin: 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

/* What happened to this thread, as a compact timeline. Not .thread-info__threads: those rows are
   links with a hover, and these are statements — so no negative margin and no hover surface, which
   would promise a click that does nothing. */
.thread-info__events {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

/* Marker | sentence | compact time. The sentence wraps; the time (`43m ago`) must not. First-line
   aligned so a two-line sentence still has its opening line level with the node and the stamp. */
.thread-info__event {
  display: grid;
  grid-template-columns: 8px minmax(0, 1fr) auto;
  column-gap: var(--space-2);
  align-items: start;
  position: relative;
}

/* Spine from this node's centre through the gap to the next. The last event has no tail. */
.thread-info__event:not(:last-child)::before {
  content: "";
  position: absolute;
  left: 4px;
  top: 8px;
  bottom: calc(-1 * var(--space-1) - 8px);
  width: 1px;
  transform: translateX(-50%);
  background: var(--color-border);
}

.thread-info__event-marker {
  width: 8px;
  height: 8px;
  margin-top: 4px;
  border-radius: var(--radius-pill);
  background: var(--color-border-strong);
  box-shadow: 0 0 0 2px var(--color-surface);
  position: relative;
  z-index: 1;
}

.thread-info__event-text {
  min-width: 0;
  font-size: 12px;
  line-height: 1.35;
}

/* A move's quoted reason and its Linear note (FRO-150), under the sentence. Unclamped: the reason is
   the point, and it is capped at input. */
.thread-info__event-detail {
  display: block;
  margin-top: 2px;
  color: var(--color-muted);
  overflow-wrap: anywhere;
}

.thread-info__event-time {
  color: var(--color-muted);
  font-size: 11px;
  line-height: 1.35;
  white-space: nowrap;
  margin-top: 1px;
}

/* Actions. Destinations share one row — the same four moves as the list hover cluster. Assignment
   and re-run sit underneath as text, because they are not destinations. */
.thread-info__actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* button_to wraps its button in a form, which would otherwise become the grid/flex child and leave
   the button unstretched. display:contents hands the button straight to the cluster or the text
   stack. */
.thread-info__action-form {
  margin: 0;
  display: contents;
}

.thread-info__action-cluster {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
  gap: var(--space-1);
}

/* Same 28px bordered icon language as .conversation-row__action, stretched into a labeled cell so
   the rail — which does not reveal-on-hover — still names each move. */
.thread-info__cluster-action {
  appearance: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  width: 100%;
  min-width: 0;
  height: 100%;
  padding: var(--space-2) var(--space-1);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-muted);
  font: inherit;
  font-size: 11px;
  font-weight: 600;
  line-height: 1.2;
  cursor: pointer;
}

.thread-info__cluster-action:hover {
  color: var(--color-text);
  background: var(--color-surface-sunken);
}

.thread-info__cluster-icon {
  --icon-size: 14px;
}

.thread-info__cluster-caption {
  text-align: center;
}

.thread-info__text-actions {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-1);
}

.thread-info__text-action {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  padding: var(--space-1) 0;
  border: 0;
  background: none;
  color: var(--color-muted);
  font: inherit;
  font-size: var(--text-sm);
  cursor: pointer;
}

.thread-info__text-action:hover {
  color: var(--color-text);
}

.thread-info__text-action--accent {
  color: var(--color-accent-text);
  font-weight: 600;
}

.thread-info__text-action--accent:hover {
  color: var(--color-link-hover);
  text-decoration: underline;
}

.thread-info__text-action .icon {
  --icon-size: 14px;
}

/* The duplicate notice. Accent-soft rather than danger: a copy is dealt with, not wrong, and
   the same wash the settings notice uses. The link is the original's ticket (or subject), so
   it has to read as a control against that wash. */
.thread-info__duplicate {
  margin: 0;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-accent-soft);
  color: var(--color-accent-text);
  font-size: var(--text-sm);
}

.thread-info__duplicate-link {
  font-weight: 600;
  text-decoration: underline;
}

.thread-info__duplicate-link:hover {
  color: var(--color-text);
}

.badge {
  display: inline-flex;
  align-items: center;
  padding: 2px var(--space-2);
  border: 1px solid;
  border-radius: 999px;
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: 1.4;
}

.badge--danger {
  color: var(--color-danger);
  border-color: var(--color-danger);
  background: var(--color-danger-soft);
}

.badge--success {
  color: var(--color-success);
  border-color: var(--color-success);
  background: var(--color-success-soft);
}

.badge--accent {
  color: var(--color-accent-text);
  border-color: var(--color-accent);
  background: var(--color-accent-soft);
}

.badge--warning {
  color: var(--color-warning);
  border-color: var(--color-warning);
  background: var(--color-warning-soft);
}

.badge--muted {
  color: var(--color-muted);
  border-color: var(--color-border-strong);
  background: var(--color-surface-sunken);
}

/* A badge whose colour is not one of ours: a request type, which carries a hex an admin picked or
   a Linear label brought with it. The three properties are written inline by BadgeColor — this rule
   only says which of them goes where, so the derivation stays in one testable place. */
.badge--custom {
  color: var(--badge-fg);
  border-color: var(--badge-border);
  background: var(--badge-bg);
}

/* Priority as a hand-drawn glyph (conversations/_priority_icon): the same four tokens the badges
   carry, now with a shape as well, which is what lets the list row drop the word.

   `fill: currentColor` so one colour rule paints the whole drawing, and the colour is set on the
   svg itself rather than inherited — .dropdown__option[aria-checked="true"] recolours its own text
   to accent, and an element's own rule beats an inherited value whatever the specificity, so
   Urgent stays red while it is the checked option. */
.priority-icon {
  width: 14px;
  height: 14px;
  flex: 0 0 auto;
  display: block;
  fill: currentColor;
}

.priority-icon--muted {
  color: var(--color-muted);
}

.priority-icon--accent {
  color: var(--color-accent-text);
}

.priority-icon--warning {
  color: var(--color-warning);
}

.priority-icon--danger {
  color: var(--color-danger);
}

/* Every level draws all three bars and dims the ones above it, so the glyph keeps one footprint
   and the level reads from how many are lit rather than from how much ink is on the row. Dimmed
   rather than dropped: an unlit bar is what makes "one of three" legible instead of just "small". */
.priority-icon--level-1 .priority-icon__bar:nth-child(n+2),
.priority-icon--level-2 .priority-icon__bar:nth-child(n+3) {
  opacity: 0.26;
}

/* Editable metadata trigger: the .badge look on a <button>. Reset native chrome only —
   never the badge's background/color (the .badge--* variant owns those). */
.metadata-field__trigger {
  appearance: none;
  font-family: inherit;
  gap: var(--space-1);
  cursor: pointer;
}

.metadata-field__trigger:hover {
  filter: brightness(0.97);
}

.metadata-field__trigger:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 1px;
}

/* Open/active: tighten the border to accent without an outset ring (avoids overflow clipping). */
.metadata-field__trigger[aria-expanded=true] {
  border-color: var(--color-accent);
}

/* Unset placeholder ("Set type") — dashed to read as "click to classify". */
.metadata-field__trigger--empty {
  border-style: dashed;
  font-weight: 500;
}

.metadata-field__label {
  white-space: nowrap;
}

.metadata-field__caret {
  --icon-size: 12px;
  opacity: 0.7;
}

/* Picker + Assign to me on one row. Sibling forms (the take posts current_user.id; the
   typeahead posts whoever you pick), so this wrapper — not a nested form — is what lines
   them up. Wraps on a narrow rail the way the request-type chips do. */
.metadata-field__control {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  min-width: 0;
  max-width: 100%;
}

/* The typeahead form is a flex item; without this it can take the row and wrap the take. */
.metadata-field__control > form {
  width: max-content;
  max-width: 100%;
}

/* Button-as-link: .thread-info__link is authored for <a>, so its chrome would stay on a
   <button>. Reset native appearance and reuse the accent/weight/underline language. */
.metadata-field__assign-me {
  appearance: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: var(--color-accent-text);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
}

.metadata-field__assign-me:hover {
  color: var(--color-link-hover);
  text-decoration: underline;
}

.metadata-field__assign-me:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 1px;
}

/* Under the assignee picker: min-width:0 so a long "Assigned by the Access Issue · urgent rule"
   wraps inside the rail instead of overflowing it. The field is a column flex, so this is its
   own row rather than sitting beside the chip. */
.metadata-field__hint {
  min-width: 0;
  max-width: 100%;
}

.metadata-field__plus {
  --icon-size: 12px;
  opacity: 0.7;
}

/* Request types: chips sit beside the "+ Add type" trigger inside one field. Scoped to the --multi
   modifier so the single-select priority field keeps exactly the layout it has today. */
.metadata-field--multi {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.metadata-field__chips {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.metadata-field__chip {
  gap: var(--space-1);
}

/* The chip's remove button. Reusing a badge's insides on a <button> keeps the browser's own
   background and border, so those are stripped here — and the box is padded out to a real target
   rather than the ~10px the glyph alone would give. */
.badge__remove {
  appearance: none;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  min-height: 18px;
  margin-right: -4px;
  padding: 0 2px;
  border-radius: 999px;
  cursor: pointer;
  opacity: 0.7;
}

.badge__remove:hover {
  opacity: 1;
  background: color-mix(in srgb, currentColor 12%, transparent);
}

.badge__remove:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 1px;
}

.badge__remove-icon {
  --icon-size: 10px;
}

/* Checkable menu option: the tick is always laid out so labels stay aligned whether or not the
   row is on, and selection reads three ways (tick, weight, background) rather than by color alone. */
.dropdown__option--checkable {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* --iconed reserves a glyph column with padding rather than a flex gap, and hangs the glyph in it.
   That is what keeps "None" — the one option with no glyph, because unset is not a level — on the
   same text column as the four above it, the way "Clear all" already lines up under its ticks. */
.dropdown__option--iconed {
  position: relative;
  padding-left: calc(var(--space-3) + 14px + var(--space-2));
}

.dropdown__option--iconed .priority-icon {
  position: absolute;
  left: var(--space-3);
  top: 50%;
  transform: translateY(-50%);
}

.dropdown__option-check {
  --icon-size: 12px;
  flex-shrink: 0;
}

/* Secondary text trailing an option's label — "Out of office → Priya" — pushed to the far edge so
   names stay on one column whether or not a row carries one. */
.dropdown__option-hint {
  margin-left: auto;
  padding-left: var(--space-2);
  color: var(--color-muted);
  font-size: var(--text-sm);
  white-space: nowrap;
}

.dropdown__option[aria-checked=false] .dropdown__option-check {
  visibility: hidden;
}

/* Check + glyph: both hang in reserved columns so "None" (no glyph) still shares the text start. */
.dropdown__option--checkable.dropdown__option--iconed {
  padding-left: calc(var(--space-3) + 12px + var(--space-2) + 14px + var(--space-2));
}

.dropdown__option--checkable.dropdown__option--iconed .dropdown__option-check {
  position: absolute;
  left: var(--space-3);
  top: 50%;
  transform: translateY(-50%);
}

.dropdown__option--checkable.dropdown__option--iconed .priority-icon {
  left: calc(var(--space-3) + 12px + var(--space-2));
}

/* "Clear all" carries no tick, so it is indented by the tick's width + gap to sit on the same
   text column as the options above it. */
.metadata-field__clear .dropdown__option {
  padding-left: calc(var(--space-3) + 12px + var(--space-2));
}

.dropdown__divider {
  height: 1px;
  margin: var(--space-1) 0;
  background: var(--color-border);
}

/* Metadata triggers are left-anchored, so their menu opens left-aligned — the toolbar's
   right:0 (its triggers sit on the right) would run this menu off the left edge.

   Capped and scrolled for the same reason .filter-menu is: the request-type list is admin-managed
   now, so its length is not knowable from here. Uncapped, a taxonomy of a dozen types opens a panel
   taller than the thread pane and .app__thread's overflow shears the bottom of it off — and no
   amount of scrolling recovers a clipped panel. */
.metadata-field .dropdown__menu {
  left: 0;
  right: auto;
  min-width: 220px;
  max-height: 60vh;
  overflow-y: auto;
}

.dropdown__empty {
  margin: 0;
  padding: var(--space-2) var(--space-3);
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.dropdown__empty[hidden],
.dropdown__divider[hidden] {
  display: none;
}

/* Typeahead search row. Sticky so a long taxonomy keeps the filter in view; digits sit in a
   reserved right column via ::after so they never enter the option's text (or Capybara's). */
.typeahead__search {
  position: sticky;
  top: calc(-1 * var(--space-1));
  z-index: 1;
  margin: calc(-1 * var(--space-1)) calc(-1 * var(--space-1)) 0;
  padding: var(--space-1);
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
}

.typeahead__query {
  appearance: none;
  width: 100%;
  border: 0;
  background: transparent;
  font: inherit;
  color: var(--color-text);
  padding: var(--space-2);
}

.typeahead__query::placeholder {
  color: var(--color-muted);
}

.typeahead__query:focus {
  outline: none;
}

.dropdown__option[data-typeahead-digit] {
  padding-right: calc(var(--space-3) + 1.25em);
}

.dropdown__option[data-typeahead-digit]::after {
  content: attr(data-typeahead-digit);
  position: absolute;
  right: var(--space-3);
  top: 50%;
  transform: translateY(-50%);
  color: var(--color-muted);
  font-size: 12px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}

/* ---------- Analytics report ---------- */
/* Flex column with a non-scrolling header and a scrolling body — the same shape .app__thread /
   .thread__scroll already have, which is what lets the range and project pickers be ordinary
   dropdowns rather than body-parented fixed panels. */
.report {
  display: flex;
  flex-direction: column;
  min-width: 0;
  overflow: hidden;
  background: var(--color-surface-sunken);
}

.report__header {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-5);
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
}

.report__heading {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  align-items: flex-start;
  min-width: 0;
}

.report__back {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--color-muted);
  text-decoration: none;
  font-size: var(--text-sm);
}

.report__back:hover {
  color: var(--color-text);
}

.report__back-icon {
  --icon-size: 14px;
}

.report__title {
  margin: 0;
  font-size: var(--text-2xl);
  font-weight: var(--font-display-weight, 600);
  letter-spacing: var(--tracking-tight);
}

.report__page {
  padding: var(--space-6);
  background: var(--color-surface);
  min-height: 100%;
}

.report__scope,
.report__scope-form {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.report__scope .dropdown__menu {
  left: 0;
  right: auto;
}

.report__picker {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text);
  border-radius: var(--radius-pill);
  padding: var(--space-2) var(--space-4);
  font: inherit;
  font-weight: 500;
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: border-color 0.12s ease, box-shadow 0.12s ease, transform 0.12s ease;
}

.report__picker:hover {
  border-color: var(--color-border-strong);
  box-shadow: var(--shadow-md);
  transform: translateY(-1px);
}

.report__picker[aria-expanded=true] {
  color: var(--color-accent-text);
  border-color: var(--color-accent);
  box-shadow: var(--shadow-sm);
  transform: none;
}

.report__picker-caret {
  --icon-size: 12px;
  opacity: 0.7;
}

.report__export {
  appearance: none;
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-muted);
  border-radius: var(--radius-pill);
  padding: var(--space-2) var(--space-4);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: color 0.12s ease, border-color 0.12s ease, box-shadow 0.12s ease, transform 0.12s ease;
}

.report__export:hover {
  color: var(--color-text);
  border-color: var(--color-border-strong);
  box-shadow: var(--shadow-md);
  transform: translateY(-1px);
}

.report__scroll {
  display: block;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

.report__panels {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-5);
  padding: var(--space-5);
}

.report-panel {
  background: var(--color-surface);
  border: 1px solid var(--color-hairline);
  border-radius: var(--radius-xl);
  padding: var(--space-6);
  box-shadow: var(--shadow-lg);
  min-width: 0;
}

.report-panel--wide {
  grid-column: 1/-1;
}

/* The panel name is a caption, not a headline: the figures under it are the reason to look at
   the page, so the h2 deliberately sits below them in weight and size. */
.report-panel__title {
  margin: 0 0 var(--space-4);
  color: var(--color-muted);
  font-size: var(--text-sm);
  font-weight: var(--font-display-weight, 600);
  letter-spacing: 0.02em;
}

.report-panel__subtitle {
  margin: var(--space-5) 0 var(--space-3);
  font-size: var(--text-sm);
  font-weight: 600;
}

.report-panel__caption {
  margin: var(--space-3) 0 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.report-panel__legend {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: var(--space-3) 0 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.report-panel__swatch {
  display: inline-block;
  width: 8px;
  height: 8px;
  margin-right: var(--space-1);
  border-radius: var(--radius-pill);
  vertical-align: middle;
}

.report-panel__swatch--in {
  background: var(--color-accent);
}

.report-panel__swatch--out {
  background: var(--color-success);
}

.chart--volume {
  display: flex;
  align-items: stretch;
  gap: 3px;
  height: 200px;
  margin: 0;
  padding: 0;
  list-style: none;
  border-bottom: 1px solid var(--color-border);
  /* Quarter-height rules, so a bar's height is readable against something. Drawn as a background
     gradient rather than four spacer elements — the bars are the only children worth having. */
  background-image: repeating-linear-gradient(to top, transparent 0 calc(25% - 1px), var(--color-hairline) calc(25% - 1px) 25%);
}

.chart__day {
  position: relative;
  flex: 1;
  min-width: 0;
  height: 100%;
}

.chart__bars {
  display: flex;
  align-items: flex-end;
  gap: 1px;
  width: 100%;
  height: 100%;
}

/* min-height stays 0 on purpose: analytics_bar_percent already floors a non-zero day at 2%, so a
   minimum here would only paint a bar for a day that had no mail at all. */
.chart__bar {
  flex: 1;
  height: var(--bar);
  min-height: 0;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}

.chart__bar--in {
  background: var(--gradient-accent);
}

.chart__bar--out {
  background: var(--gradient-success);
}

/* The day's figures are already in aria-label for the screen reader and the specs, so the tooltip
   is that same string drawn by CSS — no duplicate copy in the markup and nothing for JS to do. */
.chart__day::after {
  content: attr(aria-label);
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  z-index: 1;
  margin-bottom: var(--space-1);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  background: var(--color-text);
  color: var(--color-surface);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  box-shadow: var(--shadow-md);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.12s ease;
}

/* .report__scroll is overflow-y: auto, which clips the x axis too, so a tooltip centred on the
   first or last column would be sheared at the panel edge. Anchor those two inside the chart. */
.chart__day:first-child::after {
  left: 0;
  transform: none;
}

.chart__day:last-child::after {
  left: auto;
  right: 0;
  transform: none;
}

.chart__day:hover::after {
  opacity: 1;
  visibility: visible;
}

/* Hovering one day recedes the rest, so the tooltip is read against its own column. */
.chart--volume:hover .chart__day {
  opacity: 0.4;
}

.chart--volume .chart__day:hover {
  opacity: 1;
}

.chart--volume .chart__day {
  transition: opacity 0.12s ease;
}

/* An Over time bar opens that day's threads, so it answers the pointer the way a count does. */
a.chart__bar:hover {
  filter: brightness(1.15);
}

a.chart__bar:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 1px;
}

/* Four charts stack in the Over time panel; at the Volume chart's height it would be a page of its own. */
.chart--timeline {
  height: 120px;
}

/* The product-area rows' short form: the same columns at row height, inside a table cell, without
   the quarter rules — at this height they would be all the reader sees. */
.chart--spark {
  height: 32px;
  background-image: none;
}

.report-timeline__areas td.report-timeline__spark {
  width: 60%;
  min-width: 160px;
  text-align: left;
}

.chart--share {
  display: flex;
  height: 32px;
  overflow: hidden;
  border-radius: var(--radius-pill);
  background: var(--color-surface-sunken);
}

/* Each segment declares its own hue in --share-fill and the sheen is applied once here, so the
   positional palette below stays a list of colours rather than a list of gradients. */
.chart__share {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 var(--share);
  min-width: 0;
  overflow: hidden;
  font-size: 11px;
  font-weight: 600;
  color: var(--color-on-accent);
  white-space: nowrap;
  background-image: linear-gradient(180deg, color-mix(in srgb, var(--share-fill) 86%, var(--color-surface)), var(--share-fill));
}

/* Hairline gaps in the page's own surface colour, so neighbouring segments stay countable when
   two of them are close in hue. */
.chart__share + .chart__share {
  box-shadow: inset 1px 0 0 var(--color-surface);
}

a.chart__share {
  text-decoration: none;
}

a.chart__share:hover {
  text-decoration: underline;
}

a.chart__share:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 1px;
}

.chart__share:nth-child(1) {
  --share-fill: var(--color-accent);
}

.chart__share:nth-child(2) {
  --share-fill: var(--color-danger);
}

.chart__share:nth-child(3) {
  --share-fill: var(--color-warning);
}

.chart__share:nth-child(4) {
  --share-fill: var(--color-success);
}

.chart__share:nth-child(5) {
  --share-fill: var(--color-muted);
}

/* Fast to slow, so green sits at the fast end. The positional palette above puts success LAST,
   which on a duration bar paints the slowest threads as the good news. */
.chart--share-graded .chart__share:nth-child(1) {
  --share-fill: var(--color-success);
}

.chart--share-graded .chart__share:nth-child(2) {
  --share-fill: color-mix(in srgb, var(--color-success) 45%, var(--color-warning));
}

.chart--share-graded .chart__share:nth-child(3) {
  --share-fill: var(--color-warning);
}

.chart--share-graded .chart__share:nth-child(4) {
  --share-fill: var(--color-danger);
}

.report-stats {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-5);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* A boxed tile inside an already-bordered panel is a border inside a border. The figures carry
   themselves at this size, so the tile is whitespace and a single hairline between columns. */
.report-stat {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.report-stat:nth-child(even) {
  padding-left: var(--space-5);
  border-left: 1px solid var(--color-hairline);
}

.report-stat__label {
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.report-stat__value {
  font-size: var(--text-2xl);
  font-weight: 600;
  letter-spacing: var(--tracking-tight);
  font-variant-numeric: tabular-nums;
}

/* "about 3 hours" is prose, not a figure: it needs the smaller size to sit on one line in a
   half-width panel, and tabular figures do nothing for it. */
.report-stat__value--duration {
  font-size: var(--text-xl);
  letter-spacing: normal;
  font-variant-numeric: normal;
}

/* The secondary counts in Classification are a list, not a row of headline figures — a stack of
   four 28px numbers would outshout the panel's own share bars. Same markup, denser reading. */
.report-stats--compact {
  grid-template-columns: 1fr;
  gap: 0;
  margin-top: var(--space-5);
}

.report-stats--compact .report-stat {
  flex-direction: row;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--color-hairline);
}

.report-stats--compact .report-stat:last-child {
  border-bottom: 0;
}

.report-stats--compact .report-stat:nth-child(even) {
  padding-left: 0;
  border-left: 0;
}

.report-stats--compact .report-stat__value {
  font-size: var(--text-lg);
  letter-spacing: normal;
}

/* The headline strip sits above the panel grid, in the grid's own gutters, so it reads as the
   page's summary rather than as one more panel. One row of tiles at full width, wrapping to as
   many columns as fit; the hairline between tiles is the same one .report-stat uses. */
.report-summary {
  margin: var(--space-5) var(--space-5) 0;
  background: var(--color-surface);
  border: 1px solid var(--color-hairline);
  border-radius: var(--radius-xl);
  padding: var(--space-5) var(--space-6);
  box-shadow: var(--shadow-lg);
}

/* Same recipe as .thread-loading__label — the app has no visually-hidden utility. */
.report-summary__title {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Fixed column counts rather than auto-fit, so the tile that starts each row is known and can drop
   the hairline; an auto-fit wrap would leave a stray rule and indent on whichever tile wrapped. */
.report-summary__tiles {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: var(--space-4) 0;
  margin: 0;
  padding: 0;
  list-style: none;
}

.report-summary__tile {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
  padding: 0 var(--space-4);
  border-left: 1px solid var(--color-hairline);
  cursor: help;
}

.report-summary__tile:first-child {
  padding-left: 0;
  border-left: 0;
}

@media (max-width: 1280px) {
  .report-summary__tiles {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
  .report-summary__tile:nth-child(4n+1) {
    padding-left: 0;
    border-left: 0;
  }
}
@media (max-width: 640px) {
  .report-summary__tiles {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .report-summary__tile:nth-child(4n+1) {
    padding-left: var(--space-4);
    border-left: 1px solid var(--color-hairline);
  }
  .report-summary__tile:nth-child(odd) {
    padding-left: 0;
    border-left: 0;
  }
}
.report-summary__note {
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.report-glossary {
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-hairline);
  font-size: var(--text-sm);
}

.report-glossary summary {
  color: var(--color-muted);
  font-weight: 600;
  cursor: pointer;
}

.report-glossary__terms {
  margin: var(--space-3) 0 0;
}

.report-glossary__term {
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--color-hairline);
}

.report-glossary__term:last-child {
  border-bottom: 0;
}

.report-glossary__term dt {
  font-weight: 600;
}

.report-glossary__term dd {
  margin: var(--space-1) 0 0;
}

.report-glossary__term p {
  margin: 0 0 var(--space-2);
  color: var(--color-muted);
}

.report-glossary__term pre {
  margin: 0;
  padding: var(--space-2) var(--space-3);
  overflow-x: auto;
  background: var(--color-surface-sunken);
  border-radius: var(--radius-md);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  white-space: pre-wrap;
}

/* Attention: five cards across the wide panel, each a link into the inbox. A card with nothing in
   it stays on the hairline; one with threads in it takes its tone's soft fill and a coloured edge,
   so what needs someone is the thing the eye lands on. */
.report-attention {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-3);
  margin: 0 0 var(--space-3);
  padding: 0;
  list-style: none;
}

.report-attention__item {
  min-width: 0;
}

.report-attention__card {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  height: 100%;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--color-hairline);
  border-left-width: 3px;
  border-radius: var(--radius-md);
  color: inherit;
  text-decoration: none;
}

a.report-attention__card:hover {
  border-color: var(--color-border-strong);
}

a.report-attention__card:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 2px;
}

.report-attention__card--warning {
  background: var(--color-warning-soft);
  border-left-color: var(--color-warning);
}

.report-attention__card--danger {
  background: var(--color-danger-soft);
  border-left-color: var(--color-danger);
}

.report-attention__card--danger .report-stat__value {
  color: var(--color-danger);
}

.report-attention__note {
  overflow: hidden;
  color: var(--color-muted);
  font-size: var(--text-sm);
  text-overflow: ellipsis;
  white-space: nowrap;
}

@media (max-width: 1280px) {
  .report-attention {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}
@media (max-width: 640px) {
  .report-attention {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
/* Trends: two lists side by side inside one wide panel, collapsing to a stack where the panel
   grid itself does. */
.report-panel__columns {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-6);
}

.report-panel__column {
  min-width: 0;
}

.report-panel__column .report-panel__subtitle {
  margin-top: var(--space-4);
}

.trend-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.trend {
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--color-hairline);
}

.trend:last-child {
  border-bottom: 0;
}

/* Chip, then the figures pushed to the right edge so every row's count and delta share one
   column — the same reason the per-assignee table right-aligns its numbers. */
.trend__head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.trend__project {
  color: var(--color-muted);
  font-size: var(--text-sm);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.trend__count {
  margin-left: auto;
  font-size: var(--text-lg);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* The delta carries its own sign and its own colour, so a reader scanning the column sees the
   direction before reading the number. Steady stays muted: no change is not news. */
.trend__delta {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  min-width: 3.5ch;
  justify-content: flex-end;
  font-size: var(--text-sm);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--color-muted);
}

.trend__delta--rising {
  color: var(--color-danger);
}

.trend__delta--falling {
  color: var(--color-success);
}

.trend__delta-icon {
  --icon-size: 14px;
}

.trend__samples {
  margin: var(--space-2) 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.trend__sample {
  display: block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--color-text);
  font-size: var(--text-sm);
  text-decoration: none;
}

.trend__sample:hover {
  color: var(--color-link-hover);
  text-decoration: underline;
}

/* Free-text themes: a grid of cards above the two taxonomy lists. Not boxed — the panel is already
   one — so each card is a hairline over its title, the way each list row is a hairline under its
   chip, and the grid wraps them as many across as fit. */
.trend-themes {
  margin-bottom: var(--space-4);
}

.trend-themes__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  column-gap: var(--space-6);
  margin: 0;
  padding: 0;
  list-style: none;
}

.trend-themes__grid .trend {
  min-width: 0;
  border-top: 1px solid var(--color-hairline);
  border-bottom: 0;
}

.trend__title {
  min-width: 0;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.trend-themes__grid .trend__project {
  margin: var(--space-1) 0 0;
}

.trend__see-all {
  display: inline-block;
  margin-top: var(--space-2);
  color: var(--color-accent-text);
  font-size: var(--text-sm);
  text-decoration: none;
}

.trend__see-all:hover {
  color: var(--color-link-hover);
  text-decoration: underline;
}

/* The knowledge-gap note under a theme card: muted and small like the project line, because it is
   a prompt to act rather than a figure — the count above is still the finding. */
.trend__knowledge {
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px dashed var(--color-hairline);
}

.trend__knowledge-note {
  display: flex;
  align-items: flex-start;
  gap: var(--space-1);
  margin: 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.trend__knowledge-icon {
  --icon-size: 14px;
  flex: none;
  margin-top: 2px;
}

.trend__knowledge-actions {
  display: flex;
  flex-wrap: wrap;
  column-gap: var(--space-3);
  margin: 0;
}

/* Product insights: a Trends row plus one line of actions — See all, then where it stands in
   Linear. The Linear line is small and muted like See all's neighbours; Escalate is the one
   accented control, as Create issue is on the rail. button_to wraps it in a form, which is kept
   inline so the pair sits on one line. */
.insight .trend__project {
  margin: var(--space-1) 0 0;
}

.insight__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: var(--space-3);
}

.insight__linear {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  margin-top: var(--space-2);
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.insight__linear-icon {
  --icon-size: 14px;
}

.insight__escalate-form {
  display: inline;
  margin: 0;
}

.insight__escalate {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  margin-top: var(--space-2);
  padding: 0;
  border: 0;
  background: none;
  color: var(--color-accent-text);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
}

.insight__escalate:hover {
  color: var(--color-link-hover);
  text-decoration: underline;
}

.insight__escalate:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 2px;
}

.insight__escalate .icon {
  --icon-size: 14px;
}

.report-counts {
  margin: 0;
  padding: 0;
  list-style: none;
}

.report-counts li {
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--color-hairline);
  font-size: var(--text-sm);
}

.report-counts li:last-child {
  border-bottom: 0;
}

.report-counts li span:last-child {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.report-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}

.report-table th, .report-table td {
  padding: var(--space-3);
  text-align: left;
  border-bottom: 1px solid var(--color-hairline);
}

.report-table thead th {
  color: var(--color-muted);
  font-weight: 600;
  letter-spacing: 0.02em;
}

.report-table tbody th {
  font-weight: 600;
}

.report-table td {
  font-variant-numeric: tabular-nums;
}

/* Person stays left; every measured column is right-aligned with its heading so the figures
   share one edge. "about 3 hours" rides along — a column that changes alignment by row type
   reads worse than one prose cell hanging off the right rule. */
.report-table th:not(:first-child), .report-table td {
  text-align: right;
}

.report-table tbody tr {
  transition: background-color 0.12s ease;
}

.report-table tbody tr:hover {
  background: var(--color-surface-sunken);
}

.report-table tbody tr:last-child th, .report-table tbody tr:last-child td {
  border-bottom: 0;
}

/* The Escalations rows are tickets, not measured columns: every cell is words, so every cell reads
   from the left rather than hanging off the right rule. */
.report-escalations__rows th:not(:first-child), .report-escalations__rows td {
  text-align: left;
}

/* Product area × request type. Scrolls sideways inside the panel rather than widening the page —
   a workspace with a dozen types is wider than a phone, and the area column stays readable. None
   is set apart (muted, italic, a tinted band) because it is the gap, not a category. */
.report-breakdown {
  overflow-x: auto;
  margin-top: var(--space-3);
}

.report-table-scroll {
  overflow-x: auto;
}

.report-breakdown__table {
  min-width: max-content;
}

.report-breakdown__table th[scope=row] {
  white-space: nowrap;
}

.report-breakdown__table tfoot th, .report-breakdown__table tfoot td {
  border-top: 1px solid var(--color-hairline);
  border-bottom: 0;
  font-weight: 600;
}

.report-breakdown__link,
.report-drill {
  color: inherit;
  text-decoration: none;
}

.report-breakdown__link {
  font-weight: 600;
}

.report-breakdown__link:hover, .report-breakdown__link:focus-visible,
.report-drill:hover, .report-drill:focus-visible {
  text-decoration: underline;
}

.report-breakdown__empty {
  color: var(--color-muted);
}

.report-breakdown__none {
  color: var(--color-muted);
  font-style: italic;
}

.report-breakdown__none-row {
  background: var(--color-surface-sunken);
}

.report-breakdown__shared {
  margin-left: var(--space-2);
  color: var(--color-muted);
  font-size: var(--text-sm);
  font-weight: 400;
}

.report-breakdown__delta {
  margin-left: var(--space-2);
}

.report-banner {
  margin: 0 0 var(--space-4);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--color-hairline);
  border-left: 3px solid color-mix(in srgb, var(--color-accent) 30%, var(--color-border));
  border-radius: var(--radius-md);
  background: var(--color-surface-sunken);
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.report-panel__caveats {
  margin: var(--space-4) 0 0;
  padding: 0 0 0 var(--space-4);
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.report-panel__caveats li + li {
  margin-top: var(--space-2);
}

.chart--funnel {
  margin: 0;
  padding: 0;
  list-style: none;
}

.chart__funnel-row {
  display: grid;
  grid-template-columns: minmax(7rem, 11rem) 2.5rem minmax(0, 1fr);
  gap: var(--space-3);
  align-items: center;
  /* Pulled out to the panel's padding edge so the hover band spans the full row rather than
     stopping short of the label. */
  margin: 0 calc(var(--space-3) * -1);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  transition: background-color 0.12s ease;
}

.chart__funnel-row:hover {
  background: var(--color-surface-sunken);
}

.chart__funnel-label {
  font-size: var(--text-sm);
}

.chart__funnel-count {
  font-variant-numeric: tabular-nums;
  font-size: var(--text-sm);
  font-weight: 600;
  text-align: right;
}

.chart__funnel-track {
  height: 10px;
  overflow: hidden;
  border-radius: var(--radius-pill);
  background: var(--color-surface-sunken);
}

.chart__funnel-bar {
  display: block;
  height: 100%;
  width: var(--bar);
  border-radius: var(--radius-pill);
  background: linear-gradient(90deg, var(--color-accent), color-mix(in srgb, var(--color-accent) 72%, var(--color-surface)));
}

/* The shared empty state centres itself and carries its own space-6, which inside a panel that
   already has space-6 padding reads as a hole. Scoped to the panel so the Inbox keeps its own. */
.report-panel .empty-state {
  padding: var(--space-4) 0;
  text-align: left;
}

/* Two columns want about 1100px before a panel's stat pair and its 200px chart start to crowd.
   Below that the panels stack, which is a wider breakpoint than the app-wide 900px one. */
@media (max-width: 1100px) {
  .report__panels {
    grid-template-columns: 1fr;
  }
}
/* The two trend lists share a wide panel, so they have half the panel each at any width the
   grid is two-up — they only need to stack once the whole page has. */
@media (max-width: 700px) {
  .report-panel__columns {
    grid-template-columns: 1fr;
  }
}
@media (prefers-reduced-motion: reduce) {
  .report__picker,
  .report__export,
  .report-table tbody tr,
  .chart--funnel .chart__funnel-row,
  .chart--volume .chart__day,
  .chart__day::after {
    transition: none;
  }
  .report__picker:hover,
  .report__export:hover {
    transform: none;
  }
}
.thread__linear {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  color: var(--color-text);
  font-size: var(--text-sm);
  font-weight: 600;
  white-space: nowrap;
}

.thread__linear:hover {
  border-color: var(--color-accent);
  color: var(--color-accent-text);
}

.thread__linear--plain {
  cursor: default;
}

.thread__linear--plain:hover {
  border-color: var(--color-border-strong);
  color: var(--color-text);
}

.thread__linear-icon {
  --icon-size: 14px;
}

.thread__scroll {
  flex: 1;
  min-width: 0;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* Shared column for cards and the in-flow reply. Padding lives here so both edges align;
   the old 860px cap is gone — the pane is the reading width. */
.thread__flow {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-5);
  min-width: 0;
  max-width: 100%;
}

.thread__messages {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-width: 0;
  max-width: 100%;
}

/* Each page above the first arrives in a frame of its own, nested inside the frame whose link
   fetched it (see ConversationTimeline.frame_id). `display: contents` is what keeps that nesting
   invisible: the cards inside become flex items of .thread__messages, so a page loaded later has
   the same gaps as the one it joins instead of being one flush block. */
.thread__messages turbo-frame {
  display: contents;
}

.timeline-older__button {
  align-self: center;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

.timeline-older__icon {
  --icon-size: 14px;
}

/* Turbo marks the frame busy for the length of the fetch. A page of mail is slow enough to be
   worth saying so about, and the second click that would queue behind it buys nothing. */
.thread__messages turbo-frame[busy] > .timeline-older__button {
  opacity: 0.55;
  pointer-events: none;
}

.message-card {
  position: relative;
  min-width: 0;
  max-width: 100%;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-4) var(--space-5);
  box-shadow: var(--shadow-sm);
}

.message-card.is-anchor-target {
  outline: 2px solid var(--color-warning);
  outline-offset: 2px;
}

/* A middle message on a long thread, collapsed to one row until it is clicked (messages/_summary).
   The card keeps its direction tint and edge so the thread still reads as the same correspondence;
   only the padding shrinks, and the whole row is the link. */
.message-card--collapsed {
  padding: 0;
}

.message-summary {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  min-width: 0;
  padding: var(--space-3) var(--space-5);
  color: inherit;
  text-decoration: none;
  border-radius: inherit;
}

.message-summary:hover .message-summary__from {
  text-decoration: underline;
}

.message-summary__from {
  flex: none;
  font-weight: 600;
}

.message-summary__snippet {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.message-summary__time {
  flex: none;
  color: var(--color-muted);
  font-size: 12px;
}

.message-card--collapsed turbo-frame[busy] .message-summary {
  opacity: 0.6;
  cursor: progress;
}

/* Agent replies stay tinted, but they use the same full column as inbound mail. */
.message-card--outbound {
  background: var(--color-accent-soft);
  border-color: var(--color-accent-edge);
}

/* An internal comment is not mail, and has to be unmistakable about it at a glance — so it takes
   the one surface colour neither half of the correspondence uses, and a dashed edge, which reads as
   "not sent" the way a dashed border does everywhere else. Full width and not offset to either
   side: it is addressed to the whole team rather than passing between two people. */
/* Positioned so a note's hover actions can float on its top edge. */
.message-card--comment {
  position: relative;
  background: var(--color-comment-soft);
  border-color: var(--color-comment-border);
  border-style: dashed;
}

.message-card--comment .message-card__meta {
  align-items: center;
  border-bottom-color: var(--color-comment-border);
}

.message-card__author {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

/* The rule above the file chips follows the card it is drawn on, or a grey line cuts across the
   amber. The chips themselves stay on the surface colour, which reads as raised against it. */
.message-card--comment .attachments {
  border-top-color: var(--color-comment-border);
}

/* The audience, beside the author rather than in the trailing slot the timestamp owns. */
.message-card__audience {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  color: var(--color-warning);
  font-size: var(--text-sm);
  font-weight: 600;
}

.message-card__audience-icon {
  --icon-size: 13px;
  flex: none;
}

.message-card__meta {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-bottom: var(--space-3);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--color-border);
}

.message-card__from {
  font-weight: 600;
}

.message-card__email {
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.message-card__automatic {
  font-size: 11px;
  padding: 0 var(--space-2);
}

.message-card__time {
  margin-left: auto;
  color: var(--color-muted);
  font-size: 12px;
}

/* Message cards wrap From and the envelope on separate rows. Comment cards have no heading,
   so they keep author / audience / time on one wrapping line. */
.message-card__meta:has(.message-card__heading) {
  flex-direction: column;
  align-items: stretch;
}

.message-card__heading {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  flex-wrap: wrap;
  min-width: 0;
}

/* dt/dd lose their UA margin; labels stay a fixed gutter so To / Cc / Bcc line up. */
.message-card__recipients {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: var(--space-1) 0 0;
  min-width: 0;
}

.message-card__recipient {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  min-width: 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.message-card__recipient-label {
  flex: none;
  width: 3.25ch;
  margin: 0;
  font-weight: 500;
}

.message-card__recipient-addresses {
  flex: 1;
  margin: 0;
  min-width: 0;
  overflow-wrap: break-word;
}

.message-card__body {
  line-height: 1.55;
  word-break: break-word;
  overflow-x: auto;
  min-width: 0;
  max-width: 100%;
}

/* Outbound cards render the same HTML the composer just sent. Inbound mail is
   paragraph-shaped on purpose and keeps the looser rhythm below; replies use
   soft returns, so matching the composer's 1.4 and zeroing `p` margins is what
   stops greeting / body / sign-off reading as blank-line gaps. */
.message-card--outbound .message-card__body {
  line-height: 1.4;
}

.message-card--outbound .message-card__body p {
  margin: 0;
}

.message-card__body img {
  max-width: 100%;
  height: auto;
}

/* Legacy mail is table-shaped. word-break on the body plus max-width on each cell
   lets auto-layout assign a ~1ch column. Size the table to its content and scroll
   it inside the card instead of wrapping tokens into a needle. */
.message-card__body table,
.quoted-body__content table {
  width: max-content;
  max-width: none;
}

.message-card__body td,
.message-card__body th,
.quoted-body__content td,
.quoted-body__content th {
  word-break: normal;
  overflow-wrap: normal;
}

.message-card__body a {
  color: var(--color-accent-text);
  text-decoration: underline;
}

/* Agents can now send numbered and bulleted replies, and those come back through ingest into
   this same body. Left to the browser they indent 40px and space themselves at 1em, which
   reads as unstyled beside the card's own rhythm. Client mail carrying lists gets the same
   treatment, which it never had before. */
.message-card__body ul,
.message-card__body ol {
  margin: var(--space-2) 0;
  padding-left: var(--space-5);
}

.message-card__body li {
  margin: 0;
}

.message-card__body p {
  margin: 0 0 var(--space-2);
}

.message-card__body p:last-child {
  margin-bottom: 0;
}

/* A Slack client's snippets and inline code (SlackMrkdwnRenderer), and client mail's, which could
   always carry both and was left to the browser's default — a 13px serif mono on nothing, which
   reads as a rendering fault rather than as code. Tinted rather than outlined: a snippet sits in the
   reading flow and a border would cut the card into sections it does not have.

   pre-wrap, never the `pre` default: a code block is the one thing in a message with lines longer
   than the card, and letting it set its own width would push the whole thread pane sideways. The
   overflow is the escape hatch for a single unbreakable token — a minified URL, a hash — that has no
   space to wrap at. */
.message-card__body code {
  font-family: var(--font-mono);
  font-size: 0.9em;
  background: var(--color-surface-sunken);
  border-radius: 4px;
  padding: 0.1em 0.3em;
}

.message-card__body pre {
  margin: var(--space-2) 0;
  padding: var(--space-2) var(--space-3);
  background: var(--color-surface-sunken);
  border-radius: var(--radius-sm);
  overflow-x: auto;
  white-space: pre-wrap;
  word-break: break-word;
}

/* The padding and tint belong to the block, so the code inside it must not draw a second one. */
.message-card__body pre code {
  background: none;
  border-radius: 0;
  padding: 0;
}

.comment-mention {
  font-weight: 600;
  color: var(--color-warning);
  background: color-mix(in srgb, var(--color-warning) 14%, transparent);
  border-radius: 4px;
  padding: 0 0.2em;
}

/* ---------- Comment: edited marker ---------- */
/* Beside the timestamp and styled like it, because it qualifies it: these are two halves of when
   this note came to say what it says. Focusable, so the instant in its title is reachable without a
   mouse — the same reason .message-card__time carries a tabindex. */
.message-card__edited {
  color: var(--color-muted);
  font-size: 12px;
  font-style: italic;
  cursor: help;
}

/* ---------- Comment: actions ---------- */
/* Floated onto the card's top edge rather than placed in the meta row, because the row has no room
   for it: author, audience and timestamp already fill the reading column, so three more buttons in
   flow wrap to a second line and cost every note a blank row whether it is hovered or not.

   Out of flow, it also cannot reflow anything — so hiding it by opacity rather than display costs
   nothing, and the card neither grows nor shifts when the pointer crosses it. Ten pixels above the
   border and no further: enough to clear the timestamp beneath it, little enough that a card resting
   against the top of the scroller does not lose it.

   It stays out of the way until the card is hovered or something in the cluster takes focus, which
   is what keeps twenty notes from showing sixty buttons at rest. */
.comment-actions {
  position: absolute;
  top: -10px;
  right: var(--space-5);
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--color-comment-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  box-shadow: var(--shadow-sm);
  opacity: 0;
  transition: opacity 0.12s ease;
}

.message-card--comment:hover .comment-actions,
.comment-actions:focus-within {
  opacity: 1;
}

/* Always visible to a reader who has said they cannot use hover — a touch or keyboard-only user
   would otherwise have to focus a button they cannot see to find out it is there. */
@media (hover: none) {
  .comment-actions {
    opacity: 1;
  }
}
.comment-actions__form {
  display: inline-flex;
  margin: 0;
}

.comment-actions__button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-muted);
  cursor: pointer;
}

/* The band the comment composer's own tools hover to, so a note's controls and the box that wrote
   it belong to one surface rather than sitting a blue hover apart. */
.comment-actions__button:hover {
  background: var(--color-comment-band-strong);
  color: var(--color-warning);
}

.comment-actions__button[aria-expanded=true] {
  background: var(--color-comment-band-strong);
  color: var(--color-warning);
}

.comment-actions__button--danger:hover {
  background: var(--color-danger-soft);
  color: var(--color-danger);
}

.comment-actions__button:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: -2px;
}

.comment-actions__icon {
  --icon-size: 15px;
}

/* ---------- Message: comment-on-this ---------- */
/* The same floating cluster a note uses, on the mail it can be about. Amber would
   paint a comment-coloured control onto a client's message, so this one stays on
   the card's own surface and hovers to the accent. */
.message-actions {
  position: absolute;
  top: -10px;
  right: var(--space-5);
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  box-shadow: var(--shadow-sm);
  opacity: 0;
  transition: opacity 0.12s ease;
}

.message-card:hover .message-actions,
.message-actions:focus-within {
  opacity: 1;
}

@media (hover: none) {
  .message-actions {
    opacity: 1;
  }
}
.message-actions__button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-muted);
  cursor: pointer;
}

.message-actions__button:hover {
  background: var(--color-accent-soft);
  color: var(--color-accent-text);
}

.message-actions__button:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: -2px;
}

.message-actions__icon {
  --icon-size: 15px;
}

/* ---------- Message: Reply on latest ---------- */
/* A labeled control on the latest card, not the hover cluster. Front-style: writing
   to the client is deliberate, and the composer it opens is the next card in the thread. */
.message-reply {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.message-reply[hidden] {
  display: none;
}

.message-reply__button {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

.message-reply__button--secondary {
  background: var(--color-surface);
  color: var(--color-text);
  border-color: var(--color-border-strong);
}

.message-reply__button--secondary:hover {
  background: var(--color-surface-sunken);
}

.message-reply__icon {
  --icon-size: 14px;
}

/* ---------- Comment: message anchor ---------- */
/* A compact excerpt, not a quoted reply. The left rule is what reads as "about
   that mail" without repeating the whole body underneath the note. */
.comment-anchor {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0 0 var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-left: 3px solid var(--color-comment-border);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  background: color-mix(in srgb, var(--color-warning) 8%, transparent);
  color: inherit;
  text-decoration: none;
}

.comment-anchor:hover {
  background: color-mix(in srgb, var(--color-warning) 14%, transparent);
}

.comment-anchor:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 2px;
}

.comment-anchor__label {
  font-size: 12px;
  font-weight: 600;
  color: var(--color-warning);
}

.comment-anchor__excerpt {
  margin: 0;
  quotes: none;
  color: var(--color-muted);
  font-size: var(--text-sm);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.message-card--deleted .comment-anchor {
  margin-top: var(--space-3);
  margin-bottom: 0;
}

/* ---------- Comment: reactions ---------- */
/* Under the body and any files, because a reaction is an answer TO the note rather than part of it.
   No rule above it, unlike .attachments: the chips are raised objects and already read as their own
   band. */
.comment-reactions {
  margin-top: var(--space-3);
}

.comment-reactions[hidden] {
  display: none;
}

.comment-reactions__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  list-style: none;
  margin: 0;
  padding: 0;
}

.comment-reactions__form {
  display: inline-flex;
  margin: 0;
}

.comment-reactions__chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 2px var(--space-2);
  border: 1px solid var(--color-comment-border);
  border-radius: 999px;
  background: var(--color-surface);
  color: var(--color-muted);
  font-size: 12px;
  line-height: 1.5;
  cursor: pointer;
}

.comment-reactions__chip:hover {
  border-color: var(--color-warning);
}

.comment-reactions__chip:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 1px;
}

/* The viewer's own reaction, filled rather than merely outlined: scanning a note's chips, the one
   question a reader asks first is which of these is already theirs — and the answer must not rest on
   a border colour alone, so the count goes bold with it. */
.comment-reactions__chip--mine {
  background: var(--color-comment-band-strong);
  border-color: var(--color-warning);
  color: var(--color-warning);
}

.comment-reactions__chip--mine .comment-reactions__count {
  font-weight: 700;
}

/* A glyph is not body text and inherits the chip's 12px badly — several of these render as a flat
   smudge at that size. */
.comment-reactions__glyph {
  font-size: 14px;
  line-height: 1;
}

.comment-reactions__count {
  font-variant-numeric: tabular-nums;
}

/* ---------- Comment: tombstone ---------- */
/* All that is left of a deleted note. Solid rather than dashed and drained of the amber: the card
   still holds the note's place in the timeline, but there is nothing here to read as a note. */
.message-card--deleted {
  background: var(--color-surface-sunken);
  border-color: var(--color-border);
  border-style: solid;
  padding: var(--space-3) var(--space-5);
}

.message-card__tombstone {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.message-card__tombstone-icon {
  --icon-size: 14px;
  flex: none;
}

/* ---------- Comment: editing in place ---------- */
/* The card while its own note is being corrected. Drained of the amber for as long as the form is
   open, so the composing surface inside it — which paints itself amber off data-mode — is the thing
   the eye lands on rather than one amber box inside another. */
.message-card--comment.is-editing {
  background: var(--color-surface);
  border-style: solid;
}

.comment-edit {
  margin-top: var(--space-3);
}

/* What happens to the note's files, said in the same shape as the .attachments label the card shows
   when it is not being edited — a reader should recognize the line rather than parse it. */
.comment-edit__files {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  margin: 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.comment-edit__files-icon {
  --icon-size: 14px;
  flex: none;
}

/* On the sanitizer's allowlist and the tag every mail client wraps a quote in, but styled nowhere
   until now — so it shipped the browser's 40px default indent, which stair-steps a reply-to-a-reply
   off the side of the card. Same left rule as .quoted-body__content, so a nested quote reads as one
   more level of the same thing rather than a different treatment. */
.message-card__body blockquote {
  margin: var(--space-2) 0;
  padding-left: var(--space-3);
  border-left: 2px solid var(--color-border);
  color: var(--color-muted);
}

/* ---------- Quoted reply history ---------- */
/* Inline in the reading flow, not a bordered meta row like .blocked-images below: this is body
   content the agent may want, and on a two-word reply a full-width banner would be taller than the
   message it is decluttering. */
.quoted-body {
  margin-top: var(--space-3);
}

.quoted-body__caret {
  --icon-size: 14px;
  flex: none;
}

.quoted-body__toggle,
.message-body-clamp__toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

.quoted-body.is-open .quoted-body__caret,
.message-body-clamp.is-open .quoted-body__caret {
  transform: rotate(180deg);
}

/* The label swaps in CSS off the class `disclosure` already sets, so the shared controller three
   settings screens depend on needs no per-caller knowledge of what its button says. The clamp
   reuses the same labels under its own is-open class — it cannot share `disclosure`, which
   would hide the preview. */
.quoted-body__label--hide {
  display: none;
}

.quoted-body.is-open .quoted-body__label--show,
.message-body-clamp.is-open .quoted-body__label--show {
  display: none;
}

.quoted-body.is-open .quoted-body__label--hide,
.message-body-clamp.is-open .quoted-body__label--hide {
  display: inline;
}

.quoted-body__loading {
  margin: 0;
  font-size: var(--text-sm);
}

/* The quote's lazy frame needs a box: `.thread__messages turbo-frame` is display:contents, and a
   frame with no box never intersects, so Turbo's lazy loader would never fetch it. */
.thread__messages .quoted-body__content > turbo-frame {
  display: block;
}

.quoted-body__content {
  margin-top: var(--space-3);
  padding-left: var(--space-3);
  border-left: 2px solid var(--color-border);
  color: var(--color-muted);
  overflow-x: auto;
  min-width: 0;
  max-width: 100%;
}

/* ---------- Tall inbound body clamp ---------- */
/* Preview stays on screen; only the overflow is clipped. The toggle is hidden until
   message-clamp measures an actual overflow, so a two-word reply gets no chrome. */
.message-body-clamp {
  min-width: 0;
}

.message-body-clamp__content {
  min-width: 0;
  max-height: min(32rem, 50vh);
  overflow-x: auto;
  overflow-y: hidden;
}

.message-body-clamp.is-open .message-body-clamp__content {
  max-height: none;
  overflow: visible;
}

.message-body-clamp__toggle {
  margin-top: var(--space-3);
}

.message-body-clamp__toggle[hidden] {
  display: none;
}

/* ---------- Blocked remote images ---------- */
/* Deliberately mirrors the .attachments strip below — same top border, same spacing, same
   muted 14px icon + label — so a card carrying both reads as one stack of meta rows rather
   than two competing banners. */
.blocked-images {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  font-size: var(--text-sm);
  color: var(--color-muted);
}

/* A card with quoted text renders the strip hidden until its quote loads and turns out to hold a
   blocked image (blocked-images#rescan); display:flex above would otherwise win over [hidden]. */
.blocked-images[hidden] {
  display: none;
}

.blocked-images__icon {
  --icon-size: 14px;
  flex: none;
}

/* Trailing edge, the way .message-card__time sits. Survives wrapping: on a narrow pane the
   button drops to its own line instead of crushing the label. */
.blocked-images__button {
  margin-left: auto;
}

/* Targeted by attribute rather than class, because the sanitizer allowlist has no `class` —
   a class here could never survive on sender markup. The image keeps its alt text, which is
   the only description of it we have, and the dashed box gives that text somewhere to sit.
   data-image-failed is set by the Stimulus controller when a revealed image fails to load. */
.message-card__body img[data-blocked-src],
.message-card__body img[data-image-failed] {
  min-width: 88px;
  min-height: 64px;
  padding: var(--space-2);
  border: 1px dashed var(--color-border-strong);
  border-radius: var(--radius-sm);
  background: var(--color-surface-sunken);
  color: var(--color-muted);
  font-size: var(--text-sm);
}

/* ---------- Attachments ---------- */
.attachments {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
}

.attachments__label {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  margin: 0 0 var(--space-2);
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.attachments__icon {
  --icon-size: 14px;
}

.attachments__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Fixed width, not content-sized, so a row of chips with wildly different filename
   lengths still lines up. */
.attachment {
  width: 140px;
}

.attachment__link {
  display: block;
  overflow: hidden;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: inherit;
  text-decoration: none;
}

a.attachment__link:hover {
  border-color: var(--color-accent);
}

a.attachment__link:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 2px;
}

/* Nothing to click on a file that is still downloading or could not be stored. */
.attachment__link--inert {
  color: var(--color-muted);
  cursor: default;
}

.attachment__preview {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 1;
  background: var(--color-surface-sunken);
  border-bottom: 1px solid var(--color-border);
}

/* cover, so a very wide or very tall image fills the square without letterboxing. */
.attachment__thumb {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.attachment__icon {
  --icon-size: 28px;
  color: var(--color-muted);
}

.attachment__caption {
  display: block;
  padding: var(--space-2);
}

.attachment__name {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--text-sm);
}

.attachment__meta {
  display: block;
  color: var(--color-muted);
  font-size: 12px;
}

/* ---------- Composer ---------- */
.thread__composers {
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}

.composer {
  flex-shrink: 0;
  border-top: 1px solid var(--color-border);
  background: var(--color-surface);
  padding: var(--space-3) var(--space-4);
}

.compose {
  display: flex;
  flex-direction: column;
  min-height: 0;
  height: 100%;
  overflow: hidden;
  background: var(--color-surface);
}

.compose__header {
  flex-shrink: 0;
  padding: var(--space-4) var(--space-5) 0;
}

.compose__heading {
  margin: 0;
  font-size: var(--text-xl);
}

.composer--compose {
  flex: 1;
  min-height: 0;
  border-top: 0;
  display: flex;
  flex-direction: column;
}

.composer--compose .composer__box {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.composer--compose .composer__editor {
  flex: 1;
}

#reply_composer[hidden] {
  display: none;
}

/* Draft card in the timeline: same tint and radius as an outbound message, without the
   pinned-foot border and without a second box-inside-the-panel. */
.composer--in-flow {
  border: 1px solid var(--color-accent-edge);
  border-radius: var(--radius-lg);
  background: var(--color-accent-soft);
  padding: var(--space-4) var(--space-5);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}

.composer--in-flow .composer__box {
  border: 0;
  border-radius: 0;
  background: transparent;
}

.composer--in-flow .composer__box:focus-within {
  border-color: transparent;
  box-shadow: none;
}

.composer--in-flow:focus-within {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 2px;
}

.composer-typers {
  margin: 0;
  padding: var(--space-2) var(--space-4) 0;
  color: var(--color-muted);
  font-size: 12px;
  line-height: 1.4;
}

.composer-typers[hidden] {
  display: none;
}

.composer-typers__person {
  display: none;
}

.composer-typers__name {
  color: var(--color-text);
  font-weight: 600;
}

.composer__form {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* From / To / Cc / Bcc. Always visible in email Reply so the agent can see who will receive
   the mail before they expand the box. Slack and the comment bar never render the markup. */
.composer__recipients {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

/* display:flex above outranks the UA [hidden] rule — the same trap .composer__tool[hidden]
   already documents. Comment mode sets both the form's data-mode and the hidden attribute;
   either one has to be enough on its own. */
.composer__form[data-mode=comment] .composer__recipients,
.composer__recipients[hidden] {
  display: none;
}

.composer__recipient {
  display: grid;
  grid-template-columns: 4.5rem minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-2);
}

/* Same [hidden] trap as the strip: display:grid would keep the collapsed Bcc row on screen. */
.composer__recipient[hidden],
.composer__bcc-toggle[hidden] {
  display: none;
}

.composer__recipient-label {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-muted);
}

.composer__recipient-input {
  width: 100%;
  min-width: 0;
  padding: var(--space-1) var(--space-2);
  font-size: var(--text-sm);
}

/* Change sender replaces this wrap; it must fill the recipient grid's 1fr column. */
#composer_to_wrap {
  min-width: 0;
}

/* To / Cc / Bcc chips sit in the same box the plain input used. The inner field is
   borderless so the container's focus-within ring is the only chrome — the same
   ring .field__input:focus already draws. */
.recipient-chips {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1);
  cursor: text;
}

.recipient-chips:focus-within {
  border-color: var(--color-accent);
  outline: none;
  box-shadow: 0 0 0 3px var(--color-accent-soft);
}

.recipient-chips.is-invalid:focus-within {
  border-color: var(--color-danger);
  box-shadow: 0 0 0 3px var(--color-danger-soft);
}

.recipient-chips__input {
  appearance: none;
  flex: 1 1 8ch;
  min-width: 8ch;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: var(--text-sm);
}

.recipient-chips__input:focus {
  outline: none;
  box-shadow: none;
}

.recipient-chips:has(.recipient-chips__chip) .recipient-chips__input::placeholder {
  color: transparent;
}

.recipient-chips__chip {
  max-width: 100%;
  gap: var(--space-1);
}

.recipient-chips__chip [data-recipient-chips-address] {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 24ch;
}

.recipient-chips__chip.is-selected {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 1px;
}

.recipient-chips__chip.badge--danger.is-selected {
  outline-color: var(--color-danger);
}

/* Fixed rather than absolute: the composer sits on the pane's bottom edge, and
   an in-flow list would shear at the thread pane. Same recipe as .message-menu. */
.recipient-chips__suggestions.dropdown__menu {
  position: fixed;
  top: auto;
  right: auto;
  z-index: 60;
  min-width: 220px;
  max-width: min(360px, 100vw - 16px);
  max-height: 220px;
  overflow-y: auto;
}

.recipient-chips__suggestions[hidden] {
  display: none;
}

.recipient-chips__suggestion-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.recipient-chips__suggestion {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  box-sizing: border-box;
  white-space: normal;
}

.recipient-chips__suggestion-avatar {
  flex: none;
}

.recipient-chips__suggestion-avatar[hidden] {
  display: none;
}

.recipient-chips__suggestion-copy {
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.recipient-chips__suggestion-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
}

.recipient-chips__suggestion-email {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.recipient-chips__suggestion-email[hidden] {
  display: none;
}

.composer__from-static {
  margin: 0;
  min-width: 0;
  font-size: var(--text-sm);
  color: var(--color-text);
  overflow-wrap: anywhere;
}

.composer__bcc-toggle {
  justify-self: end;
  padding: var(--space-1) var(--space-2);
}

/* ---------- Comment mode ---------- */
/* The surface recolors, not just the switch: the writer's eye is on the box they are typing in, and
   this is the one mistake here that cannot be taken back. Same amber as the card the comment
   becomes, so the box and its result read as one thing. */
.composer__form[data-mode=comment] .composer__box {
  border-color: var(--color-comment-border);
  background: var(--color-comment-soft);
}

.composer__form[data-mode=comment] .composer__box:focus-within {
  border-color: var(--color-warning);
  box-shadow: 0 0 0 3px var(--color-comment-band);
}

.composer__form[data-mode=comment] .composer__footer {
  border-top-color: var(--color-comment-border);
}

/* Re-asserted at this specificity, or the accent-soft hover and pressed states from the base tool
   would paint blue on the amber box. */
.composer__form[data-mode=comment] .composer__tool:hover {
  background: var(--color-comment-band-strong);
}

.composer__form[data-mode=comment] .composer__tool[aria-pressed=true],
.composer__form[data-mode=comment] .composer__tool[aria-expanded=true] {
  color: var(--color-warning);
  background: var(--color-comment-band-strong);
}

.composer__form[data-mode=comment] .composer__files {
  border-top-color: var(--color-comment-border);
}

/* The emoji palette, likewise: it is a band of the composing surface, and its own sunken grey and
   accent-soft hover would read as a strip of the reply composer left behind on the amber one. */
.composer__form[data-mode=comment] .composer__emoji {
  background: var(--color-comment-band);
  border-top-color: var(--color-comment-border);
}

.composer__form[data-mode=comment] .composer__emoji-option:hover {
  background: var(--color-comment-band-strong);
}

.composer__form[data-mode=comment] .composer__attach:hover {
  background: var(--color-comment-band-strong);
  color: var(--color-warning);
}

.composer__error {
  margin: 0;
  font-size: 12px;
  color: var(--color-danger);
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  /* A parked mailbox's address can be long; wrap it rather than pushing the pane wide. */
  word-break: break-word;
}

/* Trailing edge, the way .blocked-images__button sits — and it drops to its own line on a
   narrow pane instead of crushing the sentence. */
.composer__error-action {
  margin-left: auto;
}

/* Deliberately mirrors .blocked-images: same flex row, same muted 14px icon + --text-sm
   label, so the view-only strip reads as the same family of quiet explanatory rows rather
   than a new kind of banner. */
.composer__notice {
  margin: 0;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  font-size: var(--text-sm);
  color: var(--color-muted);
}

.composer__notice-icon {
  --icon-size: 14px;
  flex: none;
}

/* The From row and the notice under it, kept together so a move can swap them as one (FRO-150). The
   notice sits under the From value rather than the label, its icon and sentence one unit that wraps
   as one: the indent is the recipient grid's label column plus its gap. */
.composer__from {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.composer__from .composer__notice {
  flex-wrap: nowrap;
  align-items: flex-start;
  padding-left: calc(4.5rem + var(--space-2));
}

.composer__from .composer__notice-icon {
  margin-top: 2px;
}

.composer__notice-text {
  min-width: 0;
}

/* Only announced while a send is in flight; empty the rest of the time, so it takes up no
   room in the resting composer. */
/* The spinner in the Send button is the visual "sending" signal — this line carries the same
   state for screen readers only. The spinner is decorative (aria-hidden), so deleting this
   text rather than hiding it would leave assistive tech with nothing to announce. */
.composer__status {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Sits under the box on the leading edge, aligned with the text the agent is typing.
   Quiet by design: a hint the eye can skip once it has been learned. */
.composer__hint {
  margin: 0;
  font-size: 12px;
  color: var(--color-muted);
}

/* ---------- The composer box ---------- */
/* One bordered container holds the editor, the picked files and a built-in footer (quiet
   format + Send). The error, hint and status lines stay outside it — they are messages
   *about* the send rather than part of the surface the agent composes on. */
.composer__box {
  /* The containing block for the drag sheet below, which covers the box rather than the pane. */
  position: relative;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  overflow: hidden;
}

/* The focus ring moves from the field to the container, because the editor is no longer the
   only thing inside it. Same treatment as .field__input:focus, so it reads as the same family. */
.composer__box:focus-within {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px var(--color-accent-soft);
}

/* A file dragged over the box answers on the box, in the accent the focus ring already uses:
   this is the surface that will take it. One class carries both the ring and the sheet, so the
   border and the words over it cannot end up disagreeing. */
.composer__box.is-drop-target {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px var(--color-accent-soft);
}

/* The dashed outline is what carries this, not the fill: the reply box sits on an accent-tinted
   card of its own, where a tinted sheet alone would read as the words having vanished rather than
   as a target. Inset a hair so the outline is a panel inside the box on every surface. */
.composer__drop-sheet {
  position: absolute;
  inset: var(--space-1);
  display: none;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  /* Transparent to the pointer: a sheet that could be entered would read as the pointer leaving
     the box the moment it appeared, and the drop would never arrive. */
  pointer-events: none;
  border: 1px dashed var(--color-accent);
  border-radius: var(--radius-sm);
  background: var(--color-accent-soft);
  color: var(--color-accent-text);
  font-size: var(--text-sm);
  font-weight: 600;
}

.composer__box.is-drop-target .composer__drop-sheet {
  display: flex;
}

.composer__drop-sheet-icon {
  --icon-size: 16px;
}

/* Format and actions share one footer so they read as the bottom of the box, not a second card. */
.composer__footer {
  border-top: 1px solid var(--color-hairline);
}

.composer__toolbar,
.composer__insert {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-2);
}

/* Format is quieter than the insert row: muted glyphs, no fill, no band. */
.composer__toolbar {
  color: var(--color-muted);
}

.composer__toolbar .composer__tool {
  color: var(--color-muted);
}

.composer__insert {
  flex: 1;
  min-width: 0;
  padding-left: 0;
}

/* These are <button>s, so the UA background, border and font have to be reset explicitly —
   borrowing an icon-button look without stripping the native chrome leaves grey buttonface
   showing through. */
.composer__tool {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-text);
  font: inherit;
  cursor: pointer;
}

/* Re-asserted, because the display above outranks the UA's own [hidden] rule — without this the
   templates button would still lay out (and still be tabbable) on the surfaces that hide it. */
.composer__tool[hidden] {
  display: none;
}

.composer__tool:hover {
  background: var(--color-accent-soft);
}

/* The pressed look mirrors .btn--ghost.is-on and .dropdown__option[aria-checked] — the app
   already says "on" this way. Both colour and background carry it, never colour alone. */
.composer__tool[aria-pressed=true],
.composer__tool[aria-expanded=true] {
  color: var(--color-accent-text);
  background: var(--color-accent-soft);
}

.composer__tool:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* The global :focus-visible ring sits 2px outside the element, which on a 28px button in a
   28px-tall band collides with the container border. Drawn inside instead. */
.composer__tool:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: -2px;
}

.composer__tool-icon {
  --icon-size: 18px;
}

.composer__tool-divider {
  width: 1px;
  height: 20px;
  margin: 0 var(--space-1);
  background: var(--color-border);
  flex: none;
}

/* Pushes Expand to the trailing edge of the insert row, away from emoji / templates. */
.composer__toolbar-spacer {
  flex: 1;
  min-width: var(--space-2);
}

.composer__expand-icon--in {
  display: none;
}

.composer__form.is-tall .composer__expand-icon--out {
  display: none;
}

.composer__form.is-tall .composer__expand-icon--in {
  display: inline-flex;
}

/* In the flow above the footer, not floating: .app__thread clips its overflow and this composer
   sits on its bottom edge, so an absolutely-positioned popover would be cut off exactly where
   it opens. */
.composer__link-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding: var(--space-2);
  background: var(--color-surface-sunken);
  border-top: 1px solid var(--color-border);
}

.composer__link-row[hidden] {
  display: none;
}

.composer__link-icon {
  --icon-size: 14px;
  flex: none;
  color: var(--color-muted);
}

.composer__link-input {
  flex: 1;
  min-width: 140px;
  width: auto;
  padding: var(--space-1) var(--space-2);
  font-size: var(--text-sm);
}

.composer__link-input--invalid {
  border-color: var(--color-danger);
}

.composer__link-error {
  flex-basis: 100%;
  margin: 0;
  font-size: 12px;
  color: var(--color-danger);
}

.composer__link-error[hidden] {
  display: none;
}

/* Visually hidden but present: the placeholder is not an accessible name. */
.composer__link-label {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.composer__editor {
  min-height: 44px;
  max-height: 40vh;
  overflow-y: auto;
  padding: var(--space-3);
  line-height: 1.5;
  word-break: break-word;
  /* The box owns the focus ring; a second one on the editor would draw a rectangle inside a
     rectangle. */
  outline: none;
}

.composer__editor:empty::before {
  content: attr(data-placeholder);
  color: var(--color-muted);
  pointer-events: none;
}

/* The agent's own formatting, shown the way the recipient will see it. Lists especially: the
   browser default indents them 40px, which reads as broken next to 12px of padding. */
.composer__editor p {
  margin: 0 0 var(--space-2);
}

.composer__editor p:last-child {
  margin-bottom: 0;
}

/* Reply, compose, and the template editor share the outbound rhythm. Comment
   keeps the paragraph gap above so a note to the team does not pick up the mail
   spacing. */
#reply_composer .composer__editor,
#compose_composer .composer__editor,
.reply-template-editor .composer__editor {
  line-height: 1.4;
}

#reply_composer .composer__editor p,
#compose_composer .composer__editor p,
.reply-template-editor .composer__editor p {
  margin: 0;
}

.composer__editor ul,
.composer__editor ol {
  margin: var(--space-2) 0;
  padding-left: var(--space-5);
}

.composer__editor a {
  color: var(--color-accent-text);
  text-decoration: underline;
}

/* ---------- Emoji palette ---------- */
/* In the flow above the footer for the same reason as the link row and the mention list: the
   thread pane clips its overflow and the composer sits on its bottom edge, so a floating panel
   would be cut off exactly where it opens. Capped and scrollable so a narrow pane — where the grid
   auto-fills to fewer columns and more rows — cannot push the editor off the screen. */
.composer__emoji {
  padding: var(--space-2);
  background: var(--color-surface-sunken);
  border-top: 1px solid var(--color-border);
  max-height: 220px;
  overflow-y: auto;
}

.composer__emoji[hidden] {
  display: none;
}

/* auto-fill rather than a fixed column count: the palette is the last thing that should force the
   composer wider than its pane, and the arrow keys read the row length back off this layout. */
.composer__emoji-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(30px, 1fr));
  gap: 2px;
}

/* A <button>, so the UA background, border and font are reset explicitly — the same reset
   .composer__tool needs, for the same reason. */
.composer__emoji-option {
  appearance: none;
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 1;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  /* The glyph IS the control, so it is sized like body text rather than like the 18px icons in the
     toolbar above it; line-height 1 is what keeps the cell square. */
  font-family: inherit;
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}

.composer__emoji-option:hover {
  background: var(--color-accent-soft);
}

/* Drawn inside the element, as on the toolbar buttons: the global ring sits 2px outside, which on
   a 30px cell in a 2px grid gap would overlap its neighbours. */
.composer__emoji-option:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: -2px;
}

/* Above the footer, in the composing flow: the composer is pinned to the bottom of the thread
   pane, so a list below the box would open off the screen. */
.composer__mentions {
  border-top: 1px solid var(--color-comment-border);
  max-height: 220px;
  overflow-y: auto;
}

.composer:not(.composer--comment) .composer__mentions {
  border-top-color: var(--color-border);
}

.composer__mentions[hidden] {
  display: none;
}

.composer__mentions-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-1);
}

.composer__mentions-empty {
  margin: 0;
  padding: var(--space-2) var(--space-3);
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.composer__mentions-empty[hidden] {
  display: none;
}

.composer__mention-option {
  appearance: none;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  border: 0;
  background: transparent;
  text-align: left;
  padding: var(--space-2);
  border-radius: var(--radius-sm);
  font: inherit;
  color: var(--color-text);
  cursor: pointer;
}

.composer__mention-option:hover,
.composer__mention-option[aria-selected=true] {
  background: var(--color-comment-band);
}

.composer:not(.composer--comment) .composer__mention-option:hover,
.composer:not(.composer--comment) .composer__mention-option[aria-selected=true] {
  background: var(--color-accent-soft);
}

.composer__mention-avatar {
  flex: none;
}

.composer__mention-avatar[hidden] {
  display: none;
}

.composer__mention-copy {
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.composer__mention-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
}

.composer__mention-email {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.composer__mention-email[hidden] {
  display: none;
}

/* ---------- Reply templates picker ---------- */
/* Above the footer and in the flow, for the reason the three panels before it are: the thread pane
   clips its overflow and the composer sits on its bottom edge. Capped and scrollable so a long list
   cannot push the editor off the screen — the emoji palette's cap, for the same reason. */
.composer__templates {
  background: var(--color-surface-sunken);
  border-top: 1px solid var(--color-border);
  max-height: 220px;
  overflow-y: auto;
}

.composer__templates[hidden] {
  display: none;
}

.composer__templates-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-1);
}

.composer__templates-empty {
  margin: 0;
  padding: var(--space-2) var(--space-3);
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.composer__templates-empty[hidden] {
  display: none;
}

/* A <button>, so the UA background, border, font and alignment are reset explicitly — the same
   reset .composer__mention-option needs, for the same reason. */
.composer__template-option {
  appearance: none;
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: 100%;
  border: 0;
  background: transparent;
  text-align: left;
  padding: var(--space-2);
  border-radius: var(--radius-sm);
  font: inherit;
  color: var(--color-text);
  cursor: pointer;
}

.composer__template-option:hover,
.composer__template-option[aria-selected=true] {
  background: var(--color-accent-soft);
}

/* The shortcut rides on the name's baseline rather than under it: it is a second way to say the
   same name, not a second fact about the template. */
.composer__template-head {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  min-width: 0;
}

.composer__template-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
}

.composer__template-shortcut {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--color-accent-text);
}

.composer__template-shortcut[hidden] {
  display: none;
}

/* Pushed to the trailing edge, where a badge on a row belongs, so the name and its shortcut still read
   as one thing on the leading edge. Not the shared .badge: this sits on the baseline of a 600-weight
   name inside a panel two hundred pixels tall, and the pill's padding would push the row taller. */
.composer__template-audience {
  flex: none;
  margin-left: auto;
  color: var(--color-muted);
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.composer__template-audience[hidden] {
  display: none;
}

/* One line, clipped: the preview is a hint at which template this is, not the template. A row that
   wrapped to three lines would put four options past the panel's cap. */
.composer__template-preview {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

/* Dimmed, but still focusable and still hovering — the reason it cannot be picked is written inside
   it, and an explanation nobody can reach is not an explanation. */
.composer__template-option.is-unresolvable {
  cursor: not-allowed;
  opacity: 0.7;
}

.composer__template-option.is-unresolvable .composer__template-name {
  font-weight: 500;
}

/* What picking this one also DOES. Accented rather than muted like the preview above it: the preview
   is a hint at which template this is, while this line is the one thing on the row that describes an
   act. Wraps, unlike every other line in the option — a clipped "…and archi" is a warning that has
   been truncated into a puzzle. */
.composer__template-actions {
  color: var(--color-accent-text);
  font-size: var(--text-sm);
}

.composer__template-actions[hidden] {
  display: none;
}

.composer__template-reason {
  color: var(--color-warning);
  font-size: var(--text-sm);
}

.composer__template-reason[hidden] {
  display: none;
}

/* ---------- Knowledge picker ---------- */
/* Same in-flow panel as reply templates, with a search field the slash trigger
   cannot own — `/` already inserts templates. The list scrolls under a sticky
   search so typing a query does not lose the field. */
.composer__knowledge {
  background: var(--color-surface-sunken);
  border-top: 1px solid var(--color-border);
}

.composer__knowledge[hidden] {
  display: none;
}

.composer__knowledge-search-wrap {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: var(--space-2) var(--space-2) var(--space-1);
  background: var(--color-surface-sunken);
}

.composer__knowledge-search-wrap[hidden] {
  display: none;
}

.composer__knowledge-search {
  padding: var(--space-1) var(--space-2);
  font-size: var(--text-sm);
}

.composer__knowledge-search-label {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.composer__knowledge-list {
  max-height: 180px;
  overflow-y: auto;
}

/* ---------- AI draft grounding (FRO-60) ---------- */
/* Outside .composer__box with the macro line: a note about the words in the box, not part of
   them. Quiet by default — a muted caption and one-line rows — because it is reference, and an
   empty set is a single muted sentence rather than a panel. */
.composer__grounding {
  margin: var(--space-2) 0 0;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-surface-sunken);
  font-size: var(--text-sm);
}

.composer__grounding[hidden],
.composer__form:not([data-mode=reply]) .composer__grounding {
  display: none;
}

.composer__grounding.is-empty {
  background: transparent;
  padding-block: 0;
}

.composer__grounding-label {
  margin: 0;
  color: var(--color-muted);
}

.composer__grounding-list {
  list-style: none;
  margin: var(--space-1) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.composer__grounding-list:empty {
  display: none;
}

.composer__grounding-item {
  min-width: 0;
}

.composer__grounding-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.composer__grounding-title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
  color: var(--color-text);
}

.composer__grounding-badge,
.composer__grounding-source {
  flex: none;
}

.composer__grounding-badge[hidden],
.composer__grounding-source[hidden] {
  display: none;
}

.composer__grounding-source {
  color: var(--color-accent-text);
}

/* Trailing edge, where the macro line keeps its Remove. */
.composer__grounding-actions {
  display: flex;
  gap: var(--space-1);
  margin-left: auto;
  flex: none;
}

.composer__grounding-preview {
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--color-muted);
}

/* ---------- The armed macro, above Send ---------- */
/* Outside .composer__box with the error and the hint, because like them it is a message ABOUT the
   send. Tinted rather than bordered: it sits directly under the box's own border, and a second
   outline a few pixels below the first reads as a rendering fault. */
.composer__macro {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-2) 0 0;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-accent-soft);
  color: var(--color-text);
  font-size: var(--text-sm);
}

.composer__macro[hidden] {
  display: none;
}

.composer__macro-icon {
  color: var(--color-accent-text);
  flex: none;
}

/* Pushed to the trailing edge so the sentence keeps the leading one, and the button is where the eye
   goes after reading it rather than in the middle of it. */
.composer__macro-cancel {
  margin-left: auto;
  flex: none;
}

/* Which message a note is about, above the editor. Same shape as the macro line:
   a status sentence with Remove on the trailing edge. Amber in comment mode so it
   belongs to the note surface rather than painting a grey strip on it. */
.composer__anchor {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0 var(--space-3);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--color-border);
  color: var(--color-text);
  font-size: var(--text-sm);
}

.composer__anchor[hidden],
.composer__form:not([data-mode=comment]) .composer__anchor {
  display: none;
}

.composer__form[data-mode=comment] .composer__anchor {
  border-bottom-color: var(--color-comment-border);
  color: var(--color-warning);
}

.composer__anchor-icon {
  flex: none;
  --icon-size: 14px;
}

.composer__anchor q {
  quotes: none;
}

.composer__anchor-cancel {
  margin-left: auto;
  flex: none;
}

/* ---------- Picked files ---------- */
.composer__files {
  padding: var(--space-2) var(--space-3);
  border-top: 1px solid var(--color-border);
}

.composer__files[hidden] {
  display: none;
}

/* Capped so a reply with eight files still leaves the editor and Send reachable. */
.composer__files-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 25vh;
  overflow-y: auto;
}

.composer__file {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) 0;
  font-size: var(--text-sm);
}

.composer__file-icon {
  --icon-size: 16px;
  flex: none;
  color: var(--color-muted);
}

/* Both glyphs are in the markup and CSS picks one, so the Stimulus controller only toggles a
   class on the row and never reaches into the icon. The shown glyph restates .icon's
   inline-flex — `block` here would drop the centring and sit the glyph on the text baseline. */
.composer__file-icon--image {
  display: none;
}

.composer__file--image .composer__file-icon--image {
  display: inline-flex;
}

.composer__file--image .composer__file-icon--file {
  display: none;
}

/* The flexible column: the name ellipsises, the size and the remove control never shrink. */
.composer__file-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.composer__file-size {
  color: var(--color-muted);
  font-size: 12px;
  flex: none;
}

.composer__file-status {
  color: var(--color-muted);
  font-size: 12px;
  flex: none;
}

.composer__file-progress {
  color: var(--color-muted);
  font-size: 12px;
  flex: none;
}

.composer__file.is-failed {
  color: var(--color-danger);
}

.composer__file.is-failed .composer__file-status {
  color: var(--color-danger);
}

.composer__file.is-uploading .composer__file-size {
  display: none;
}

.composer__file-remove {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-muted);
  font: inherit;
  cursor: pointer;
  flex: none;
}

.composer__file-remove:hover {
  color: var(--color-danger);
  background: var(--color-danger-soft);
}

.composer__file-remove-icon {
  --icon-size: 14px;
}

.composer__files-total {
  margin: var(--space-1) 0 0;
  font-size: 12px;
  color: var(--color-muted);
}

.composer__files-total--over {
  color: var(--color-danger);
}

/* Sits below the running total and survives it: a rejected file never joins the list, so this
   is the only record that it was dropped. */
.composer__files-rejected {
  margin: var(--space-1) 0 0;
  font-size: 12px;
  color: var(--color-danger);
  word-break: break-word;
}

.composer__files-rejected[hidden] {
  display: none;
}

/* ---------- Action row ---------- */
/* Built into .composer__footer, not a second card. Attach is a ghost tool like its neighbors
   so the footer does not grow a leftover gray slab. */
.composer__actions {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  padding: 0 var(--space-2) var(--space-1);
}

.composer__trailing {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-left: auto;
  flex: none;
}

.composer__discard:hover {
  color: var(--color-danger);
  background: var(--color-danger-soft);
}

/* Never rendered, but still focusable and announced if left in the tab order. */
.composer__file-input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.composer__send-cluster {
  display: flex;
  align-items: center;
  flex: none;
  gap: 0;
}

.composer__send-delay {
  position: relative;
}

.composer__send-delay-trigger {
  width: 24px;
  height: 32px;
}

.composer__send-delay-custom {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-2);
  min-width: 252px;
}

.composer__send-delay-custom[hidden] {
  display: none;
}

.composer__send-delay-field {
  margin-bottom: 0;
}

.composer__send-delay-confirmation {
  margin: 0;
  font-size: 12px;
  color: var(--color-muted);
}

.composer__send-delay-error {
  margin: 0;
  font-size: 12px;
  color: var(--color-danger);
}

.composer__send-delay-error[hidden] {
  display: none;
}

.composer__send-delay-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2);
}

.composer__send-delay-actions .btn--primary {
  width: auto;
}

.composer__send-delay input[type=datetime-local].field__input {
  appearance: none;
  min-width: 0;
}

.composer__send-delay input[type=datetime-local].field__input::-webkit-calendar-picker-indicator {
  cursor: pointer;
  opacity: 0.65;
}

.composer__send {
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}

/* Muted and flat until there is something to send, so an armed composer is visibly different
   from an empty one rather than differing only in the cursor. */
.composer__send:disabled {
  opacity: 1;
  background: transparent;
  color: var(--color-muted);
  cursor: not-allowed;
}

/* ---------- Resting vs. expanded ---------- */
/* At rest the box is nothing but the editor line — the same treatment as .field__input, one row
   high. The footer collapses to zero rather than display:none, because a height of zero is
   something that can be animated to and from. visibility is what takes its controls out of the
   tab order and the accessibility tree while they cannot be seen; overflow:hidden on the child
   is what lets a grid item shrink below its content. */
.composer__collapse {
  display: grid;
  grid-template-rows: 0fr;
  visibility: hidden;
  transition: grid-template-rows 0.15s ease, visibility 0.15s;
}

/* Its own element rather than the footer directly: overflow:hidden lets a grid item's content
   shrink to nothing, but the item's own padding and border survive it — and the footer is
   padded, which would leave empty box behind at rest. */
.composer__collapse-inner {
  overflow: hidden;
}

.composer__form.is-expanded .composer__collapse {
  grid-template-rows: 1fr;
  visibility: visible;
}

/* grid-template-rows is chosen over max-height because the footer wraps on a narrow pane: any
   fixed ceiling would either clip it or make the collapse snap late. */
/* The hint sits outside the box, in a flex column with a gap — collapsing it to zero height
   would leave that 8px gap behind as a ghost row, so it is simply not rendered while resting. */
.composer__form:not(.is-expanded) .composer__hint {
  display: none;
}

/* ---------- Tall composer ---------- */
/* The editor node stays where it is and is restyled. Replacing it would drop the caret, the
   markup, and any upload still in flight — the whole reason this is a class on the thread
   rather than a second composer. The header stays, so the agent still knows which thread
   they are writing on. Reply and comment yield different columns: Reply is in the scroller,
   comment is still the pinned foot. */
.thread.is-composer-tall:has(#comment_composer .is-tall) .thread__scroll {
  flex: 0 0 0;
  min-height: 0;
  overflow: hidden;
  visibility: hidden;
}

.thread.is-composer-tall:has(#comment_composer .is-tall) .thread__composers {
  flex: 1;
  min-height: 0;
}

.thread.is-composer-tall:has(#reply_composer .is-tall) .thread__scroll {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.thread.is-composer-tall:has(#reply_composer .is-tall) .thread__flow {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.thread.is-composer-tall:has(#reply_composer .is-tall) .thread__messages {
  flex: 0 0 0;
  min-height: 0;
  overflow: hidden;
  visibility: hidden;
}

.thread.is-composer-tall .composer:has(.is-tall) {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.thread.is-composer-tall .composer:has(.is-tall) .composer__form {
  flex: 1;
  min-height: 0;
}

.thread.is-composer-tall .composer:has(.is-tall) .composer__box {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.thread.is-composer-tall .composer:has(.is-tall) .composer__editor {
  flex: 1;
  max-height: none;
  min-height: 0;
}

/* The send blocks on the Gmail API for about a second. Without a visible in-flight state the
   button reads as dead and invites a second click. */
.composer__spinner {
  display: none;
  width: 16px;
  height: 16px;
  border: 2px solid color-mix(in srgb, var(--color-on-accent) 40%, transparent);
  border-top-color: var(--color-on-accent);
  border-radius: 50%;
  animation: composer-spin 0.6s linear infinite;
}

/* The button is disabled while sending, but the spinner is now the only signal that the reply
   is on its way — so it keeps the full accent background rather than the resting disabled look,
   which is transparent and would leave a white ring drawn on nothing. */
.composer__form.is-sending .composer__send:disabled {
  opacity: 1;
  background: var(--color-accent);
  color: var(--color-on-accent);
}

.composer__form.is-sending .composer__spinner {
  display: block;
}

.composer__form.is-sending .composer__send-icon {
  display: none;
}

@keyframes composer-spin {
  to {
    transform: rotate(360deg);
  }
}
/* Respect a reduced-motion preference: the state still reads from the swapped glyph and the
   "Sending…" line, so the spin itself is the only thing dropped. */
@media (prefers-reduced-motion: reduce) {
  .composer__spinner {
    animation: none;
  }
  /* The two states still read from what is on screen; only the movement between them is dropped. */
  .composer__collapse {
    transition: none;
  }
}
/* ---------- Empty states ---------- */
.empty-state {
  padding: var(--space-6);
  text-align: center;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

/* The decoration as a block right after the copy rather than the box's own background, so it sits
   under the words even when the empty state fills a whole pane. Zero height while there is none. */
.empty-state::after {
  content: "";
  display: block;
  height: var(--theme-decoration-space);
  margin-top: min(var(--space-4), var(--theme-decoration-space));
  background: var(--theme-decoration) center/contain no-repeat;
}

/* A theme's own line for an empty state (Appearance::CATALOG, e.g. Gotham's "All quiet in
   Gotham."). Every variant is rendered and only the active theme's is displayed, so switching
   theme in the picker updates it with no round trip — and display: none keeps the others out of
   the accessibility tree, so a screen reader hears one line. */
.empty-state__themed {
  display: none;
}

[data-theme=gotham] .empty-state__themed[data-theme-copy=gotham] {
  display: inline;
}

[data-theme=gotham] .empty-state:has(> [data-theme-copy=gotham]) > .empty-state__default {
  display: none;
}

[data-theme=creamsicle] .empty-state__themed[data-theme-copy=creamsicle] {
  display: inline;
}

[data-theme=creamsicle] .empty-state:has(> [data-theme-copy=creamsicle]) > .empty-state__default {
  display: none;
}

[data-theme=ember] .empty-state__themed[data-theme-copy=ember] {
  display: inline;
}

[data-theme=ember] .empty-state:has(> [data-theme-copy=ember]) > .empty-state__default {
  display: none;
}

[data-theme=bubblegum] .empty-state__themed[data-theme-copy=bubblegum] {
  display: inline;
}

[data-theme=bubblegum] .empty-state:has(> [data-theme-copy=bubblegum]) > .empty-state__default {
  display: none;
}

[data-theme=neon-flamingo] .empty-state__themed[data-theme-copy=neon-flamingo] {
  display: inline;
}

[data-theme=neon-flamingo] .empty-state:has(> [data-theme-copy=neon-flamingo]) > .empty-state__default {
  display: none;
}

.empty-state__themed {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: var(--font-display-weight, 600);
  color: var(--color-text);
}

/* ---------- Responsive: stack panes on narrow viewports ---------- */
@media (max-width: 900px) {
  /* The inbox lock above is more specific (`.app:not(...)`), so it has to be
     restated here or a phone would keep a 100% / overflow-hidden shell and clip
     the stacked panes. */
  .app,
  .app:not(.app--report):not(.app--settings) {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto 1fr;
    height: auto;
    max-height: none;
    min-height: 100vh;
    overflow: visible;
  }
  .app:not(.app--report):not(.app--settings) > * {
    max-height: none;
  }
  .thread {
    overflow: visible;
  }
  /* Every collapsed rule is scoped above this breakpoint, so down here the toggle would flip the
     class, write the cookie and change nothing on screen — a control that claims it worked and
     then surprises the reader with a narrow rail the next time they open a desktop. It only
     exists where it does something. */
  .sidebar__collapse-toggle {
    display: none;
  }
  /* Same reasoning for FRO-138's resize handles and rail toggle: the stacked rules set the columns
     outright, so the width properties no longer apply and neither would a drag. */
  .panel-resize, .thread-info__collapse-toggle {
    display: none;
  }
  /* The list is the whole page down here, so the strip has far more room than it needs and
     space-between would fling four tabs across it. Grouped left on their own gap instead, which is
     what the strip looked like before it had counts to make room for. */
  .list__tabs {
    justify-content: flex-start;
    gap: var(--space-4);
  }
  .app__list, .app__thread {
    max-height: none;
  }
  .report {
    overflow: visible;
  }
  .report__scroll {
    overflow: visible;
  }
  .report__header {
    flex-wrap: wrap;
  }
  /* Let the thread pane flow with the page (no fixed height to scroll inside), so the
     messages and composer stack naturally instead of collapsing the inner scroll region. */
  .app__thread {
    overflow: visible;
  }
  /* Flex rather than block so the rail can be ordered above the thread — see below. */
  /* The frame is overflow:hidden on desktop so the two columns clip into their own
     scrollports. Down here it is only a stacking wrapper, and that hidden shears
     the typeahead menus the rail opens. */
  #conversation {
    display: flex;
    flex-direction: column;
    overflow: visible;
  }
  .thread__scroll {
    overflow: visible;
  }
  /* The pane no longer has a height to fill, so the tall editor grows from a viewport floor
     instead of from flex. */
  .thread.is-composer-tall .composer:has(.is-tall) .composer__editor {
    min-height: 70vh;
  }
  /* Left in DOM order the rail would land below a full message list AND the composer, which on a
     phone is the same as not being there. overflow:visible because the rail no longer has a height
     to scroll within — and because clipping here would shear the pickers' menus. */
  .thread-info {
    order: -1;
    width: auto;
    overflow: visible;
    border-left: 0;
    border-bottom: 1px solid var(--color-border);
  }
}
/* ---------- Button variants used by Settings ---------- */
.btn--sm {
  padding: 4px var(--space-2);
  font-size: 12px;
  width: auto;
}

.btn--danger {
  background: var(--color-danger-soft);
  color: var(--color-danger);
  border-color: var(--color-danger-edge);
}

.btn--danger:hover {
  background: color-mix(in srgb, var(--color-danger) 12%, var(--color-danger-soft));
}

.btn--ghost.is-on {
  color: var(--color-accent-text);
  border-color: var(--color-accent);
}

/* An empty turbo-frame is `display: inline` and still makes a line box. This one
   sits after `.app` in the body, so that one line is enough to make the document
   taller than a viewport-locked inbox. Block (and :empty-hidden) takes it out of
   the height chain; the overlay it hosts is position:fixed either way. */
#modal {
  display: block;
}

#modal:empty {
  display: none;
}

/* ---------- Modal overlay (per-project settings) ---------- */
.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
  background: var(--color-scrim);
}

.modal {
  display: flex;
  flex-direction: column;
  width: 100%;
  background: var(--color-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  outline: none;
}

.modal__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--color-border);
}

.modal__title {
  margin: 0;
  font-size: var(--text-lg);
}

.modal__close {
  appearance: none;
  border: 1px solid transparent;
  background: transparent;
  color: var(--color-muted);
  border-radius: var(--radius-md);
  padding: var(--space-1);
  cursor: pointer;
  display: inline-flex;
}

.modal__close:hover {
  color: var(--color-text);
  background: var(--color-surface-sunken);
}

/* ---------- Command palette (Cmd-K) ---------- */
/* Over .modal-overlay (100): the hotkey is global, and Esc closes this first so it
   cannot trap letter shortcuts behind a dialog the reader can no longer see. */
.command-palette {
  position: fixed;
  inset: 0;
  z-index: 110;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  padding: 15vh var(--space-4) var(--space-4);
}

.command-palette[hidden] {
  display: none;
}

.command-palette__backdrop {
  position: absolute;
  inset: 0;
  background: var(--color-scrim);
}

.command-palette__panel {
  position: relative;
  display: flex;
  flex-direction: column;
  width: min(560px, 100%);
  max-height: min(420px, 80vh);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
}

.command-palette__title {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
}

.command-palette__search {
  flex-shrink: 0;
  border-bottom: 1px solid var(--color-border);
}

.command-palette__query {
  appearance: none;
  width: 100%;
  border: 0;
  background: transparent;
  font: inherit;
  font-size: var(--text-lg);
  color: var(--color-text);
  padding: var(--space-4);
}

.command-palette__query::placeholder {
  color: var(--color-muted);
}

.command-palette__query:focus {
  outline: none;
}

.command-palette__results {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: var(--space-2);
}

.command-palette__group + .command-palette__group {
  margin-top: var(--space-2);
}

.command-palette__group-label {
  margin: 0;
  padding: var(--space-1) var(--space-3);
  color: var(--color-muted);
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
}

.command-palette__option {
  appearance: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  width: 100%;
  margin: 0;
  border: 0;
  background: transparent;
  text-align: left;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  font: inherit;
  color: var(--color-text);
  cursor: pointer;
}

.command-palette__option:hover,
.command-palette__option.is-active {
  background: var(--color-accent-soft);
}

.command-palette__option-main {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.command-palette__option-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.command-palette__option-meta {
  color: var(--color-muted);
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.command-palette__option-meta:empty {
  display: none;
}

.command-palette__option-hint {
  flex-shrink: 0;
  color: var(--color-muted);
  font-size: 12px;
}

.command-palette__empty {
  margin: 0;
  padding: var(--space-4);
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.command-palette__empty[hidden] {
  display: none;
}

/* The ticket an exact id names, and the closing "See all results" row: the label carries the weight
   so the row an Enter will open reads as the answer rather than as one hit among several. */
.command-palette__option--jump .command-palette__option-label,
.command-palette__option--see-all .command-palette__option-label {
  font-weight: 600;
}

.command-palette__option--see-all .command-palette__option-label {
  color: var(--color-accent-text);
}

/* ---------- Universal search page (/search) ---------- */
.search-page__header {
  flex-wrap: wrap;
}

.search-page__form {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 1;
  min-width: 0;
  max-width: 640px;
}

.search-page__field {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 1;
  min-width: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  padding: var(--space-2) var(--space-4);
}

.search-page__field:focus-within {
  border-color: var(--color-accent);
}

.search-page__field-icon {
  --icon-size: 16px;
  color: var(--color-muted);
}

.search-page__query {
  appearance: none;
  flex: 1;
  min-width: 0;
  border: 0;
  background: transparent;
  font: inherit;
  color: var(--color-text);
}

.search-page__query:focus {
  outline: none;
}

.search-page__tabs {
  justify-content: flex-start;
  gap: var(--space-5);
  margin: 0 0 var(--space-4);
}

.search-results {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--color-border);
}

.search-result {
  border-bottom: 1px solid var(--color-border);
}

.search-result__link {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-3) var(--space-2);
  color: var(--color-text);
  text-decoration: none;
  min-width: 0;
}

.search-result__link:hover {
  background: var(--color-surface-sunken);
}

.search-result__title {
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.search-result__meta {
  color: var(--color-muted);
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.search-result__meta:empty {
  display: none;
}

.search-page__pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  margin-top: var(--space-4);
}

.search-page__page {
  color: var(--color-muted);
  font-size: var(--text-sm);
}

/* ---------- Settings page ---------- */
/* The pane beside the settings rail. The section inside it scrolls, not the window, so the rail
   stays put the way it does on the inbox. */
.settings {
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--color-surface);
}

/* The vertical space belongs to the message, not the container: the container is always in the DOM
   (turbo_stream replaces it by id) and holds ERB whitespace, so :empty never matches it and padding
   here would sit above every section that has nothing to say. */
.settings__flash {
  padding: 0 var(--space-6);
}

.settings__flash-msg {
  margin: var(--space-4) 0 0;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
}

.settings__flash-msg--notice {
  background: var(--color-accent-soft);
  color: var(--color-accent-text);
}

.settings__flash-msg--error {
  background: var(--color-danger-soft);
  color: var(--color-danger);
}

.settings__detail {
  display: block;
  overflow-y: auto;
  padding: var(--space-6);
  min-height: 0;
}

/* The pane is as wide as the monitor, and these sections are forms and rows of prose — stretching
   a two-field form across 2000px is not more room, it is a longer line to read. The section is the
   column instead, wide enough for the permissions matrix and the reply-template editor, which were
   the two the 900px modal squeezed. */
.settings-section {
  max-width: 880px;
}

/* .btn--primary is full-width by default because its first home was the sidebar's + Project form,
   where it spans the rail. On both settings surfaces it sits inline beside the field it submits, so
   each opts out — the project modal included, or its Save would stretch and wrap. */
:is(.settings, .add-user, .invite-teammate, .report) .btn--primary {
  width: auto;
}

.settings-section__head {
  margin-bottom: var(--space-4);
}

.settings-section__head:has(.settings-section__actions) {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

.settings-section__actions {
  flex-shrink: 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-2);
}

.settings-section__title {
  margin: 0;
  font-size: var(--text-lg);
}

.settings-section__hint {
  margin: var(--space-1) 0 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.settings-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

.settings-toolbar .list__search {
  flex: 1;
  min-width: 180px;
}

/* Never wider than the toolbar: a long project name would otherwise push the select past the pane at
   phone width. */
.settings-toolbar .field__select {
  width: auto;
  max-width: 100%;
  min-width: 0;
}

.settings-form {
  margin-bottom: var(--space-4);
}

.settings-form__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

.settings-form__row .field__input {
  flex: 1;
  min-width: 120px;
}

.settings-form__check {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: var(--text-sm);
  color: var(--color-muted);
  white-space: nowrap;
}

.settings-form__error {
  margin: 0 0 var(--space-2);
  color: var(--color-danger);
  font-size: var(--text-sm);
}

/* A filter's refused save: the error opens the form, just under the editor's history link. */
.settings-section__hint + form > .settings-form__error {
  margin-top: var(--space-3);
}

/* Controls that only make sense as a set, in a real <fieldset> so the grouping is in the
   accessibility tree and not only in the spacing. The browser's own border and padding are dropped:
   the group sits inside a form that is already boxed, and a second frame inside it reads as a panel
   within a panel. */
/* Left as a block rather than given a flex gap, so the .settings-form__row + .settings-form__row
   rule above still spaces its selects — a gap on top of that margin would space them twice. */
.settings-form__group {
  margin-top: var(--space-3);
  padding: 0;
  border: 0;
}

.settings-form__legend {
  padding: 0;
  margin-bottom: var(--space-2);
  color: var(--color-muted);
  font-size: var(--text-sm);
  font-weight: 600;
}

.settings-form__group .settings-form__check {
  margin-top: var(--space-2);
}

.settings-form__group:disabled {
  opacity: 0.65;
}

.settings-unanswered__field {
  margin-bottom: var(--space-3);
}

.settings-unanswered__field .field__select {
  width: 100%;
  max-width: 20rem;
}

.settings-unanswered__note {
  margin-top: var(--space-4);
}

.settings-ai-drafts__field {
  margin-bottom: var(--space-3);
}

.settings-ai-drafts__field .field__input,
.settings-ai-drafts__field .field__select {
  width: 100%;
  max-width: 20rem;
}

.settings-ai-drafts__instructions .field__input {
  max-width: 32rem;
}

.settings-ai-drafts__note {
  margin-top: var(--space-4);
}

.settings-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* display:flex on the list and its rows outranks the UA [hidden] rule, the same trap
   .dropdown__menu documents above. */
.settings-list__group[hidden],
.settings-row[hidden],
.settings-list > .empty-state[hidden],
.settings-list__group .settings-list[hidden] {
  display: none;
}

.settings-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

.settings-row--stack {
  flex-direction: column;
  align-items: stretch;
}

.settings-row__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}

.settings-row__identity {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 1;
  min-width: 0;
}

.settings-row__main {
  display: flex;
  flex-direction: column;
  gap: 1px;
  flex: 1;
  min-width: 0;
}

/* Ellipsised like the .settings-row__sub beneath it, which has always had this: a display name may
   be eighty characters, and an unbroken one has no wrap point, so without it the name ran out of
   its column and printed underneath the row's own buttons. */
.settings-row__name {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
}

.settings-row__sub {
  color: var(--color-muted);
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
}

.settings-row__badge {
  flex-shrink: 0;
}

.settings-row__badges {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-1);
  flex-shrink: 0;
}

.user-avatar.settings-row__invite-avatar {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-muted);
}

.user-avatar.settings-row__invite-avatar svg {
  width: 18px;
  height: 18px;
}

.knowledge-resolution-draft {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

.knowledge-resolution-draft__spinner {
  width: 10px;
  height: 10px;
  flex: 0 0 auto;
  border: 2px solid var(--color-border-strong);
  border-top-color: var(--color-muted);
  border-radius: 50%;
  animation: composer-spin 0.6s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
  .knowledge-resolution-draft__spinner {
    animation: none;
  }
}
/* Resolutions queue hygiene (FRO-61): the bulk-skip bar above the list, a row's tick box, and a
   group of similar drafts — the audience group's heading with the group's own Skip beside it. */
.knowledge-resolutions__bulk {
  display: flex;
  justify-content: flex-end;
  margin-bottom: var(--space-2);
}

.knowledge-resolutions__bulk[hidden] {
  display: none;
}

.knowledge-resolution__select {
  flex: none;
  margin: 0;
}

.knowledge-resolution__close a {
  color: var(--color-accent-text);
  font-weight: 600;
}

.knowledge-resolution-group {
  padding: var(--space-3);
  border: 1px dashed var(--color-border-strong);
  border-radius: var(--radius-md);
}

.knowledge-resolution-group__label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 1;
  min-width: 0;
}

/* An audience group in a settings list: its heading, then its own nested list. A list item rather
   than a sibling of the list, so a group's rows are its children for anyone navigating by list —
   which also keeps the whole thing inside the one <ul> a turbo stream replaces. */
.settings-list__group {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* The section-label treatment the sidebar and the dropdown sections already use, so an audience
   heading reads as the same kind of divider everywhere it appears. */
.settings-list__group-label {
  margin: 0;
  color: var(--color-muted);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
}

/* Button-as-caption: authored for <button>, so appearance and font have to be reset or the
   UA chrome stays. Same caret-swap the quoted-body and thread-info disclosures use. */
.settings-list__group-toggle {
  appearance: none;
  width: 100%;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  text-align: left;
  cursor: pointer;
}

.settings-list__group-toggle:hover {
  color: var(--color-text);
}

.settings-list__group-caret {
  --icon-size: 14px;
  color: var(--color-muted);
}

.settings-list__group.is-open .settings-list__group-caret {
  transform: rotate(180deg);
}

.settings-list__group-domain {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Counts stay sentence case so "2 members" is not shouted as "2 MEMBERS". */
.settings-list__group-meta {
  flex: none;
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
}

.settings-row__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
}

/* Only inside the Email section: its action row follows the bound-mailbox list directly, and the
   .settings-subform it replaced carried this margin. Without it the buttons sit flush against the
   last address. */
#project_settings_email .settings-row__actions {
  margin-top: var(--space-2);
}

/* Second block in Email: how inbound From is attributed, after how mail gets in.
   Space-6 matches the gap between settings-section heads so the two jobs read as
   siblings rather than one list continuing into the next. */
#project_settings_system_senders {
  margin-top: var(--space-6);
}

/* Second block in Knowledge: unverified resolution drafts, after the published
   articles they are waiting to join. Space-6 matches System senders under Email. */
#project_settings_knowledge_resolutions {
  margin-top: var(--space-6);
}

/* The irreversible act on General. Its own box rather than another settings-row: the danger
   tokens have to wrap the whole warning, not only the button, or Delete reads as one more
   control in the name form. Space-6 matches System senders under Email — a second job, not
   a continuation of the form above. */
.settings-danger-zone {
  margin-top: var(--space-6);
  padding: var(--space-4);
  border: 1px solid var(--color-danger-edge);
  border-radius: var(--radius-md);
  background: var(--color-danger-soft);
}

#project_settings_general > .settings-form__group:first-of-type {
  margin-top: 0;
}

.settings-danger-zone__title {
  margin: 0 0 var(--space-3);
  color: var(--color-danger);
  font-size: var(--text-sm);
  font-weight: var(--font-display-weight, 700);
}

.settings-danger-zone__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

.settings-danger-zone__copy {
  flex: 1;
  min-width: 12rem;
}

.settings-danger-zone__name {
  margin: 0;
  font-weight: 600;
}

.settings-danger-zone__hint {
  margin: var(--space-1) 0 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.settings-danger-zone .btn--danger {
  width: auto;
  flex-shrink: 0;
}

.settings-row__action {
  display: inline;
  margin: 0;
}

/* After adding a system sender that already has open inbox threads. Accent-soft
   like the settings flash, so the confirm reads as the next step of the add
   rather than a second error. */
.settings-callout {
  margin: var(--space-3) 0;
  padding: var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-accent-soft);
  color: var(--color-accent-text);
}

.settings-callout__title {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 600;
}

/* The warning tone (FRO-152): the forward's steer and PII notes. Settings had no warning box — the plain
   callout is accent-toned and the danger zone is for destruction. Existing token pair only. */
.settings-callout--warning {
  background: var(--color-warning-soft);
  border: 1px solid var(--color-warning);
  color: var(--color-text);
}

.settings-callout--warning .settings-callout__title .icon {
  color: var(--color-warning);
}

.settings-callout__hint {
  margin: var(--space-1) 0 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.settings-callout__action {
  display: inline;
  margin: var(--space-2) 0 0;
}

.settings-sublist {
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.settings-sublist__item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  font-size: var(--text-sm);
}

.settings-sublist__label {
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The author line under an allowlist entry. Its own block so the value keeps the row's weight and
   the provenance reads as the footnote it is. */
.settings-sublist__meta {
  display: block;
  color: var(--color-muted);
  font-size: 12px;
}

/* A list whose rows are two lines each needs more air between rows than the 4px that separates
   one-line rows, or the gap inside a row reads the same as the gap between two of them and the
   author line looks like it belongs to the address below it. */
.settings-sublist--annotated {
  gap: var(--space-2);
}

.settings-sublist__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  flex-shrink: 0;
}

.settings-sublist__empty {
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.settings-subform {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.settings-subform .field__input {
  flex: 1;
  min-width: 0;
}

/* The project row's Linear wiring. Wraps rather than squeezes, so the two selects drop onto their
   own lines at narrow width instead of shrinking past the point their option text is readable. */
.settings-linear {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.settings-linear__label,
.settings-auto-reply__label {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-muted);
}

.settings-linear__form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  flex: 1;
  min-width: 0;
}

.settings-linear__form .field__select {
  flex: 1;
  min-width: 140px;
  font-size: var(--text-sm);
}

.settings-linear__static {
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Full row under the team/state pickers so the checkbox is a second decision, not a third
   control squeezed onto the same line as the selects. */
.settings-linear__auto-create {
  flex-basis: 100%;
}

.settings-linear__note {
  margin-top: var(--space-2);
}

/* How a project authenticates its mailboxes, and which OAuth client that resolves to. Laid out like
   .settings-linear above and for one extra reason: this row sits in the Email section, directly
   above Slack's manifest panel, so height spent here is height the Copy button down there loses
   before it needs a scroll. The select is sized to its own content rather than stretched — there is
   one provider, and a full-width control implies a choice that is not being offered — which leaves
   the summary beside it the room to ellipsise a seventy-character client id instead of wrapping. */
.settings-mailbox-auth {
  margin-top: var(--space-3);
}

.settings-mailbox-auth__form {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}

.settings-mailbox-auth__form .field__select {
  font-size: var(--text-sm);
  min-width: 120px;
}

.settings-mailbox-auth .settings-row__sub {
  flex: 1;
  min-width: 0;
  white-space: nowrap;
}

/* The domain list, on the line below. Not appended to the summary beside the select: a client id
   runs to seventy characters and ellipsises, which took the domains with it. */
.settings-mailbox-auth__domains {
  margin-top: var(--space-1);
}

/* Connect email, when the project is still on the default and more than one Google app exists.
   Native select for the Linear pickers' reason: a popover here is clipped by .settings__detail.
   Sized to its options rather than stretched — a full-width control on a project row implies a
   setting, and this is a one-shot destination. */
.settings-mailbox-connect {
  display: flex;
  align-items: center;
  min-width: 0;
}

.settings-mailbox-connect .field__select {
  font-size: var(--text-sm);
  min-width: 220px;
  max-width: 100%;
}

/* A value the user is meant to copy — a reset link, a Slack Request URL. Spaced like the two
   project-row blocks above so every stacked settings row breaks the same way.

   min-width:0 on the field is load-bearing, not defensive: a flex item's default min-width is its
   content, and a ~100-character URL is wide enough to shove the Copy button clean off the row.
   The hint claims a whole line of its own so it never competes with them for the remainder. */
.settings-copy-field {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.settings-copy-field__url {
  flex: 1;
  min-width: 0;
  font-size: var(--text-sm);
}

.settings-copy-field__hint {
  flex-basis: 100%;
  margin: 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

/* The project's Slack bot panel. Steps stack down the section, each a labelled block, so the three
   pastes read in the order Slack forces them into. */
.settings-slack {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-top: var(--space-3);
}

.settings-slack__step {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* margin-bottom, not 0: the credential step's title is followed immediately by a field label, and
   without it the two read as one two-line heading. The other steps have a hint paragraph between
   them, which is why this only shows on step 2. */
.settings-slack__step-title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-muted);
}

/* The app manifest. Bordered like .settings-row so it reads as one block rather than loose text.

   max-height plus overflow are both load-bearing: a manifest is around thirty lines, the modal body
   already scrolls, and an uncapped block would push the credential fields and the Disconnect button
   off the panel entirely. overflow-x keeps a long request_url inside its own box instead of widening
   the modal — the same failure .settings-copy-field__url avoids with min-width: 0. */
.settings-slack__manifest {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-2);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.settings-slack__manifest-body {
  margin: 0;
  max-height: 220px;
  overflow: auto;
  font-size: 12px;
  line-height: 1.5;
  white-space: pre;
  color: var(--color-text);
}

.settings-slack__manifest-body:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 2px;
}

.settings-slack__manifest-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2);
}

.settings-slack__manifest-status {
  color: var(--color-muted);
  font-size: var(--text-sm);
}

/* The one action row the panel ends on. Spaced off whatever precedes it — a revealed credential
   form, or the Request URL — so the buttons read as the section's footer rather than as part of it. */
.settings-slack__actions {
  margin-top: var(--space-2);
  align-items: center;
}

/* The project row's acknowledgement switch. Sibling of the Linear block above and spaced to match
   it, so the two settings read as one column rather than two treatments.

   __note belongs to the per-project modal, where the section has room to explain itself: it states
   a consequence of the checkbox above it, so it trails the control rather than sitting in the
   section header where the other hints live. The admin list has no room to repeat that per row and
   says it once in its own header instead. */
.settings-auto-reply {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.settings-auto-reply__note {
  margin-top: var(--space-2);
}

/* The two admin taxonomies — request types in Settings, project parts in the project modal. One
   rule set rather than two, because they are the same row at two scopes and a copy would drift.
   The row carries its own editor, revealed by the disclosure controller — so the read-only
   description is hidden while that editor is open rather than printing the same sentence twice. */
.settings-taxonomy__preview {
  margin: var(--space-2) 0 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.settings-row.is-open .settings-taxonomy__preview {
  display: none;
}

/* margin-left:auto rather than relying on the head's space-between: with three children the middle
   one floats by the chip's width, so counts staggered across ~40px while the buttons beside them
   were flush. Pushed right, the numbers read as the column they are. */
.settings-taxonomy__usage {
  flex-shrink: 0;
  margin-left: auto;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

/* Page-specific override, not a change to the shared .badge: at the narrow breakpoint a two-word
   type name broke the pill onto two lines. Scoped here so every other badge keeps wrapping. */
.settings-taxonomy__badge {
  white-space: nowrap;
  flex-shrink: 0;
}

/* Cancel/Save under a row's revealed editor. One rule for the taxonomies and for the Users row,
   which reveals a name field the same way — two names because they are different blocks, one
   declaration block because the footer of a revealed editor must not drift between sections. */
.settings-taxonomy__actions,
.settings-row__form-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

/* ---------- Assignment rules in the project modal ---------- */
/* The rule IS its sentence — "Access Issue · urgent → Sarah" — so the row's one piece of content takes
   the line and pushes the actions right, the way .settings-row__main does for a name/description pair.
   flex:1 rather than margin-left:auto on the buttons, because the sentence is the part that has to be
   allowed to wrap: a rule naming three conditions and two people is longer than the row is wide. */
.settings-assignment-rule__sentence {
  flex: 1;
  min-width: 0;
}

.settings-assignment-rule__next,
.settings-assignment-rule__activity {
  flex-shrink: 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
  white-space: nowrap;
}

/* Three condition selects on one line, and each has to stay wide enough for its longest option ("Any
   request type") to be readable. Below that they wrap onto their own lines rather than shrinking —
   the same call .settings-linear makes for the two team pickers. */
.settings-assignment-rule__fields .field__select {
  flex: 1;
  min-width: 140px;
}

/* A native multi-select is a list box, not a collapsed picker, so the caret .field__select draws for
   a single select would sit over the last option. The height is enough to show a short rotation as
   rows rather than as a one-line control you have to open. */
.settings-assignment-rule__members {
  min-width: 180px;
  min-height: 4.75rem;
  padding-right: var(--space-3);
  background-image: none;
  overflow: auto;
  cursor: default;
}

/* The message conditions (FRO-151, shared with FRO-152's filters): a visible label beside each input,
   which no settings row had before — .settings-form__row has no label column. Stacked on a phone.

   minmax(0, 1fr) and the min-width: 0s are what keep a select inside the pane: a bare 1fr track, a
   fieldset and a select all default to growing to fit their content, and a select's content is its
   longest option — a 49-character project name pushed the pane 23px past a phone's screen. */
.settings-rule-conditions__field {
  display: grid;
  grid-template-columns: 7rem minmax(0, 1fr);
  gap: var(--space-2);
  align-items: start;
  margin-top: var(--space-2);
}

.settings-rule-conditions__field .field__label {
  margin-top: var(--space-2);
}

.settings-rule-conditions,
.settings-assignment-rule__action {
  min-width: 0;
}

.settings-rule-conditions__field .field__select {
  min-width: 0;
  max-width: 100%;
}

/* Assign or Move, revealed by CSS alone — the :has() technique .settings-appearance__option uses.
   display:none takes the hidden half out of the tab order and the accessibility tree; it still posts,
   and the model drops it on save. With no radios (a single-project installation) the members show,
   which is the form it always was. */
.settings-assignment-rule__when-assign,
.settings-assignment-rule__when-move {
  display: none;
}

.settings-assignment-rule__action:not(:has(input[name$="[kind]"])) .settings-assignment-rule__when-assign,
.settings-assignment-rule__action:has(input[value=assign]:checked) .settings-assignment-rule__when-assign {
  display: flex;
}

.settings-assignment-rule__action:has(input[value=move]:checked) .settings-assignment-rule__when-move {
  display: block;
}

.settings-assignment-rule__when-move .settings-form__check {
  white-space: normal;
}

/* "Test this rule": each result a link across the row, subject and meta on the left, the time
   right-aligned and never wrapped. */
.settings-assignment-rule__preview {
  margin-top: var(--space-3);
}

.settings-assignment-rule__preview-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
  width: 100%;
  min-width: 0;
  color: inherit;
  text-decoration: none;
}

/* The label keeps a readable width; on a narrow pane a tag ("Spam — not forwarded") wraps below it
   rather than squeezing the subject to a few words a line. */
.settings-assignment-rule__preview-row .settings-sublist__label {
  flex: 1 1 12rem;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* "Test this filter"'s attachment count sits inline after the subject, not on a line of its own. */
.settings-assignment-rule__preview-count {
  color: var(--color-muted);
  font-size: 12px;
  white-space: nowrap;
  margin-left: var(--space-1);
}

.settings-assignment-rule__preview-count .icon {
  --icon-size: 12px;
  vertical-align: -1px;
}

/* The badge and the time line up down the list: centred rather than on the baseline, which made a row
   with a badge a few pixels taller, and the time in a column of its own width so a badge does not
   shift with "3d ago" beside it versus "12d ago". margin-left: auto keeps the time on the right edge
   when a tag has wrapped onto a line of its own at a narrow width. */
.settings-assignment-rule__preview-row .settings-row__badge {
  align-self: center;
}

.settings-assignment-rule__preview-row .settings-assignment-rule__activity {
  min-width: 4.5em;
  margin-left: auto;
  text-align: right;
}

/* The incoming hint's link, told apart from the hint it sits in. */
.settings-section__hint a {
  color: var(--color-accent-text);
  text-decoration: underline;
}

.settings-section__hint a:hover {
  color: var(--color-text);
}

/* Filters (FRO-152): one checkbox per action, each revealing its control by CSS alone — the :has()
   technique above, with checkboxes rather than a radio pair because a filter does several things at
   once. The revealed control sits under the checkbox's text. The hidden ones still post, and an
   unticked box tells the model to drop them. */
.settings-filter__action {
  margin-top: var(--space-2);
  min-width: 0;
}

.settings-filter__when {
  display: none;
  padding-inline-start: calc(1rem + var(--space-2));
  margin-top: var(--space-1);
  min-width: 0;
}

.settings-filter__action:has(> .settings-form__check input:checked) .settings-filter__when {
  display: block;
}

.settings-filter__when .field__select {
  min-width: 0;
  max-width: 100%;
}

/* A multi-select is a list box: no single-select caret over its last row (the members list's reason). */
.settings-filter__when .field__select[multiple] {
  padding-right: var(--space-3);
  background-image: none;
  overflow: auto;
}

.settings-filter__when .settings-form__check {
  white-space: normal;
}

.settings-filter__action > .field__hint {
  padding-inline-start: calc(1rem + var(--space-2));
}

.settings-filter__inbox {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-3);
  margin-top: var(--space-3);
}

.settings-filter__inbox-hint {
  display: none;
  flex-basis: 100%;
  margin: 0;
}

.settings-filter__forward {
  margin-top: var(--space-3);
}

.settings-filter__forward .settings-callout {
  margin-top: var(--space-2);
}

.settings-filter__inbox:has(input[value=archive]:checked, input[value=spam]:checked) .settings-filter__inbox-hint {
  display: block;
}

@media (max-width: 640px) {
  .settings-rule-conditions__field {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-1);
  }
  .settings-rule-conditions__field .field__label {
    margin-top: 0;
  }
  .settings-filter__inbox {
    flex-direction: column;
    align-items: flex-start;
  }
  .settings-filter__when .field__select {
    width: 100%;
  }
  /* A long sentence takes the whole line; the badge, the activity label and the actions drop beneath
     it. Wrapping alone is not enough: the sentence would keep sharing its line and shrink to a column.
     The wrapped lines start at the left, so the activity label does not jump from side to side down
     the list depending on whether a badge shares its line. */
  .settings-assignment-rule .settings-row__head {
    flex-wrap: wrap;
    justify-content: flex-start;
  }
  .settings-assignment-rule__sentence {
    flex-basis: 100%;
  }
  /* The Filter log's rows the same way: the sentence on its own line, badge and time beneath it. */
  .settings-filter-log__entry .settings-row__head {
    flex-wrap: wrap;
    justify-content: flex-start;
  }
  .settings-filter-log__sentence {
    flex-basis: 100%;
  }
}
/* Settings → Filter log (FRO-152). The rows are .settings-row--stack; these keep a long sentence wrapping
   inside its row, the kind glyph muted, and each page of older entries flush under the one above it. */
.settings-filter-log {
  margin: 0;
  padding: 0;
  list-style: none;
}

.settings-filter-log__sentence {
  min-width: 0;
  overflow-wrap: anywhere;
}

.settings-filter-log__icon {
  color: var(--color-muted);
  margin-inline-end: 4px;
}

.settings-filter-log__change {
  overflow-wrap: anywhere;
}

.settings-filter-log__older {
  display: block;
  margin-top: var(--space-3);
}

.settings-filter-log__chip {
  margin-bottom: var(--space-3);
}

/* ---------- Reply templates in Settings ---------- */
/* The composer's own box, borrowed whole, with the two things it assumes about the thread pane
   corrected: it is not pinned to the bottom of anything here, and it has no pane height to grow
   into. Everything else about it — the toolbar, the link row, the emoji palette, the placeholder —
   is the composer's and is deliberately left alone. */
.reply-template-editor {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

.reply-template-editor .composer__editor {
  max-height: 240px;
  overflow-y: auto;
}

/* Name and shortcut read as one label, so the row's actions stay on the trailing edge every other
   settings row keeps them on — a third child of a space-between row would strand the shortcut in the
   middle of it. */
.reply-template-row__label {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  min-width: 0;
}

/* The handle, wherever it is printed — a settings row, the hint under the field, the picker's own
   option. Mono and accented for the reason a variable token is: it is something typed literally. */
.reply-template-shortcut {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--color-accent-text);
  white-space: nowrap;
}

/* What this template does beyond writing words, in the collapsed row. Accented and glyphed for the
   reason the picker's own action line is: everything else in the row describes words, and this is
   the one part that describes an act. Hidden while the editor is open, exactly as the preview above
   it is — the controls in the form say it already, and there they say it as something changeable. */
.reply-template-row__macro {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  margin: var(--space-1) 0 0;
  color: var(--color-accent-text);
  font-size: var(--text-sm);
}

.settings-row.is-open .reply-template-row__macro {
  display: none;
}

.reply-template-variables {
  margin-top: var(--space-2);
}

/* A definition list, because that is what it is: each token IS the term its sentence defines. */
.reply-template-variables__list {
  margin: var(--space-2) 0 0;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-1) var(--space-3);
  font-size: var(--text-sm);
}

.reply-template-variables__list[hidden] {
  display: none;
}

/* Monospace, because the token has to be copied character for character — a proportional font that
   renders {{contact.first_name}} prettily is a font that hides the underscore. */
.reply-template-variables__token {
  margin: 0;
  font-family: var(--font-mono);
  color: var(--color-accent-text);
  white-space: nowrap;
}

.reply-template-variables__label {
  margin: 0;
  color: var(--color-muted);
}

/* The colour picker: swatches laid out inline, NOT in a .dropdown popover. The settings modal is
   overflow:hidden around a scrolling .settings__detail, so an absolutely positioned menu opened
   from a row near the bottom is clipped — the same constraint that keeps the Linear team pickers
   native selects.

   Real radio inputs do the work: the platform gives arrow-key movement, roving focus and :checked
   for free, so there is no controller here. The input itself is taken out of the visual flow but
   left focusable, and the swatch reacts to its state through :checked/:focus-visible siblings. */
.color-swatches {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  border: 0;
}

.color-swatches__legend {
  padding: 0;
  margin-bottom: var(--space-2);
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.color-swatch {
  display: inline-flex;
  cursor: pointer;
}

/* Not display:none — that would take the radio out of the tab order and off the accessibility
   tree, which is the whole reason for using real inputs. */
.color-swatch__input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

.color-swatch__dot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 999px;
  background: var(--swatch-color);
  /* An inset ring rather than an outset one: .settings__detail scrolls, and an outset ring on the
     bottom row would be clipped by it. */
  box-shadow: inset 0 0 0 1px var(--color-swatch-ring);
  color: var(--color-swatch-check);
}

/* White tick on a colour nobody vetted — a pale Linear label would swallow it. The shadow is what
   makes it read on a light swatch as well as a dark one, without needing a second derived colour. */
.color-swatch__check {
  --icon-size: 12px;
  visibility: hidden;
  filter: drop-shadow(0 0 1px var(--color-swatch-check-shadow));
}

/* Selection reads two ways beyond the colour — the tick and the ring — never by colour alone.
   The ring's inner band is --color-text rather than the swatch's own colour: ringing a colour in
   itself is invisible, which left the tick carrying the whole signal. Dark-on-white reads at 24px
   whatever the swatch is.
   Still drawn INSET, and the dot does not change size: an outset ring on the bottom row of a
   scrolling pane is what gets clipped, and a dot that grew on selection would reflow the row
   every time the choice moved. */
.color-swatch__input:checked + .color-swatch__dot {
  box-shadow: inset 0 0 0 2px var(--color-surface), inset 0 0 0 4px var(--color-text);
}

.color-swatch__input:checked + .color-swatch__dot .color-swatch__check {
  visibility: visible;
}

.color-swatch__input:focus-visible + .color-swatch__dot {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 2px;
}

/* The app's first textarea. .field__input already gives it the border, padding and focus ring;
   only the resize handle's axis is left to say, so it cannot be dragged wider than its column. */
textarea.field__input {
  resize: vertical;
  min-height: calc(2lh + var(--space-4));
}

/* The add form's rows stack, unlike the one-line Users and Projects forms above. */
.settings-form__toggle {
  margin-bottom: var(--space-2);
}

.settings-form__row + .settings-form__row {
  margin-top: var(--space-2);
}

/* New article and Import markdown sit on one row while both are closed. An open
   disclosure takes the full width so the file field and part select are not
   squeezed into the toggle's shrink-wrapped column. */
.knowledge-article-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.knowledge-article-actions .settings-form__toggle {
  margin-bottom: 0;
}

.knowledge-article-actions > .settings-form.is-open {
  flex: 1 1 100%;
}

/* The shared settings row is a flex line. A hint in that line sits beside the
   file field and steals its width; it belongs on the next line. */
.knowledge-article-actions .field__hint {
  flex-basis: 100%;
}

/* Native file chrome is the default grey button. The picker button is redrawn
   as the ghost sm the rest of this pane uses; the filename stays the UA's. */
input[type=file].field__input {
  padding: var(--space-1) var(--space-2);
}

input[type=file].field__input::file-selector-button {
  appearance: none;
  margin-right: var(--space-2);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  padding: 4px var(--space-2);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  color: var(--color-muted);
  background: transparent;
  cursor: pointer;
}

input[type=file].field__input::file-selector-button:hover {
  color: var(--color-text);
  background: var(--color-surface-sunken);
}

.settings-permissions {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.settings-permissions__user {
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

.settings-permissions__projects {
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.settings-permissions__project {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.settings-permissions__project-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.settings-permissions__note {
  margin: var(--space-1) 0 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

/* form_with wraps each select in a <form>; without this the block element breaks the row. */
.settings-permissions__form {
  display: contents;
}

/* Fixed width so every row's control shares one left and one right edge. A native select sizes
   to its content, and "No access" against "Can view & reply" would leave the column ragged —
   and the column edge is what the eye follows when scanning a card. */
.settings-permissions__access {
  flex: 0 0 auto;
  min-width: 148px;
  font-size: var(--text-sm);
}

/* Ungranted rows recede so the granted ones are what the eye catches. Supplementary only: the
   level is stated in the select's own text, never by colour alone. */
.settings-permissions__access--none {
  color: var(--color-muted);
}

/* ---------- Per-project settings ----------
   Project settings is a full page now, same furniture as /settings. These leftovers style the
   section partials that still carry .project-settings__section from the modal years. */
/* Duplicate picker. Narrower than project settings: it is a search and a list, not four
   sections. max-height plus overflow on the results (not the panel) keeps the search field
   pinned while the matches scroll. */
.modal.duplicate-picker {
  max-width: 480px;
  max-height: 82vh;
}

/* Move to project (FRO-150). Same footprint as the duplicate picker. The form scrolls rather than
   the panel, so the header and its ✕ stay put on a short screen and the commit, the last child,
   is always reachable. Each consequence is a quiet explanatory row, joining the .composer__notice
   family: a 14px muted icon and small muted text, the sentence carrying the meaning. */
.modal.move-confirm {
  max-width: 480px;
  max-height: 82vh;
}

.move-confirm__form,
.move-confirm__unavailable {
  margin: 0;
  padding: var(--space-4) var(--space-5) var(--space-5);
  overflow-y: auto;
}

.move-confirm__route {
  margin: 0 0 var(--space-3);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
}

.move-confirm__route-from {
  color: var(--color-muted);
}

.move-confirm__route-arrow {
  --icon-size: 14px;
  color: var(--color-muted);
}

.move-confirm__heading {
  margin: var(--space-4) 0 var(--space-2);
  font-size: var(--text-sm);
  font-weight: 600;
}

.move-confirm__effects {
  margin: 0 0 var(--space-4);
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.move-confirm__effect {
  margin: 0;
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  font-size: var(--text-sm);
  color: var(--color-muted);
}

.move-confirm__effect-icon {
  --icon-size: 14px;
  flex: none;
  display: inline-flex;
  margin-top: 2px;
}

/* Not colour alone: the row carries the warning glyph and says "has no email mailbox". */
.move-confirm__effect--warning {
  color: var(--color-warning);
}

.move-confirm__note {
  resize: vertical;
}

/* The label names the destination, which can be long; the shared check style never wraps. */
.move-confirm__rules .settings-form__check {
  white-space: normal;
  align-items: flex-start;
}

.move-confirm__rules .settings-form__check input {
  margin-top: 3px;
}

.move-confirm__commit {
  width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A project row in either Move picker: the name takes the room and ellipsizes (the full name is in
   title), and the hint stays right-aligned and visible, ellipsizing itself only past its cap. The
   digit badge sits in the option's reserved right padding, which nothing overflows into now. */
.dropdown__option.move-picker__option {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.dropdown__option.move-picker__option[hidden] {
  display: none;
}

.move-picker__name {
  flex: 1 1 0;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.dropdown__option-hint.move-picker__hint {
  flex: 0 1 auto;
  max-width: 45%;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* An address may ellipsize, but "No active email mailbox" is a warning whose last word is the point:
   it keeps its whole width and the name gives way instead. */
.dropdown__option-hint.move-picker__hint--warning {
  flex: none;
  max-width: none;
}

.modal.message-source {
  max-width: 640px;
  max-height: 82vh;
}

.message-source__body {
  margin: 0;
  padding: var(--space-4) var(--space-5);
  overflow: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  font-size: var(--text-sm);
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
  color: var(--color-text);
}

/* Add user. Narrower than project settings: four stacked fields, not four sections. */
.modal.add-user {
  max-width: 420px;
}

.modal.add-user .settings-form {
  margin: 0;
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.modal.add-user .settings-form__row {
  flex-direction: column;
  align-items: stretch;
}

.modal.add-user .settings-form__row .field__input {
  min-width: 0;
}

/* Horizontal so Generate/Copy sit beside the temp password. flex-wrap drops
   the buttons under the field when the 420px modal cannot fit all three. */
.modal.add-user .settings-form__row--password {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
}

.modal.add-user .settings-form__row--password .field__input {
  flex: 1 1 12rem;
}

.settings-form__password-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  flex: 0 0 auto;
}

.settings-form__password-actions .btn {
  width: auto;
}

.modal.add-user .settings-form__row:last-child {
  flex-direction: row;
  justify-content: flex-end;
}

.duplicate-picker__hint {
  margin: 0;
  padding: var(--space-3) var(--space-5) 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.duplicate-picker__search {
  position: relative;
  margin: var(--space-3) var(--space-5) 0;
}

.duplicate-picker__search-icon {
  position: absolute;
  left: var(--space-3);
  top: 50%;
  transform: translateY(-50%);
  color: var(--color-muted);
  pointer-events: none;
}

.duplicate-picker__search-input {
  padding-left: 36px;
}

.duplicate-picker__results {
  list-style: none;
  margin: var(--space-3) 0 0;
  padding: 0 0 var(--space-4);
  max-height: 320px;
  overflow-y: auto;
}

.duplicate-picker__pick {
  margin: 0;
}

.duplicate-picker__candidate {
  appearance: none;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  width: 100%;
  margin: 0;
  padding: var(--space-3) var(--space-5);
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.duplicate-picker__candidate:hover,
.duplicate-picker__candidate:focus-visible {
  background: var(--color-accent-soft);
}

.duplicate-picker__subject {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}

.duplicate-picker__meta,
.duplicate-picker__time {
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.duplicate-picker__empty {
  margin: 0;
  padding: var(--space-4) var(--space-5) var(--space-5);
  color: var(--color-muted);
  font-size: var(--text-sm);
}

/* Rails wraps a field that failed validation in <div class="field_with_errors">, and nothing in
   this app styles it. Dropped into .settings-form__row — a flex row whose .field__input is the
   flex:1 item — the div becomes the flex item instead and the input shrinks to its intrinsic
   width, exactly when the admin needs to edit what they just typed. display:contents takes the
   wrapper out of the layout while leaving it in the DOM. Lives on .settings now that the name
   form is a settings pane, and on .add-user which is still a modal. */
.settings .field_with_errors,
.add-user .field_with_errors {
  display: contents;
}

.settings-account {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

.settings-account__identity {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
  flex: 1;
}

.settings-account__actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-shrink: 0;
}

.settings-account__randomize {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

.settings-account__randomize-icon {
  --icon-size: 14px;
}

/* The role tag sits in a flex column; hug its content instead of stretching full width. */
.settings-account .settings-row__badge {
  align-self: flex-start;
  margin-top: 2px;
}

.settings-account__profile,
.settings-account__password,
.settings-account__signature {
  margin-top: var(--space-4);
}

.settings-account__profile .settings-form,
.settings-account__password .settings-form {
  max-width: 24rem;
}

.settings-account__profile .field__input[readonly] {
  background: var(--color-surface-sunken);
  color: var(--color-muted);
  cursor: default;
}

.settings-account__signature .reply-template-editor {
  margin-top: var(--space-2);
}

/* Time zone (FRO-147). The trigger names the zone and its clock right now, so the setting is
   also where a reader confirms which zone every time is in. The menu holds ~150 zones: it scrolls
   inside itself and opens left-aligned, since the trigger sits at the start of the row. */
.settings-account__time-zone {
  margin-top: var(--space-4);
}

.settings-account__time-zone-picker {
  width: max-content;
  max-width: 100%;
  margin-top: var(--space-2);
}

.settings-account__time-zone-trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  max-width: 100%;
}

.settings-account__time-zone-icon {
  --icon-size: 16px;
  flex-shrink: 0;
}

.settings-account__time-zone-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.settings-account__time-zone-now {
  color: var(--color-muted);
  white-space: nowrap;
}

.settings-account__time-zone-menu {
  left: 0;
  right: auto;
  width: min(32rem, 100vw - 2 * var(--space-4));
  max-height: min(24rem, 60vh);
  overflow-y: auto;
}

.settings-mailbox {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--color-border);
}

.settings-mailbox:last-child {
  border-bottom: 0;
}

.settings-mailbox__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

.settings-mailbox__form .field {
  margin-bottom: var(--space-3);
}

.settings-mailbox__form .field:last-of-type {
  margin-bottom: var(--space-2);
}

.settings-appearance {
  margin-top: var(--space-5);
  padding: 0;
  border: 0;
}

.settings-appearance__hint {
  margin: 0 0 var(--space-3);
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.settings-appearance__options {
  display: flex;
  width: 100%;
  max-width: 22rem;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--color-surface-sunken);
}

.settings-appearance__option {
  position: relative;
  flex: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-sm);
  color: var(--color-muted);
  cursor: pointer;
}

.settings-appearance__option + .settings-appearance__option {
  border-left: 1px solid var(--color-border);
}

/* Stretched rather than clipped: a 1px radio fails Capybara's visible-field matchers,
   and a clipped control is also a worse hit target than the whole segment. */
.settings-appearance__input {
  appearance: none;
  position: absolute;
  inset: 0;
  margin: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
}

.settings-appearance__icon {
  --icon-size: 14px;
}

.settings-appearance__option:has(.settings-appearance__input:checked) {
  color: var(--color-accent-text);
  font-weight: 600;
  background: var(--color-accent-soft);
}

.settings-appearance__option:has(.settings-appearance__input:focus-visible) {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: -2px;
}

/* Picker v2 (FRO-155): the gallery, grouped Light / Dark. Every card paints from the real tokens of
   the theme it shows — the preview and the swatch strip carry data-theme="<id>", which re-runs that
   theme's mixin on them — so a new theme needs no card CSS of its own, and a card cannot drift from
   the theme it advertises. */
.settings-appearance__gallery {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin-top: var(--space-4);
}

.settings-appearance__group-title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-muted);
}

.settings-appearance__themes {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
  gap: var(--space-3);
}

.settings-theme {
  appearance: none;
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: 0;
  padding: var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  font: inherit;
  text-align: left;
  width: 100%;
  cursor: pointer;
}

.settings-theme:hover {
  border-color: var(--color-border-strong);
}

.settings-theme:has(.settings-appearance__input:checked),
.settings-appearance:has(#appearance_light:checked) .settings-theme[data-appearance-theme-param=light],
.settings-appearance:has(#appearance_dark:checked) .settings-theme[data-appearance-theme-param=dark] {
  border-color: var(--color-accent);
  box-shadow: inset 0 0 0 1px var(--color-accent);
}

.settings-theme:has(.settings-appearance__input:focus-visible),
.settings-theme:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 2px;
}

/* A miniature of the inbox: rail with the current item, a list row, the theme's display face and
   its primary button. Reads the theme's tokens, not the page's, because of data-theme. */
.settings-theme__preview {
  display: flex;
  height: 64px;
  overflow: hidden;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--theme-preview-ground);
  color: var(--color-text);
  font-family: var(--font-ui);
}

.settings-theme__preview-sidebar {
  flex: 0 0 24px;
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 6px 4px;
  background: var(--color-sidebar);
  border-right: 1px solid var(--color-border);
}

.settings-theme__preview-nav {
  height: 4px;
  border-radius: var(--radius-pill);
  background: var(--color-sidebar-muted);
  opacity: 0.5;
}

.settings-theme__preview-nav--current {
  background: var(--color-accent);
  opacity: 1;
}

.settings-theme__preview-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 5px 6px;
}

.settings-theme__preview-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 4px;
}

.settings-theme__preview-display {
  font-family: var(--font-display);
  font-size: 13px;
  font-weight: var(--font-display-weight, 700);
  line-height: 1;
}

.settings-theme__preview-button {
  padding: 2px 5px;
  border-radius: var(--radius-sm);
  background: var(--color-accent);
  color: var(--color-on-accent);
  font-size: 8px;
  font-weight: 700;
  line-height: 1.2;
}

.settings-theme__preview-row {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 4px;
  border-radius: 3px;
  background: var(--color-surface);
}

.settings-theme__preview-line {
  height: 3px;
  width: 80%;
  border-radius: var(--radius-pill);
  background: var(--color-text);
  opacity: 0.7;
}

.settings-theme__preview-line--muted {
  width: 55%;
  background: var(--color-muted);
}

.settings-theme__copy {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  min-width: 0;
}

.settings-theme__name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-text);
}

.settings-theme__swatches {
  display: inline-flex;
  flex: none;
}

.settings-theme__swatch {
  width: 12px;
  height: 12px;
  border-radius: var(--radius-pill);
  box-shadow: inset 0 0 0 1px var(--color-border-strong);
}

.settings-theme__swatch + .settings-theme__swatch {
  margin-left: -3px;
}

.settings-theme__swatch--surface {
  background: var(--color-surface);
}

.settings-theme__swatch--accent {
  background: var(--color-accent);
}

.settings-theme__swatch--text {
  background: var(--color-text);
}

/* Preference grid: one row per event, one column per channel. The first column is the event's
   name and the one-line of when it fires; the rest are a checkbox each, aligned to the header
   that names the channel. Fixed channel columns rather than 1fr so Email/Browser/SMS sit as a
   column even when one event's description wraps. */
.settings-notifications {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.settings-notifications__head,
.settings-notifications__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) repeat(3, 5.25rem);
  align-items: center;
  column-gap: var(--space-2);
}

.settings-notifications__head {
  padding: 0 var(--space-3);
  /* Same 1px box as the rows below, so Email / Browser / SMS sit on the column
     lines rather than drifting by the row's border width. */
  border: 1px solid transparent;
  color: var(--color-muted);
  font-size: var(--text-sm);
  font-weight: 600;
}

.settings-notifications__channel-head {
  text-align: center;
  line-height: 1.2;
}

.settings-notifications__coming-soon {
  display: block;
  font-weight: 400;
  font-size: 11px;
}

.settings-notifications__row {
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

.settings-notifications__cell {
  display: flex;
  justify-content: center;
  align-items: center;
  margin: 0;
}

/* accent-color rather than a rebuilt control: the native checkbox is the one input in this
   app that already reads correctly at this size, and all it wanted was the app's own blue. */
.settings-notifications__cell input {
  width: 15px;
  height: 15px;
  margin: 0;
  accent-color: var(--color-accent);
  cursor: pointer;
}

.settings-notifications__cell input:disabled {
  cursor: not-allowed;
  opacity: 0.45;
}

.settings-notifications__cell input:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 2px;
}

.settings-notifications__denied {
  margin: 0 0 var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-danger-soft);
  color: var(--color-danger);
  font-size: var(--text-sm);
}

@media (max-width: 640px) {
  /* The rail is already a full-width block on top of the page by here (see the 900px breakpoint),
     so the pane only needs to stop spending a desktop's padding on a phone's width. */
  .settings-appearance__themes {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .settings__detail {
    padding: var(--space-4);
  }
  .settings__flash {
    padding: 0 var(--space-4);
  }
  /* Stack each row's actions below its name/badge so they don't collide at narrow width. */
  .settings-row {
    flex-wrap: wrap;
  }
  .settings-row__main {
    flex-basis: 60%;
  }
  .settings-row__actions {
    width: 100%;
  }
  /* The access select's fixed width is what aligns the column at full width; here there is no
     room for it beside the project name, so the name takes the line and the select the next. */
  .settings-permissions__project {
    flex-wrap: wrap;
  }
  .settings-permissions__access {
    min-width: 100%;
  }
  /* A request-type row carries three actions — triage can be switched off per type — which is one
     more than fits beside a name and a thread count here. Left to wrap on its own, each row broke
     at a different point depending on how wide its count happened to be ("1 thread" vs "12
     threads"), so a lone Delete button hung under some rows and not others. Giving the actions
     their own line makes every row break the same way. A parts row carries two rather than three,
     and takes the same treatment so the two sections break identically. */
  .settings-row__head {
    flex-wrap: wrap;
  }
  .settings-row__head .settings-row__actions {
    justify-content: flex-start;
  }
  .settings-taxonomy__usage {
    margin-left: 0;
  }
}
/* ---------- Features (public explainer) ---------- */
/* A long public page, not the three-column shell. Tokens and primitives (.btn, .badge, .icon)
   are reused; the BEM block is new because login is a 360px card and settings live in a modal. */
.features {
  min-height: 100%;
  background: var(--color-surface-sunken);
}

.features .btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: auto;
}

.features__inner {
  max-width: 1080px;
  margin: 0 auto;
  padding: var(--space-6);
}

.features-header {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
}

.features-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding-top: var(--space-3);
  padding-bottom: var(--space-3);
}

.features-header__wordmark {
  font-size: var(--text-lg);
  font-weight: 700;
}

.features-hero {
  padding-top: var(--space-6);
}

.features-hero__eyebrow {
  margin: 0 0 var(--space-2);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-accent-text);
}

.features-hero__title {
  margin: 0 0 var(--space-4);
  font-size: var(--text-display);
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.03em;
}

.features-hero__lede {
  max-width: 40rem;
  margin: 0 0 var(--space-6);
  font-size: var(--text-lg);
  line-height: 1.5;
  color: var(--color-muted);
}

.features-jump {
  padding-top: 0;
}

.features-jump__inner {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding-top: var(--space-4);
  padding-bottom: var(--space-4);
}

.features-chip {
  display: inline-flex;
  align-items: center;
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  background: var(--color-surface);
  color: var(--color-muted);
  font-size: var(--text-sm);
  font-weight: 600;
}

.features-chip:hover {
  border-color: var(--color-accent);
  color: var(--color-accent-text);
  background: var(--color-accent-soft);
}

.features-glance {
  padding-bottom: var(--space-4);
}

.features-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--space-4);
}

.features-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  min-width: 0;
}

.features-card:hover {
  border-color: var(--color-border-strong);
  box-shadow: var(--shadow-md);
}

.features-card__icon {
  display: inline-flex;
  --icon-size: 20px;
  color: var(--color-accent-text);
}

.features-card__title {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: 700;
}

.features-card__copy {
  margin: 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
  line-height: 1.45;
  flex: 1;
}

.features-card__preview {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
  font-size: var(--text-sm);
  color: var(--color-muted);
}

.features-card__arrow {
  color: var(--color-accent-text);
  font-weight: 700;
}

.features-section {
  scroll-margin-top: 72px;
}

.features-section__inner {
  display: grid;
  grid-template-columns: 1.2fr 1fr;
  gap: var(--space-6);
  align-items: center;
  padding-top: var(--space-6);
  padding-bottom: var(--space-6);
}

.features-section--flip .features-section__copy {
  order: 2;
}

.features-section--flip .features-section__visual {
  order: 1;
}

.features-section__kicker {
  margin: 0 0 var(--space-2);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-accent-text);
}

.features-section__title {
  margin: 0 0 var(--space-4);
  font-size: var(--text-2xl);
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: -0.02em;
}

.features-section__copy p {
  margin: 0 0 var(--space-4);
  line-height: 1.55;
  color: var(--color-text);
}

.features-section__copy p:last-child {
  margin-bottom: 0;
}

.features-section__list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.features-section__list li {
  position: relative;
  padding-left: var(--space-4);
  line-height: 1.5;
  color: var(--color-text);
}

.features-section__list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.55em;
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: var(--color-accent);
}

.features-section__copy code,
.features-section__copy kbd {
  font-family: var(--font-mono);
  font-size: 0.92em;
  padding: 1px 5px;
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
}

.features-mock {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  overflow: hidden;
  pointer-events: none;
}

.features-mock--hero {
  display: grid;
  grid-template-columns: 148px 1fr 1.15fr;
}

.features-mock--split {
  display: grid;
  grid-template-columns: 148px 1fr;
}

.features-mock__pane {
  min-width: 0;
}

.features-mock__pane--list,
.features-mock__pane--nav {
  border-right: 1px solid var(--color-border);
}

.features-mock__pane--nav {
  padding: var(--space-3) 0;
  background: var(--color-surface-sunken);
}

.features-mock__nav-compose {
  margin: 0 var(--space-3) var(--space-2);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-accent);
  color: var(--color-on-accent);
  font-size: 12px;
  font-weight: 700;
  text-align: center;
}

.features-mock__nav-item,
.features-mock__nav-child {
  margin: 0;
  padding: 3px var(--space-4);
  font-size: 12px;
  font-weight: 600;
}

.features-mock__nav-child {
  padding-left: var(--space-6);
  font-weight: 500;
  color: var(--color-muted);
}

.features-mock__nav-item.is-current,
.features-mock__nav-child.is-current {
  color: var(--color-accent-text);
  background: var(--color-accent-soft);
}

.features-mock__nav-heading {
  margin: var(--space-3) var(--space-4) var(--space-1);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-muted);
}

.features-mock__thread-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-3);
}

.features-card__preview .features-mock__source {
  --icon-size: 16px;
}

.features-mock__pane-label {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  font-size: 12px;
  font-weight: 700;
  color: var(--color-muted);
  border-bottom: 1px solid var(--color-border);
  background: var(--color-surface-sunken);
}

.features-mock__row {
  display: grid;
  gap: 2px var(--space-2);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--color-border);
  font-size: var(--text-sm);
}

.features-mock__row:last-child {
  border-bottom: 0;
}

.features-mock__row.is-current {
  background: var(--color-accent-soft);
}

.features-mock__row:has(.features-mock__lead) {
  grid-template-columns: 1fr auto auto;
  grid-template-areas: "lead time source" "subject subject subject" "snippet snippet snippet";
}

.features-mock__row:has(> .badge) {
  grid-template-columns: auto 1fr auto;
  grid-template-areas: "badge who time" "subject subject subject";
}

.features-mock__row > .badge {
  grid-area: badge;
  font-size: 11px;
}

.features-mock__lead {
  grid-area: lead;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
}

.features-mock__project {
  font-size: 11px;
}

.features-mock__source {
  grid-area: source;
  display: inline-flex;
  align-items: center;
  color: var(--color-muted);
  --icon-size: 14px;
}

.features-mock__who {
  grid-area: who;
  color: var(--color-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.features-mock__time {
  grid-area: time;
  color: var(--color-muted);
  font-size: 12px;
}

.features-mock__subject {
  grid-area: subject;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
}

.features-mock__snippet {
  grid-area: snippet;
  color: var(--color-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.features-mock__views {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4) 0;
}

.features-mock__view {
  padding: 2px var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  color: var(--color-muted);
  font-size: 12px;
  font-weight: 600;
}

.features-mock__view.is-current {
  color: var(--color-accent-text);
  border-color: var(--color-accent);
  background: var(--color-accent-soft);
}

.features-mock__tabs {
  display: flex;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-4) 0;
  border-bottom: 1px solid var(--color-border);
}

.features-mock__tab {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-1);
  padding-bottom: var(--space-2);
  border-bottom: 2px solid transparent;
  color: var(--color-muted);
  font-size: var(--text-sm);
  font-weight: 600;
}

.features-mock__tab.is-current {
  color: var(--color-text);
  border-bottom-color: var(--color-accent);
}

.features-mock__tab-count {
  font-size: 12px;
  color: var(--color-muted);
}

.features-mock__tab.is-current .features-mock__tab-count {
  color: var(--color-accent-text);
}

.features-mock__steps {
  margin: 0;
  padding: var(--space-4);
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  counter-reset: step;
}

.features-mock__step {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  counter-increment: step;
}

.features-mock__step::before {
  content: counter(step);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  flex: 0 0 22px;
  border-radius: 999px;
  background: var(--color-surface-sunken);
  color: var(--color-muted);
  font-size: 11px;
  font-weight: 700;
}

.features-mock__step.is-current {
  background: var(--color-accent-soft);
  font-weight: 600;
}

.features-mock__step.is-current::before {
  background: var(--color-accent);
  color: var(--color-on-accent);
}

.features-mock__step-note {
  margin-left: auto;
  color: var(--color-muted);
  font-size: 12px;
  font-weight: 400;
}

.features-mock--rule,
.features-mock--linear,
.features-mock--access,
.features-mock--knowledge,
.features-mock--analytics {
  padding: var(--space-5);
}

.features-mock__rule-label,
.features-mock__access-project {
  margin: 0 0 var(--space-3);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-muted);
}

.features-mock__rule-if,
.features-mock__rule-then {
  margin: 0 0 var(--space-3);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-base);
}

.features-mock__rule-note {
  margin: var(--space-4) 0 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.features-mock__prefs {
  padding: var(--space-3) var(--space-4) var(--space-4);
  border-top: 1px solid var(--color-border);
  background: var(--color-surface-sunken);
}

.features-mock__prefs-head,
.features-mock__prefs-row {
  display: grid;
  grid-template-columns: 1fr 52px 52px;
  gap: var(--space-2);
  align-items: center;
  font-size: var(--text-sm);
}

.features-mock__prefs-head {
  color: var(--color-muted);
  font-weight: 600;
  margin-bottom: var(--space-2);
}

.features-mock__prefs-row {
  padding: var(--space-1) 0;
}

.features-mock__check {
  width: 14px;
  height: 14px;
  justify-self: center;
  border: 1px solid var(--color-border-strong);
  border-radius: 3px;
  background: var(--color-surface);
}

.features-mock__check.is-on {
  background: var(--color-accent);
  border-color: var(--color-accent);
}

.features-mock--thread {
  padding: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.features-mock__bubble {
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  font-size: var(--text-sm);
  line-height: 1.45;
}

.features-mock__bubble p {
  margin: 0;
}

.features-mock__bubble-meta {
  margin: 0 0 var(--space-2) !important;
  color: var(--color-muted);
  font-size: 12px;
  font-weight: 600;
}

.features-mock__bubble--comment {
  background: var(--color-comment-soft);
  border-color: var(--color-comment-border);
}

.features-mock__bubble--out {
  background: var(--color-accent-soft);
  border-color: color-mix(in srgb, var(--color-accent) 28%, var(--color-accent-soft));
}

.features-mock--composer {
  padding: var(--space-4);
}

.features-mock__recipients {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin-bottom: var(--space-3);
  font-size: var(--text-sm);
}

.features-mock__recipient {
  margin: 0;
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.features-mock__recipient > span:first-child {
  flex: 0 0 2.5rem;
  color: var(--color-muted);
  font-size: 12px;
  font-weight: 600;
}

.features-mock__recipient em {
  margin-left: auto;
  font-style: normal;
  color: var(--color-accent-text);
  font-size: 12px;
  font-weight: 600;
}

.features-mock__composer-footer {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.features-mock__tool {
  display: inline-flex;
  color: var(--color-muted);
  --icon-size: 16px;
}

.features-mock__send {
  margin-left: auto;
  padding: 2px var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-accent);
  color: var(--color-on-accent);
  font-size: 12px;
  font-weight: 700;
}

.features-mock__send-later {
  font-size: 12px;
  font-weight: 600;
  color: var(--color-muted);
}

.features-mock__presence {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.features-mock__chip {
  display: inline-flex;
  align-items: center;
  padding: 1px var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  background: var(--color-surface-sunken);
  font-size: 11px;
  font-weight: 600;
}

.features-mock__banner {
  margin: 0;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-accent-soft);
  font-size: 12px;
  font-weight: 600;
}

.features-mock__quote-toggle {
  margin-top: var(--space-2) !important;
  color: var(--color-accent-text);
  font-size: 12px;
  font-weight: 600;
}

.features-mock__activity {
  margin: 0;
  color: var(--color-muted);
  font-size: 12px;
}

.features-mock--knowledge .features-mock__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

.features-mock--knowledge .features-mock__rule-label:not(:first-child) {
  margin-top: var(--space-4);
}

.features-mock__bars {
  display: flex;
  align-items: flex-end;
  gap: 6px;
  height: 72px;
  margin-bottom: var(--space-4);
}

.features-mock__bar {
  flex: 1;
  height: 100%;
  display: flex;
  align-items: flex-end;
  gap: 2px;
}

.features-mock__bar::before,
.features-mock__bar::after {
  content: "";
  flex: 1;
  border-radius: 2px 2px 0 0;
}

.features-mock__bar::before {
  height: var(--in);
  background: var(--color-accent);
}

.features-mock__bar::after {
  height: var(--out);
  background: color-mix(in srgb, var(--color-accent) 35%, var(--color-surface-sunken));
}

.features-mock__stats {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-2) var(--space-4);
}

.features-mock__stats p {
  margin: 0;
  display: flex;
  justify-content: space-between;
  font-size: var(--text-sm);
}

.features-mock__stats span {
  color: var(--color-muted);
}

.features-mock__account {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-muted);
}

.features-mock__modes {
  display: flex;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}

.features-mock__mode {
  padding: 2px var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
  color: var(--color-muted);
}

.features-mock__mode.is-current {
  color: var(--color-accent-text);
  border-color: var(--color-accent);
  background: var(--color-accent-soft);
}

.features-mock__editor {
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  line-height: 1.5;
}

.features-mock__editor p {
  margin: 0 0 var(--space-2);
}

.features-mock__editor p:last-child {
  margin-bottom: 0;
}

.features-mock__caret {
  color: var(--color-muted);
  border-left: 2px solid var(--color-accent);
  padding-left: var(--space-2);
}

.features-mock__slash {
  margin-top: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
}

.features-mock__slash-row {
  margin: 0;
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-sm);
  border-bottom: 1px solid var(--color-border);
}

.features-mock__slash-row:last-child {
  border-bottom: 0;
}

.features-mock__slash-row.is-current {
  background: var(--color-accent-soft);
}

.features-mock__slash-row code {
  font-family: var(--font-mono);
  font-size: 12px;
  margin-right: var(--space-2);
}

.features-mock__ticket {
  margin: 0 0 var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--color-accent-text);
}

.features-mock--linear .features-mock__subject {
  display: block;
  margin: 0 0 var(--space-3);
  font-size: var(--text-lg);
  white-space: normal;
}

.features-mock__chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

.features-mock__linear-line {
  margin: 0;
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
  font-size: var(--text-sm);
  color: var(--color-muted);
}

.features-mock__access-levels {
  display: flex;
  gap: var(--space-2);
}

.features-mock__access-level {
  flex: 1;
  text-align: center;
  padding: var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-muted);
}

.features-mock__access-level.is-current {
  color: var(--color-accent-text);
  border-color: var(--color-accent);
  background: var(--color-accent-soft);
}

.features-close {
  padding-bottom: var(--space-6);
}

.features-close__inner {
  text-align: center;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: var(--space-6);
}

.features-close__title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-2xl);
  font-weight: 700;
}

.features-close__copy {
  margin: 0 0 var(--space-5);
  color: var(--color-muted);
}

.features-footer {
  border-top: 1px solid var(--color-border);
}

.features-footer p {
  margin: 0;
  padding-top: 0;
  padding-bottom: var(--space-5);
  font-size: 12px;
  color: var(--color-muted);
}

@media (prefers-reduced-motion: no-preference) {
  html:has(.features) {
    scroll-behavior: smooth;
  }
}
@media (max-width: 900px) {
  .features-hero__title {
    font-size: var(--text-2xl);
  }
  .features-grid {
    grid-template-columns: 1fr 1fr;
  }
  .features-section__inner {
    grid-template-columns: 1fr;
    gap: var(--space-5);
  }
  .features-section--flip .features-section__copy,
  .features-section--flip .features-section__visual {
    order: 0;
  }
  .features-mock--hero,
  .features-mock--split {
    grid-template-columns: 1fr;
  }
  .features-mock--hero .features-mock__pane--nav {
    display: none;
  }
  .features-mock__pane--list,
  .features-mock__pane--nav {
    border-right: 0;
    border-bottom: 1px solid var(--color-border);
  }
}
@media (max-width: 640px) {
  .features-grid {
    grid-template-columns: 1fr;
  }
  .features__inner {
    padding: var(--space-5);
  }
}

/*# sourceMappingURL=/assets/application-4825f80f.css.map */
