/* ===== Global cursor & selection rules - AUTHORITATIVE ============
   ONE place that owns cursor + selection across the app. Five rules,
   ordered weakest-to-strongest. ANY new "cursor: text" or
   "cursor: pointer" rule elsewhere in this file - or in any other
   stylesheet - is a bug; extend the lists below instead.

   The user has had to ask repeatedly for the I-beam to stop appearing
   on saved notes / message bodies / read content. Read this comment
   before editing this block. */

/* (1) Inheritance + default floor. CSS `cursor` doesn't inherit on
   its own - every element falls back to `auto`, which the browser
   resolves per-element (text I-beam over text nodes). `* {cursor:inherit}`
   forces inheritance so a child of `<a>` (whose rule sets pointer)
   actually shows the pointer; a child of `body` shows the default
   arrow. Each rule below targets its container; descendants follow
   via this inheritance. DO NOT change `inherit` to `default` here -
   that breaks pointer-over-link-text and arrow-over-button-icon. */
* { cursor: inherit; }
body { cursor: default; }

/* (2) Selection: app chrome is NOT selectable. Read-only prose IS
   (so users can copy). Editable inputs are too (so caret can land).
   NEVER on iOS / iPadOS: WebKit there refuses text selection inside a
   textarea or an input whose ANCESTOR says none - the caret lands but
   the long-press handles never appear and the page jerks instead
   (owner, 2026-09-16, the issue comment composer on an iPad: "text
   selection is not working and make UI jerking"). The layout's head
   script marks an Apple touch device `html.bf-ios` before the body
   exists, so the rule stands everywhere except there; a finger selects
   chrome text only by a deliberate long-press anyway. */
html:not(.bf-ios) body { user-select: none; -webkit-user-select: none; }

/* (3) Pointer cursor - interactive controls. */
a, button, [role="button"], summary, label[for], select,
.btn, .nav-link, .dropdown-item, .dropdown-toggle,
[data-bs-toggle], [data-coworker-trigger], [data-user-profile-id],
[data-reply-target],
input[type="checkbox"], input[type="radio"], input[type="file"],
input[type="submit"], input[type="button"], input[type="reset"],
input[type="range"], input[type="color"] {
    cursor: pointer;
}

/* (4) Text caret - ONLY on truly editable surfaces. Type-narrowed so
   `<input type="checkbox">` etc. stay on the pointer rule above.
   If you're tempted to add a class here that names CONTENT
   (e.g. `.note-text`, `.msg-body`, `.is-prose`, anything ending in
   `-text` / `-body` / `-content`) - STOP. That class belongs in
   block (5), not here. */
input:not([type]),
input[type="text"], input[type="email"], input[type="password"],
input[type="search"], input[type="url"], input[type="tel"],
input[type="number"], input[type="date"], input[type="time"],
input[type="datetime-local"], input[type="month"], input[type="week"],
textarea, [contenteditable="true"], [contenteditable=""] {
    cursor: text;
    user-select: text;
    -webkit-user-select: text;
}

/* (5) Read-only prose surfaces - selectable, NEVER I-beam. The
   `!important` blast wall applies ONLY to the surface element, not
   its descendants. Children inherit via rule (1)'s `* {cursor:inherit}`
   chain - so plain text shows the default arrow, while interactive
   children (`<a>`, `<button>`, `[role=button]`, `[data-...trigger]`,
   inputs) keep their own cursor rules from blocks (3) and (4) above.
   Adding `*` to this selector list breaks pointer-on-link-inside-note
   and pointer-on-clickable-chip-inside-note; do NOT do it. */
.is-prose,
.message-body,
.note, .note-text, .note-body, .note-thread,
.email-body,
.msg-body, .msg-body-host,
.msg-subject, .msg-headers,
.issues-detail-title-text,
.bf-drive-inline-notice, .bf-drive-sa-email,
pre, code, kbd, samp, blockquote {
    cursor: default !important;
    user-select: text;
    -webkit-user-select: text;
}

button:disabled, .btn:disabled,
input:disabled, select:disabled, textarea:disabled,
[aria-disabled="true"] {
    cursor: not-allowed;
}

/* Chips inside chip-input must allow click-to-remove without
   accidentally selecting their text. */
.chip-input .chip-input-chip { user-select: none; -webkit-user-select: none; }

/* `.bf-light-island` rides the SAME declaration block as `:root`, it is
   not a copy of it. A subtree carrying that class therefore holds the
   light palette on its own element, which beats the inherited dark one
   whatever the theme is - and a palette edit here can never leave the
   island behind on last season's colours.

   Who needs one: foreign HTML we render but did not author. A sender's
   email that declares colours was drawn against a white page, so we
   give it a white page instead of letting its black ink land on our
   dark card (Services/Mail/Rendering/EmailBodySheet.cs). Because the
   island carries TOKENS rather than fixed colours, everything inside it
   follows - our own chrome by reading the vars, the email's shadow tree
   because custom properties pierce the shadow boundary. */
:root,
.bf-light-island {
    /* ── Burofolk brand tokens ────────────────────────────────
       Single source of truth for the platform's brand identity.
       The three brand colors come straight from the logo file
       (wwwroot/img/brand/burofolk-icon.svg): cyan top band,
       violet middle band, pink bottom band. Every accent /
       button / focus ring / hero gradient elsewhere in the
       platform points at THESE tokens so a brand refresh is one
       edit, not a thousand-file find-and-replace. */
    --brand-cyan:   #06B6D4;
    --brand-cyan-strong: #0891B2;
    --brand-cyan-bg:    #ecfeff;
    --brand-violet: #8B5CF6;
    --brand-violet-strong: #7C3AED;
    --brand-violet-bg:     #f5f3ff;
    --brand-pink:   #EC4899;
    --brand-pink-strong:   #DB2777;
    --brand-pink-bg:       #fdf2f8;
    --brand-gradient: linear-gradient(135deg, var(--brand-cyan) 0%, var(--brand-violet) 50%, var(--brand-pink) 100%);
    /* Soft hero / hover variant - same hue arc, lower saturation so
       a brand-gradient block reads as a subtle warm wash, not a
       loud paint. Use for card hovers, empty-state backdrops,
       header accents. */
    --brand-gradient-soft: linear-gradient(135deg,
        rgba(6, 182, 212, 0.10) 0%,
        color-mix(in srgb, var(--primary) 12%, transparent) 50%,
        rgba(236, 72, 153, 0.10) 100%);

    /* Notion / Wikipedia parity: the app canvas is WHITE, not a
       pastel slate. Cards and other surfaces sit directly on
       white with subtle borders for separation; `--surface-2`
       stays as the lone tinted band for inputs / code blocks /
       row-hover states that genuinely need bg contrast. The old
       `#f8fafc` made every page read as "tinted draft"; clean
       white is the long-form reading standard. */
    --bg: #ffffff;
    --surface: #ffffff;
    --surface-2: #f1f5f9;
    --border: #e2e8f0;
    --border-strong: #cbd5e1;
    /* Hairline separators (the cockpit family's rails, drawer
       heads, saved-view dividers): softer than --border, defined
       canonically so consumers follow the theme switch instead of
       painting their per-rule fallbacks. */
    --bf-hairline: rgba(15, 23, 42, .08);
    /* The view frame (2026-07-29): every _Layout page renders inside ONE
       framed card - .app-main itself - inset from the chrome by the same
       gap on all four sides and floating on this canvas tint. The white
       doctrine above still holds where it matters: content keeps reading
       on white; the tint shows only in the thin moat between the chrome
       hairlines and the frame, never behind text. All frame geometry
       rides these four tokens so the rail composition (app-shell.css)
       and the phone collapse flip the SAME knobs - no per-surface magic
       numbers. Pinned by AppFrameTests. */
    /* THE SURFACE LADDER (owner, 2026-09-24, option C: "everything must be
       perfect"): ONE slate hue in three steps, the only background tones a
       page may use. L0 the chrome (strip, rail, footer and the moat around
       the frame), L1 the page (the frame a migrated page opens on), L2 the
       card (lists, tables, editors, forms: --surface). A card is its --border
       edge, never a resting shadow. The frame paints --bf-page (L1) on every
       page; anything that must match the page behind it (a sticky head, a
       fade) reads --bf-page, never --surface. Hover and selection are ink
       washes ON the tone they sit on. */
    --bf-l0: #edf0f5;
    --bf-l1: #f5f7fa;
    --bf-l2: var(--surface);
    --bf-edge: var(--border);
    --app-canvas: var(--bf-l0);
    --app-frame-gap: 10px;
    /* The footer's height, so a layer that must stop at the frame's bottom
       edge (the preview drawer) can compute it. */
    --app-footer-h: 36px;
    --app-frame-radius: 8px;
    /* macOS-window float: a hairline ring, a tight contact shadow and
       one LARGE soft low-alpha throw. The big blur at whisper opacity
       is what sells "floating", never a darker edge. */
    --app-frame-shadow:
        0 0 0 1px rgba(15, 23, 42, .05),
        0 2px 6px rgba(15, 23, 42, .05),
        0 24px 64px -32px rgba(15, 23, 42, .28);
    /* Chrome controls hover with translucent ink (the tenant switcher's
       color-mix idiom): an opaque --surface-2 fill is invisible on the
       canvas tint (#f1f5f9 on #f3f5f8). One token serves both themes -
       --text flips, so the mix flips with it. AppChromeCanvasTests. */
    --app-chrome-hover: color-mix(in srgb, var(--text) 7%, transparent);
    --text: #0f172a;
    /* Secondary ink sits one slate step darker than the tailwind-ish
       defaults (500->600, 400->500): muted is real reading text
       (7:1 on white), subtle is metadata that must still clear AA
       (4.8:1). Anything paler than --text-subtle is decoration and
       should be a border token, not a text token. */
    --text-muted: #475569;
    --text-subtle: #64748b;
    /* `--bf-text*` aliases are used by the home dashboard widget
       CSS (`var(--bf-text, #0f172a)` etc). They were NEVER
       declared anywhere in the tree, so every consumer fell
       through to the hardcoded fallback color - which made the
       fallback identical in light + dark mode (dark slate text).
       In light mode it read fine on white; in dark mode the dark
       slate disappeared against the dark canvas (widget titles,
       waiting subjects, activity timestamps all invisible).
       Defining them here AND in the dark-theme block below maps
       them to the canonical `--text*` tokens so they switch
       automatically with the theme. */
    --bf-text:        var(--text);
    --bf-text-muted:  var(--text-muted);
    --bf-text-subtle: var(--text-subtle);
    /* Primary semantic = INK (owner directive 2026-08-07: the violet accent
       is retired - purple borders / fills / text are the old fashion). The
       interactive layer is monochrome: ink resolves per theme through
       var(--text), the wash is a quiet ink mix. Color now belongs ONLY to
       identity (workspace --bf-color, label chips, avatars, folder palette)
       and status semantics (--danger/--warning/--success, cyan/pink data
       accents) - never to controls. */
    --primary: var(--text);
    --primary-hover: color-mix(in srgb, var(--text) 84%, var(--surface));
    --primary-bg: color-mix(in srgb, var(--text) 6%, transparent);
    /* Brand accent companions (cyan + pink). Surfaces that want a
       second tone (success-emphasis, count badges, hero corners)
       reach for these instead of inventing new hex codes. */
    --accent-cyan: var(--brand-cyan);
    --accent-cyan-bg: var(--brand-cyan-bg);
    --accent-pink: var(--brand-pink);
    --accent-pink-bg: var(--brand-pink-bg);
    /* The secondary accent follows the same ink policy (it painted links and
       comment / saved-view affordances indigo - retired with the violet). */
    --accent: var(--text);
    /* Monospace stack for code / key fields (theme-independent). */
    --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    /* The three state tones ARE the palette's red, orange and green (owner,
       2026-09-10: the amber and emerald read "sad" - "more contrasted,
       modern/premium"); their -bg is the 12% wash on the canvas. */
    --danger: var(--sys-red);
    --danger-bg: #ffebea;
    --warning: var(--sys-orange);
    --warning-bg: #fff2e0;
    /* Amber TEXT that reads on the canvas (the orange itself is too light for words). */
    --warning-text: #a15c00;
    --success: var(--sys-green);
    --success-bg: #e7f8eb;
    /* THE PALETTE (owner, 2026-09-06: "we like Apple colours… really
       contrasted, like on iPad, no pastel anywhere"): Apple's system
       colours, light here and lifted for the dark canvas below. Every kind
       and every state word owns ONE of them, distinct from its neighbours;
       the -wash is the 12% tint for the rare ground. Kinds and states only -
       never a control (primaries stay ink). */
    --sys-blue: #007aff;
    --sys-green: #34c759;
    --sys-indigo: #5856d6;
    --sys-orange: #ff9500;
    --sys-pink: #ff2d55;
    --sys-purple: #af52de;
    --sys-red: #ff3b30;
    --sys-teal: #30b0c7;
    --sys-yellow: #ffcc00;
    --sys-mint: #00c7be;
    --sys-cyan: #32ade6;
    --sys-brown: #a2845e;
    --sys-gray: #8e8e93;
    --hue-docs: var(--sys-blue);
    --hue-docs-wash: rgba(0, 122, 255, .12);
    --hue-mail: var(--sys-teal);
    --hue-mail-wash: rgba(48, 176, 199, .12);
    --hue-tickets: var(--sys-red);
    --hue-tickets-wash: rgba(255, 59, 48, .12);
    --hue-wiki: var(--sys-green);
    --hue-wiki-wash: rgba(52, 199, 89, .12);
    --hue-contacts: var(--sys-brown);
    --hue-contacts-wash: rgba(162, 132, 94, .12);
    --hue-calendar: var(--sys-orange);
    --hue-calendar-wash: rgba(255, 149, 0, .12);
    --hue-people: var(--sys-mint);
    --hue-people-wash: rgba(0, 199, 190, .12);
    --hue-ai: var(--sys-purple);
    --hue-ai-wash: rgba(175, 82, 222, .12);
    --hue-star: var(--sys-yellow);
    --hue-star-wash: rgba(255, 204, 0, .12);
    --hue-home: var(--sys-indigo);
    --hue-home-wash: rgba(88, 86, 214, .12);
    --hue-search: var(--sys-gray);
    --hue-search-wash: rgba(142, 142, 147, .12);
    --hue-work: var(--sys-orange);
    --hue-work-wash: rgba(255, 149, 0, .12);
    --hue-activity: var(--sys-pink);
    --hue-activity-wash: rgba(255, 45, 85, .12);
    --hue-settings: var(--sys-gray);
    --hue-settings-wash: rgba(142, 142, 147, .12);
    /* Informational blue, the fourth status alongside danger / warning /
       success. It carries "queued, in flight, awaiting a step" - the
       publication mark's to-publish state - where success would claim the
       work is done and warning would claim something is wrong. */
    --info: var(--sys-blue);
    --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.04), 0 1px 3px rgba(15, 23, 42, 0.06);
    --shadow: 0 4px 6px -2px rgba(15, 23, 42, 0.05), 0 10px 15px -3px rgba(15, 23, 42, 0.08);
    --shadow-lg: 0 12px 24px -6px rgba(15, 23, 42, 0.10), 0 24px 40px -8px rgba(15, 23, 42, 0.12);
    /* Default avatar paint - the satin-finish 135deg gradient at the
       brand-purple hue (263). This `:root`-scoped value is what
       elements with NO `--avatar-hue` of their own get. Per-user
       colours come from the `.bf-avatar-letter` rule below, which
       writes `linear-gradient(... hsl(var(--avatar-hue), ...))`
       directly so the per-user variable is resolved at the consumer
       element (CSS var() inside a custom property gets resolved at
       the property's DECLARATION site - `:root` here - so we can't
       parameterise the gradient via a CSS variable; the actual
       gradient string has to live in the rule that paints elements
       carrying `--avatar-hue`). */
    /* Stops widened (lightness 68% → 42%, saturation bumped) so the
       satin-finish reads as a gradient even at the smallest avatar
       sizes (16-24px) where the original 62→48 transition collapsed
       to a flat color under aggressive browser downscaling / low
       zoom. Same shape across sizes, no per-size variant needed. */
    --avatar-bg: linear-gradient(135deg, hsl(263, 72%, 68%), hsl(263, 64%, 42%));
    --avatar-color: #fff;
}

/* The island paints itself with the tokens it just claimed, so opting in
   is one class and nothing else. `color-scheme: light` is the third
   piece and the least obvious one: it stops a `prefers-color-scheme:
   dark` block INSIDE the island - a sender's own dark-mode CSS, ours
   never - from firing and putting white ink on this white page. */
.bf-light-island {
    color-scheme: light;
    background: var(--surface);
    color: var(--text);
}

/* ── NO gradient utility classes ──────────────────────────────
   Earlier builds shipped .brand-text-gradient, .brand-accent-bar
   and .brand-soft-fill as opt-in chrome flourishes. They were
   removed because the platform's design language is minimal:
   identity comes through the brand-violet color of accents and
   buttons, NOT through chrome flourishes. The --brand-gradient
   token is kept (as a value) for theming surfaces only - any
   consumer that paints it on a button / row / nav element is in
   violation of the design system. See CONVENTIONS.md "Design
   system" section. */

/* ───────────────────────── Avatar letter paint ─────────────────────────
   THE single source of truth for letter-bubble fill across every avatar
   surface in Burofolk. The canonical helpers (`Html.UserAvatar` in C#,
   `Burofolk.renderAvatar*` in JS) ALWAYS add the `bf-avatar-letter`
   class to letter spans they emit, so this one rule owns:
     - the satin-finish 135deg gradient (per-user when `--avatar-hue`
       is set inline; brand-purple fallback otherwise),
     - the white glyph colour.
   Per-call-site classes (`coworker-avatar`, `collab-avatar`,
   `nav-presence-avatar`, etc.) own size + shape + ring, never colour.
   To change how letter avatars look platform-wide, edit THIS rule. */
/* THE guest mark (owner, 2026-10-23): a guest of the organisation the page is in, on every avatar the ONE renderer
   draws (Html.UserAvatar, Burofolk.renderAvatar). A dashed hairline ring just outside the picture - an outline, so
   it takes no room, moves nothing and is never cut by a parent that clips; the word rides the tooltip. */
.bf-avatar-guest {
    outline: 1.5px dashed color-mix(in srgb, var(--text) 55%, transparent);
    outline-offset: 1.5px;
}

.bf-avatar-letter {
    /* `!important` because this rule is the canonical paint for
       letter bubbles - it MUST win over any per-call-site class
       (`.coworker-avatar`, `.collab-avatar`, ...) that later in
       the file declares its own `background: var(--avatar-bg)`.
       Without `!important`, source-order makes the per-call-site
       rule win and we ship the static brand-purple fallback for
       every user (the all-purple regression). The single-source-
       of-truth contract requires this one rule own colour.

       Lightness range widened to 68% → 42% (was 62% → 48%) so the
       gradient is still visible at the smallest avatar sizes -
       under aggressive downscaling or low browser zoom the old
       14-point delta collapsed to a flat color. The new 26-point
       delta plus the inset highlight + soft drop below give the
       bubble a satin-finish look that holds at 18px and stays
       clean at 64px. */
    background: linear-gradient(135deg,
        hsl(var(--avatar-hue, 263), 72%, 68%),
        hsl(var(--avatar-hue, 263), 64%, 42%)) !important;
    color: #fff !important;
    /* Center the glyph regardless of consumer styling so a forgotten
       `display:flex` on a per-call-site class can't ship a
       top-left-anchored letter at small sizes. Locked here means
       every consumer renders identically without copy-paste. */
    display: inline-flex !important;
    align-items: center !important;
    justify-content: center !important;
    /* Glyph weight + tracking + shadow chosen to stay legible from
       16px through 64px. The text-shadow gives the white letter a
       hairline of separation from the lighter top-left of the
       gradient where contrast is lowest. */
    font-weight: 600;
    letter-spacing: 0;
    text-shadow: 0 1px 1px rgba(15, 23, 42, 0.18);
    /* Inset highlight (top) + inset drop (bottom) define the bubble
       edge against light backgrounds at small sizes where the
       gradient alone reads as flat. Both inset so they don't bleed
       past the consumer's clipping radius. Subtle on purpose - the
       bubble is the message, not the chrome. */
    box-shadow:
        inset 0 1px 0 hsla(0, 0%, 100%, 0.22),
        inset 0 -1px 0 hsla(220, 30%, 12%, 0.18);
    /* Keep the glyph crisp on retina + downscale paths. */
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}
/* When the consumer renders the photo path, the same parent element
   receives the `<img>` child instead of a letter. The img already
   covers the bubble; strip the inset highlights so they don't bleed
   over the photo. The avatar-img class is added by both helpers on
   the photo path (Html.UserAvatar + Burofolk.renderAvatar). */
.bf-avatar-letter:has(> .avatar-img),
.avatar-img {
    box-shadow: none;
    text-shadow: none;
}
/* A PERSON IS ROUND, everywhere (owner, 2026-10-24: "use rounded avatar everywhere. we don't want square one. Rounded is
   our standard"). The ONE renderer's two forms - the photo and the letter bubble - own their shape here as they own their
   colour above: a call-site class sizes an avatar, it never squares it. A space's icon is not an avatar (its own class). */
.bf-avatar-letter, img.avatar-img { border-radius: 50% !important; }
/* The DOM form's photo (Burofolk.renderAvatarInto) fills its bubble: the whole slot, cropped round, never at its own ratio. */
.avatar-img-fill { display: block; width: 100%; height: 100%; object-fit: cover; border-radius: inherit; flex: 0 0 auto; }

html[data-theme="dark"] {
    --bg: #0b1220;
    --surface: #0f172a;
    --surface-2: #1e293b;
    --border: #1f2937;
    --border-strong: #334155;
    --bf-hairline: rgba(148, 163, 184, .16);
    /* Frame canvas: a step DEEPER than --bg so the --surface card still
       reads as lifted; the ring does the separating work shadows can't
       do on dark. */
    /* Dark: the same ladder inverted - the chrome deepest, the card lightest. */
    --bf-l0: #070b14;
    --bf-l1: #0b1120;
    --app-canvas: var(--bf-l0);
    --app-frame-shadow:
        0 0 0 1px rgba(148, 163, 184, .10),
        0 24px 70px -34px rgba(0, 0, 0, .75);
    --text: #e5e7eb;
    --text-muted: #94a3b8;
    --text-subtle: #64748b;
    /* Dark-mode brand: same hues, lighter so they pop on the dark
       canvas. The semantic tokens still point at brand-* vars so
       a future palette change is one block. */
    --brand-cyan:   #22d3ee;
    --brand-cyan-strong: #06b6d4;
    --brand-cyan-bg:     #083344;
    --brand-violet: #a78bfa;
    --brand-violet-strong: #8b5cf6;
    --brand-violet-bg:     #2e1065;
    --brand-pink:   #f472b6;
    --brand-pink-strong:   #ec4899;
    --brand-pink-bg:       #500724;
    --brand-gradient: linear-gradient(135deg, var(--brand-cyan) 0%, var(--brand-violet) 50%, var(--brand-pink) 100%);
    --brand-gradient-soft: linear-gradient(135deg,
        rgba(34, 211, 238, 0.10) 0%,
        color-mix(in srgb, var(--primary) 12%, transparent) 50%,
        rgba(244, 114, 182, 0.10) 100%);

    /* Ink accent, same policy as light: resolves through the dark --text. */
    --primary: var(--text);
    --primary-hover: color-mix(in srgb, var(--text) 84%, var(--surface));
    --primary-bg: color-mix(in srgb, var(--text) 8%, transparent);
    --accent-cyan: var(--brand-cyan);
    --accent-cyan-bg: var(--brand-cyan-bg);
    --accent-pink: var(--brand-pink);
    --accent-pink-bg: var(--brand-pink-bg);
    --accent: var(--text);
    --danger: var(--sys-red);
    --danger-bg: #3f1a18;
    --warning: var(--sys-orange);
    --warning-bg: #3f2a0a;
    --warning-text: var(--sys-orange);
    --success: var(--sys-green);
    --success-bg: #0f3320;
    /* The palette on the dark canvas: Apple's dark-mode system colours. */
    --sys-blue: #0a84ff;
    --sys-green: #30d158;
    --sys-indigo: #5e5ce6;
    --sys-orange: #ff9f0a;
    --sys-pink: #ff375f;
    --sys-purple: #bf5af2;
    --sys-red: #ff453a;
    --sys-teal: #40c8e0;
    --sys-yellow: #ffd60a;
    --sys-mint: #63e6e2;
    --sys-cyan: #64d2ff;
    --sys-brown: #ac8e68;
    --sys-gray: #98989d;
    --hue-docs: var(--sys-blue);
    --hue-docs-wash: rgba(10, 132, 255, .16);
    --hue-mail: var(--sys-teal);
    --hue-mail-wash: rgba(64, 200, 224, .16);
    --hue-tickets: var(--sys-red);
    --hue-tickets-wash: rgba(255, 69, 58, .16);
    --hue-wiki: var(--sys-green);
    --hue-wiki-wash: rgba(48, 209, 88, .16);
    --hue-contacts: var(--sys-brown);
    --hue-contacts-wash: rgba(172, 142, 104, .16);
    --hue-calendar: var(--sys-orange);
    --hue-calendar-wash: rgba(255, 159, 10, .16);
    --hue-people: var(--sys-mint);
    --hue-people-wash: rgba(99, 230, 226, .16);
    --hue-ai: var(--sys-purple);
    --hue-ai-wash: rgba(191, 90, 242, .16);
    --hue-star: var(--sys-yellow);
    --hue-star-wash: rgba(255, 214, 10, .16);
    --hue-home: var(--sys-indigo);
    --hue-home-wash: rgba(94, 92, 230, .16);
    --hue-search: var(--sys-gray);
    --hue-search-wash: rgba(152, 152, 157, .16);
    --hue-work: var(--sys-orange);
    --hue-work-wash: rgba(255, 159, 10, .16);
    --hue-activity: var(--sys-pink);
    --hue-activity-wash: rgba(255, 55, 95, .16);
    --hue-settings: var(--sys-gray);
    --hue-settings-wash: rgba(152, 152, 157, .16);
    --info: var(--sys-blue);
    --shadow-sm: 0 1px 2px rgba(0,0,0,.3);
    --shadow: 0 4px 12px rgba(0,0,0,.4);
    --shadow-lg: 0 12px 32px rgba(0,0,0,.5);
}
html[data-theme="dark"] .app-nav .navbar { background: var(--surface) !important; border-bottom-color: rgba(255, 255, 255, .08); }
html[data-theme="dark"] .app-nav .navbar-brand { color: var(--text) !important; }

/* -------------------------------------------------------------------------
   Global text-selection policy.

   Chrome text is NOT selectable by default - clicking a paragraph, button
   label, modal description, sidebar row, etc. shouldn't drop a blinking
   caret or highlight letters. That was the repeated complaint ("edit
   cursor when I click on text"). Text-selection stays ON only where the
   user actually wants to copy content: message subjects / bodies / headers,
   sender / recipient addresses, notes, labels, and explicit form inputs.

   Ordering matters: the `text`/`auto` overrides below have to sit AFTER the
   global `none` to win the cascade. Do not move this block.

   Never on an Apple touch device (html.bf-ios, stamped by the layout's head
   script): iOS WebKit refuses text selection inside a textarea whose
   ancestor says none - the iPad composers lost their selection handles
   (owner, 2026-09-16). A finger selects chrome text only by a deliberate
   long-press anyway.
   ------------------------------------------------------------------------- */
html:not(.bf-ios), html:not(.bf-ios) body {
    -webkit-user-select: none;
    -ms-user-select: none;
    user-select: none;
}

/* Inputs / textareas / editable areas: browsers use their own selection
   model here, but declaring explicitly keeps the intent clear and
   future-proofs against any theme that tries to disable it. */
input, textarea, select, [contenteditable="true"], [contenteditable=""] {
    -webkit-user-select: auto;
    -ms-user-select: auto;
    user-select: auto;
}

/* Email content - the ONE place the user said selection must stay on.
   NOTE: .mail-snippet / .mail-from / .mail-subject are deliberately
   EXCLUDED here even though they're text. Those are list-row labels
   on /Mail (sender / subject / preview snippet) and the row itself
   is a clickable navigation target. Marking them user-selectable
   made browsers paint the text-edit (I-beam) cursor on hover, which
   the user said reads as "this is editable text" and breaks the
   click affordance. The row's pointer cursor now wins, and a user
   who really wants to select a sender name can still drag-select
   within the row body since user-select isn't none, just default. */
.msg-body, .msg-body *,
.msg-body-host, .msg-body-host *,
.msg-subject, .msg-headers, .msg-headers *,
.account-email,
.note-text, .note-text *,
[data-selectable="true"], [data-selectable="true"] * {
    -webkit-user-select: text;
    -ms-user-select: text;
    user-select: text;
}

/* Mail-list row text: force the click cursor on every text child so
   the row reads as a single clickable unit. cursor isn't inherited
   by default so each child element defaults to `auto` → text I-beam
   on text nodes; an explicit `cursor: pointer` here matches the
   `.mail-row > .mail-content` declaration further down. */
.mail-row,
.mail-row *,
.mail-from,
.mail-subject,
.mail-snippet {
    cursor: pointer;
}
/* But leave the per-control overrides alone - checkbox, star, and
   the per-row "open in new tab" icon each set their own cursor and
   should NOT inherit pointer-over-everything. */
.mail-row input[type="checkbox"],
.mail-row .mail-star,
.mail-row .mail-open-tab {
    cursor: default;
}
.mail-row .mail-star { cursor: pointer; }
.mail-row .mail-open-tab { cursor: pointer; }
/* (Legacy .mail-row hover/unread rules removed - new zebra-aware rules live
   alongside .mail-row elsewhere and would otherwise get overridden here.) */

/* ── Quiet scrollbars (owner, 2026-09-06, the validated sketch) ────────
   The gutter stays reserved (zero layout shift); NOTHING is painted at rest.
   One slim rounded ink thumb appears while the pointer is over the scroller
   or the scroller is moving (app-shell.js stamps .is-scrolling for 600 ms
   after each scroll event), and deepens under the pointer. No track, no
   arrows, ONE token (--text) for both themes.

   Engines: Chrome and Safari paint through the webkit pseudo-elements, and
   Chrome (121+) IGNORES those the moment scrollbar-width or scrollbar-color
   is set on the element - which is exactly what the former "* { scrollbar-
   width: thin }" did: Windows Chrome painted its own thin classic bar with
   arrow buttons (the owner's complaint), the designed thumb only ever showed
   in Safari. So the standard properties belong to Firefox alone, scoped by
   @supports; nothing else in the stylesheets may set them (QuietScrollbarsTests).
   Touch devices keep their native overlay bars (pointer: fine only). The
   page's own bar shows only while scrolling - html:hover is true whenever the
   pointer is over the window, so it is excluded from the hover face. */
@media (pointer: fine) {
    @supports selector(::-webkit-scrollbar) {
        *::-webkit-scrollbar { width: 10px; height: 10px; }
        *::-webkit-scrollbar-track { background: transparent; }
        *::-webkit-scrollbar-corner { background: transparent; }
        *::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
        *::-webkit-scrollbar-thumb {
            background: transparent;
            border-radius: 999px;
            border: 3px solid transparent;
            background-clip: content-box;
        }
        :not(html):not(body):hover::-webkit-scrollbar-thumb,
        .is-scrolling::-webkit-scrollbar-thumb {
            background: color-mix(in srgb, var(--text, #0f172a) 22%, transparent);
            background-clip: content-box;
        }
        *::-webkit-scrollbar-thumb:hover,
        *::-webkit-scrollbar-thumb:active {
            background: color-mix(in srgb, var(--text, #0f172a) 38%, transparent);
            background-clip: content-box;
        }
    }
    @supports not selector(::-webkit-scrollbar) {
        * {
            scrollbar-width: thin;
            scrollbar-color: transparent transparent;
        }
        :not(html):not(body):hover,
        .is-scrolling {
            scrollbar-color: color-mix(in srgb, var(--text, #0f172a) 22%, transparent) transparent;
        }
    }
}
html[data-theme="dark"] { color-scheme: dark; }
/* The display face, self-hosted (wwwroot/fonts; _Layout preloads the Latin
   file): one variable file per script range, weights 700-800, and OPTIONAL -
   never swap (owner, 2026-09-26: zero layout shift). A late swap re-measured
   every heading after first paint (the page title re-widened, its count and
   the rail beside it moved); optional uses the face when the preload has it
   ready by first paint - every load once cached - and otherwise keeps the
   fallback for that page view instead of reflowing it. */
@font-face {
    font-family: "Plus Jakarta Sans";
    font-style: normal;
    font-weight: 700 800;
    font-display: optional;
    src: url(/fonts/plus-jakarta-sans-latin.woff2) format("woff2");
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
    font-family: "Plus Jakarta Sans";
    font-style: normal;
    font-weight: 700 800;
    font-display: optional;
    src: url(/fonts/plus-jakarta-sans-latin-ext.woff2) format("woff2");
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
html { color-scheme: light; }

/* Platform placeholder default: every field's placeholder rides the
   muted token in BOTH themes. Element-level specificity on purpose -
   any per-control placeholder rule (class-based) still wins. Without
   this, fields that never declared one fell back to a UA gray that
   vanishes on the dark canvas (the issue composer was the reported
   case). opacity:1 stops Firefox dimming the color a second time. */
input::placeholder,
textarea::placeholder {
    color: var(--text-muted, #656d76);
    opacity: 1;
}

/* Bootstrap utilities that default to a dark gray - unreadable on dark surfaces.
   Route them through our theme token so they stay legible in both modes. */
html[data-theme="dark"] .text-muted,
html[data-theme="dark"] .form-text,
html[data-theme="dark"] .text-secondary { color: var(--text-muted) !important; }

/* Bootstrap's .form-control paints through --bs-body-bg/--bs-body-color, which
   stay LIGHT whatever data-theme says (the app never stamps data-bs-theme) - so
   every text field rendered as a white box with near-black ink on the dark
   canvas. Dark fields go TRANSPARENT - the exact tone bf-select.css gives the
   trigger in dark - so an input and the select beside it (banette assistant
   families step, workflow composer) read as ONE control family; ink +
   placeholder follow the theme, and a disabled field keeps its honest
   one-step-off tint. NO border-color here on purpose: at this specificity
   (0,2,1) it would beat .form-control:focus (0,2,0) and freeze the focus
   border - the base .form-control rule already tokenises the border for both
   themes. Pinned by FormControlDarkThemeTests. */
html[data-theme="dark"] .form-control {
    background-color: transparent;
    color: var(--text);
}
html[data-theme="dark"] .form-control::placeholder { color: var(--text-muted); }
html[data-theme="dark"] .form-control:disabled { background-color: var(--surface-2); }

* { box-sizing: border-box; }

html, body {
    /* `height: 100%` reads the LARGE viewport on iOS Safari
       (the height the page would have if the URL bar were
       collapsed). At first paint the URL bar is showing, so
       the body extends BELOW the visible area - when the user
       scrolls, the URL bar collapses and a gap shows below
       the app shell because the layout was already sized for
       the larger viewport. `100dvh` (dynamic viewport height)
       tracks the actual visible viewport, so the shell hugs
       the visible area through every URL-bar transition. The
       `100%` fallback covers ancient browsers that ignore
       dvh. */
    height: 100%;
    height: 100dvh;
    margin: 0;
    padding: 0;
    overflow: hidden;
    background: var(--bg);
    color: var(--text);
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Inter", Roboto, "Helvetica Neue", Arial, sans-serif;
    font-size: 14px;
    line-height: 1.5;
    /* Honour iOS home-indicator inset so app chrome never sits
       UNDER the system handle. Safe-area-inset is 0 on
       desktop and on Android - no-op there. */
    padding-bottom: env(safe-area-inset-bottom, 0);
}

body { display: flex; flex-direction: column; }

/* Navbar */
.app-nav {
    flex: 0 0 auto;
    /* Own stacking context so dropdowns + stray overlays from page content
       below can never visually cover the nav (and its account/notification
       dropdowns), making links unclickable.

       The value must clear the page-content band: in-page popovers and widget
       controls layer in the low 0-120 range (e.g. the documents search-options
       control and the home activity filter), so a nav at z-index:100 TIED the
       100-band controls and lost the tie to later-in-DOM page content, letting
       a control's icon paint over an open dropdown. 200 sits above that whole
       band while staying below fullscreen page overlays (preview panel, ~500)
       and app-level overlays (modals/toasts/command palette, 1000+), which are
       meant to cover the nav. Page content < nav < overlays. */
    position: relative;
    z-index: 200;
}
.app-nav .navbar {
    background: #ffffff !important;
    /* Translucent navbar underline - same rule as cards / footer:
       hairline reads as a glass edge instead of a painted grey line. */
    border-bottom: 1px solid rgba(0, 0, 0, .06);
    padding: .5rem 1.5rem;
}
.app-nav a, .app-nav button { pointer-events: auto; }
.app-nav .navbar-brand {
    color: var(--text) !important;
    font-weight: 700;
    letter-spacing: -.01em;
    font-size: 14px;
}
/* Brand mark = icon + wordmark. The wordmark uses Plus Jakarta Sans
   (loaded in _Layout.cshtml) at weight 800 so "Burofolk" reads as a
   logotype. Slightly tightened tracking + a tiny gap from the icon
   keeps the unit cohesive. */
.brand-mark {
    display: inline-flex; align-items: center; gap: .55rem;
    padding: 0 !important;
    line-height: 1;
}
.brand-mark-icon {
    flex: 0 0 auto;
    display: block;
    transition: transform .35s cubic-bezier(.34, 1.56, .64, 1);
}
.brand-mark:hover .brand-mark-icon { transform: rotate(-6deg) scale(1.06); }
.brand-mark-wordmark {
    font-family: "Plus Jakarta Sans", "Inter", -apple-system, system-ui, sans-serif;
    font-weight: 800;
    font-size: 1.18rem;
    letter-spacing: -.02em;
    color: var(--text);
    line-height: 1;
}
.app-nav .nav-link {
    color: var(--text-muted) !important;
    font-weight: 500;
    font-size: 13px;
    padding: .4rem .75rem !important;
    border-radius: 6px;
}
.app-nav .nav-link:hover { background: var(--surface-2); color: var(--text) !important; }

/* Premium pill-style primary nav */
.app-primary-nav { gap: .25rem; }
.app-nav .nav-link-pill {
    display: inline-flex; align-items: center; gap: .45rem;
    padding: .45rem .75rem !important;
    border-radius: 8px;
    font-size: 12px; font-weight: 500;
    color: var(--text) !important;
    background: transparent;
    /* A nav pill is a single label + optional glyph/count - it must NEVER
       wrap to a second line (the "Espaces de travail" title broke onto 3
       lines when its pill was width-constrained). One line, always. */
    white-space: nowrap;
    transition: background .12s ease, color .12s ease;
}
.app-nav .nav-link-pill svg { color: var(--text-subtle); transition: color .12s ease; }
.app-nav .nav-link-pill:hover { background: var(--surface-2); }
.app-nav .nav-link-pill:hover svg { color: var(--text); }
/* Active pill: brand-tint fill + DARK text. The fill marks
   "you are here"; text stays in the platform default (dark) so
   foreground never collides with the background hue. No
   underline / strip / accent ornament - those read as
   consumer-marketing-app territory. */
.app-nav .nav-link-pill.is-active {
    background: color-mix(in srgb, var(--text) 7%, transparent);
    color: var(--text) !important;
    font-weight: 600;
}
.app-nav .nav-link-pill.is-active svg { color: var(--text); }
.app-nav .nav-link-pill.is-active .nav-pill-count {
    background: transparent;
    color: var(--primary);
}
.app-nav .nav-link-pill.dropdown-toggle::after {
    margin-left: .3rem; border-top-color: var(--text-subtle);
}
.app-nav .nav-link-pill .nav-pill-count {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 18px; height: 18px; padding: 0 5px;
    background: var(--primary-bg); color: var(--primary);
    border-radius: 10px; font-size: 10px; font-weight: 600;
    margin-left: .15rem;
}

/* Accounts nav link - swap the mailbox glyph for a spinner while any
   account is actively syncing, so the nav itself signals "work in progress"
   across browser tabs. Static look by default, animated pulse when active. */
.nav-workspaces-icon {
    position: relative;
    display: inline-flex; align-items: center; justify-content: center;
    width: 14px; height: 14px;
}
.nav-workspaces-icon .nav-workspaces-mailbox {
    display: block;
}

/* Dropdowns - soft, elevated, premium look */
.app-nav .dropdown-menu {
    border: 1px solid var(--border);
    /* Layered elevation (ambient + key + far) reads as a crafted floating
       surface rather than a flat card with a single blur behind it. */
    box-shadow:
        0 1px 2px rgba(15, 23, 42, .06),
        0 12px 28px rgba(15, 23, 42, .14),
        0 28px 56px rgba(15, 23, 42, .08);
    border-radius: 10px;
    padding: .35rem;
    min-width: 260px;
    margin-top: .5rem;
    background: var(--surface);
    /* Breathing room around the dropdown body so items don't butt up
       against the rounded border at the top and bottom. Bootstrap's
       default is 0 padding which looks cramped with our custom items. */
    padding: .35rem;
}
.app-nav .dropdown-header {
    font-size: 10px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--text-subtle);
    padding: .5rem .75rem .4rem;
}
.app-nav .dropdown-item {
    font-size: 12px; color: var(--text);
    padding: .55rem .7rem; border-radius: 8px;
    display: flex; align-items: center; gap: .7rem;
}
.app-nav .dropdown-item + .dropdown-item { margin-top: 2px; }
.app-nav .dropdown-item:hover, .app-nav .dropdown-item:focus {
    background: var(--surface-2); color: var(--text);
}
.app-nav .dropdown-divider { margin: .4rem .25rem; border-top-color: var(--border); }

/* Archive mailbox picker - avatar + label + email + trailing indicator.
   Width hugs content (tight min so very first char anchors) and items get
   a clear active / hover treatment so the user can tell at a glance which
   workspace is already open. */
.app-archive-menu {
    min-width: 260px;
    /* Never wider than the viewport. These menus are left-anchored
       (bottom-start) under a toggle that sits toward the RIGHT of the
       bar, and on tablet portrait (iPad Pro 1024px) the wide two-column
       variant (min-width:560px, see the >=860px block below) can be
       wider than the room Popper has to shift into - so it spilled past
       the right edge and got cropped. Capping the box to the viewport
       guarantees Popper's preventOverflow can always place it fully
       on-screen. Pinned by ArchiveMenuNeverExceedsViewportTests. */
    max-width: calc(100vw - 20px);
    /* The MENU itself never scrolls. Two regions live inside:
       - the dropdown-header (always visible, never moves)
       - the .app-archive-scroll inner shell (the only thing
         that scrolls when the list overflows)
       Splitting the scroll out of the menu keeps the scrollbar
       confined to the LIST region - it visually starts BELOW the
       header instead of running across it (the bug the user
       reported). */
    max-height: 70vh;
    overflow: hidden;
    padding: 0;
}
/* Flex column when open so the scrollable list absorbs whatever
   vertical space the fixed regions above it (pinned-milestones
   section + "Your issue trackers" header) leave - instead of each
   region capping itself independently. The previous fixed
   `calc(70vh - 38px)` on the list only accounted for the header, so
   once the pinned section was added the menu overflowed its own 70vh
   and `overflow:hidden` cropped the bottom rows. (.show is what
   Bootstrap toggles open; equal specificity to `.dropdown-menu.show`,
   site.css wins on source order.) */
@keyframes app-archive-menu-in {
    from { opacity: 0; }
    to { opacity: 1; }
}
.app-archive-menu.show {
    display: flex;
    flex-direction: column;
    /* Subtle fade on open. Opacity ONLY: the menu's transform is owned by
       Popper for positioning, so animating transform here would fight it. */
    animation: app-archive-menu-in 130ms cubic-bezier(.22, .61, .36, 1);
}
.app-archive-menu > .dropdown-header,
.app-archive-menu > .app-archive-pinned-ms {
    flex: 0 0 auto;
}
/* Scroll shell for the pinned-milestones list: a fixed (non-flexing) region
   capped so a long pin list scrolls on its own instead of squeezing the issue
   trackers below it out of the 70vh menu. The scroll lives on this WRAPPER,
   not the list, so in the wide menu the list can fold into multiple columns
   without the overflow-column artefact a capped-height multicol produces -
   mirroring how the tracker list scrolls via its `.app-archive-scroll`
   parent. */
.app-archive-pinned-ms-scroll {
    max-height: 34vh;
    overflow-y: auto;
    overscroll-behavior: contain;
}
.app-archive-menu .dropdown-header {
    margin: 0;
    padding: 12px 16px 8px;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    /* Border-radius inheritance from .dropdown-menu's outer mask
       handles the top corners; the header just paints flat
       between them. */
}
/* Workspace-type nav header: section label on the left, a single
   "create a new workspace of this type" affordance on the right.
   A flex row that KEEPS the header's existing height - the button's
   box is pulled in with negative vertical margin so it sits inside
   the same 10px/8px padding the bare label used, never growing the
   menu's fixed header region. The "+" mirrors the roadmap / pin icon
   buttons one row below (same 26px hit area, transparent rest state,
   subtle hover wash + accent) so the trailing-control column reads as
   one consistent language top-to-bottom. */
.app-archive-menu .dropdown-header.app-archive-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
}
.app-archive-header-label {
    /* Inherits the header's uppercase / letter-spacing / colour, so
       wrapping the bare text in a span is visually identical; this
       just lets a long label ellipsize instead of shoving the button. */
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.app-archive-header-add {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    /* Neutralise the button's extra height against the label's line
       box so adding it never grows the fixed header region. */
    margin: -5px 0;
    border-radius: 6px;
    color: rgba(15, 23, 42, .32);
    text-decoration: none;
    transition: color 120ms ease, background-color 120ms ease, transform 120ms ease;
}
.app-archive-header-add:hover {
    background: rgba(15, 23, 42, .05);
    color: var(--text);
    transform: scale(1.08);
}
.app-archive-header-add:focus { outline: none; }
.app-archive-header-add:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
html[data-theme="dark"] .app-archive-header-add { color: rgba(255, 255, 255, .35); }
html[data-theme="dark"] .app-archive-header-add:hover { background: rgba(255, 255, 255, .08); color: var(--text); }
.app-archive-scroll {
    /* The actual scroll container. Cap at 70vh minus the header's
       footprint (~38px) so the menu's outer max-height contract
       still holds. overscroll-behavior contain stops a wheel
       gesture from leaking onto the page behind the dropdown.
       Items live in a nested <ul.app-archive-scroll-list> because
       <li> directly containing more <li>s is malformed HTML -
       browsers auto-close the outer <li> early, which is what
       broke scrolling the first time we tried this split. */
    list-style: none;
    margin: 0;
    padding: 0;
    /* Flex child: take all space the fixed regions above leave, down
       to nothing, and scroll within it. min-height:0 is required or a
       flex item refuses to shrink below its content height. */
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
}
.app-archive-scroll-list {
    list-style: none;
    margin: 0;
    /* 8px inset - one step on the 4px spacing scale the menu runs on. */
    padding: 8px;
}
.app-archive-scroll::-webkit-scrollbar {
    width: 8px;
}
.app-archive-scroll::-webkit-scrollbar-track {
    background: transparent;
    border-top: 4px solid transparent;
    border-bottom: 8px solid transparent;
    background-clip: padding-box;
}
.app-archive-scroll::-webkit-scrollbar-thumb {
    border-radius: 999px;
    border: 2px solid transparent;
    min-height: 32px;
}
.app-archive-scroll::-webkit-scrollbar-thumb:hover {
    background: color-mix(in srgb, var(--text-muted) 55%, transparent);
    background-clip: padding-box;
    border: 2px solid transparent;
}
/* Nav dropdown row: anchor + sibling pin-toggle form. The <li>
   becomes a flex container; anchor takes the available width,
   the form sits at the right edge with its small star button.

   Hover background paints on the LI (not the anchor) so it
   spans the FULL row width including the star area. The
   anchor's own dropdown-item hover paint is suppressed so the
   two don't fight. */
.app-archive-menu .app-archive-scroll-list > li {
    display: flex;
    align-items: stretch;
    position: relative;
    border-radius: 8px;
    /* Breathing room between rows: a small vertical gap so adjacent
       two-line rows stop reading as one packed block, and the
       rounded hover / current highlight floats as a discrete row
       (calmer, premium menu rhythm) instead of full-bleed bands
       that touch. The horizontal padding lives on the list. */
    margin-bottom: 4px;
    transition: background-color 100ms ease;
}
.app-archive-menu .app-archive-scroll-list > li:last-child {
    margin-bottom: 0;
}
.app-archive-menu .app-archive-scroll-list > li:hover,
.app-archive-menu .app-archive-scroll-list > li:focus-within {
    background: var(--surface-2);
}
.app-archive-menu .app-archive-scroll-list > li > .app-archive-item {
    flex: 1 1 auto;
    min-width: 0;
}
.app-archive-menu .app-archive-scroll-list > li > .app-archive-item:hover,
.app-archive-menu .app-archive-scroll-list > li > .app-archive-item:focus {
    background: transparent !important;
}

/* Divided footer row for a cluster's tenant-level utility (the Documents
   cluster links the data models' home here). Same anatomy as the repository
   rows above - icon slot, name + purpose subtitle, hover chevron - so the
   menu reads as one list with a quiet utility at its foot. */
.app-archive-utility {
    list-style: none;
    margin: 6px 0 0;
    padding: 4px 0 0;
    border-top: 1px solid var(--border);
}
/* Full-bleed like the repository rows (their hover wash runs edge to edge),
   with the SAME columns: the icon starts at the rows' 8px gutter and the
   text one row-gap (.8rem, the archive-item value) after the 32px icon. The
   link deliberately wears neither .dropdown-item nor .app-archive-item -
   both carry padding cascades - so these numbers stand alone. */
.app-archive-menu .app-archive-utility .app-archive-utility-link {
    display: flex;
    align-items: center;
    gap: .8rem;
    padding: 6px .8rem 6px 8px;
    border-radius: 0;
    white-space: normal;
    color: inherit;
    text-decoration: none;
}
.app-archive-menu .app-archive-utility .app-archive-utility-link:hover,
.app-archive-menu .app-archive-utility .app-archive-utility-link:focus {
    background: var(--surface-2);
}
/* The trailing chevron reveals on hover, the archive rows' affordance. */
.app-archive-utility-link:hover .app-archive-arrow,
.app-archive-utility-link:focus-visible .app-archive-arrow {
    opacity: 1;
    transform: translateX(0);
    color: var(--text);
}
/* The glyph slot mirrors the workspace icon's footprint (a bordered square,
   monochrome mark) so the name column lines up with the rows above. */
.app-archive-utility-ic {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text-muted);
    background: var(--surface);
}

/* Two-column tracker list when the cluster is crowded AND the viewport
   has room for it. Pure-CSS, no markup branch and no threshold repeated
   per dropdown: `:has(... li:nth-child(9))` is true only once the list
   holds at least nine rows, so short clusters (the common case) stay one
   tight column, and a long list folds into two instead of becoming a tall
   scroller. The media query keeps the wider ~560px menu off narrow
   viewports where it couldn't sit cleanly (820px and below stay single
   column; 1024px and up go two-up). One selector drives the Issues /
   Wikis / Inboxes menus, so every workspace-type cluster behaves the same.

   CSS multi-column (not grid): `column-count: 2` guarantees the two columns
   are EQUAL width, and a single continuous `column-rule` hairline - the same
   colour the row dividers use - separates them, which reads far cleaner than
   a bare gap. `break-inside: avoid` keeps each row whole instead of letting
   one split across the column boundary. Rows flow column-major (down the
   left column, then down the right) - the single list simply folded in half.

   Scoped to `.app-archive-scroll > .app-archive-scroll-list` (the tracker
   list, a direct child of the scroll shell). The pinned-milestones list also
   carries `.app-archive-scroll-list` but lives under `.app-archive-pinned-ms`,
   so it is excluded from the columns here and instead pinned to its original
   single-column width below. Mirrors the `:has(> .is-current)` idiom below. */
@media (min-width: 860px) {
    .app-archive-menu:has(.app-archive-scroll > .app-archive-scroll-list > li:nth-child(9)) {
        min-width: 560px;
    }
    .app-archive-menu:has(.app-archive-scroll > .app-archive-scroll-list > li:nth-child(9)) .app-archive-scroll > .app-archive-scroll-list {
        column-count: 2;
        column-gap: 24px;
        column-rule: 1px solid var(--border);
    }
    /* Keep a row intact within one column - no mid-row split at the break. */
    .app-archive-menu:has(.app-archive-scroll > .app-archive-scroll-list > li:nth-child(9)) .app-archive-scroll > .app-archive-scroll-list > li {
        break-inside: avoid;
        -webkit-column-break-inside: avoid;
    }
    /* Pinned milestones fold into the SAME two equal columns as the trackers -
       same column-count, gap, rule AND list padding - so a pinned row is
       exactly the width of a tracker row, the two sections' centre rules line
       up into one continuous vertical line, and the section fills the widened
       menu instead of leaving dead space. The 6px padding restores the base
       list inset that the pinned list zeroes out (issues-milestone.css),
       matching the tracker list; the scroll lives on the wrapper above so the
       multi-column fold is artefact-free. */
    .app-archive-menu:has(.app-archive-scroll > .app-archive-scroll-list > li:nth-child(9)) .app-archive-pinned-ms-list {
        column-count: 2;
        column-gap: 24px;
        column-rule: 1px solid var(--border);
        padding: 8px;
    }
    .app-archive-menu:has(.app-archive-scroll > .app-archive-scroll-list > li:nth-child(9)) .app-archive-pinned-ms-list > li {
        break-inside: avoid;
        -webkit-column-break-inside: avoid;
    }
}

/* Pin/unpin star button living in the nav dropdown row. Matches
   the visual language of the home page's quick-open star (filled
   amber when pinned, subtle gray when not) and the in-app pin
   affordances pinned to dashboard cards. */
.nav-pin-toggle-form {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    margin: 0;
    padding: 0 .55rem;
}
.nav-pin-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    padding: 0;
    border: 0;
    background: transparent;
    border-radius: 6px;
    color: rgba(15, 23, 42, .32);
    cursor: pointer;
    transition: color 120ms ease, background-color 120ms ease, transform 120ms ease, opacity 120ms ease;
}
.nav-pin-toggle:hover {
    background: rgba(15, 23, 42, .05);
    color: rgba(15, 23, 42, .75);
    transform: scale(1.08);
}
.nav-pin-toggle.is-pinned { color: #f59e0b; }
.nav-pin-toggle.is-pinned:hover { color: var(--warning); }
.nav-pin-toggle:focus { outline: none; }
.nav-pin-toggle:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}
html[data-theme="dark"] .nav-pin-toggle { color: rgba(255, 255, 255, .35); }
html[data-theme="dark"] .nav-pin-toggle:hover {
    background: rgba(255, 255, 255, .08);
    color: rgba(255, 255, 255, .85);
}

.app-archive-item {
    /* Two-line rows need vertical breathing room; gap between avatar
       and text matches the spacing used on workspace cards / collab
       rows so the picker feels part of the same system. */
    padding: .6rem .8rem !important;
    gap: .75rem !important;
    position: relative;
    align-items: center !important;
}
.app-archive-icon {
    /* Slot-only class - size + roundness here, the canonical
       .bf-icon paint (auto-added by Html.WorkspaceIcon) owns
       background + glyph color so the bubble reads the workspace's
       --bf-color. The previous build painted a hard-coded
       brand-purple gradient via `background: var(--avatar-bg)`,
       which made every dropdown icon read as violet regardless of
       the workspace's identity color. Pinned by
       WorkspaceIconPaintsPerWorkspaceColorTests. */
    --bf-icon-size: 32px;
    --bf-icon-radius: 50%;
    flex: 0 0 auto;
}
/* Icon anchor inside an archive dropdown `<li>`. Sibling of
   `.dropdown-item.app-archive-item`. Owns the leading padding the
   dropdown-item used to carry (icon was inside that anchor before
   it got pulled out so it could carry its own href -> detail page).
   Vertically centered against the 36px bubble; the dropdown-item
   loses its leading padding via the rule below so the visual rhythm
   stays unchanged. */
.app-archive-icon-link {
    padding: .6rem 0 .6rem .8rem;
    display: inline-flex;
    align-items: center;
}
.app-archive-menu .app-archive-scroll-list > li > .app-archive-icon-link + .app-archive-item {
    padding-left: .75rem !important;
}
/* Canonical "workspace icon that navigates to the detail page"
   wrapper. Emitted by `Html.WorkspaceIconLink`. Stays out of the
   icon's paint (the inner `.bf-icon` owns colour + glyph); this
   rule owns interaction: subtle hover lift, real focus ring,
   pointer cursor, no underline. Inline-flex so the link box is
   exactly the icon box - no stray click target around it. Lives
   here (not in a per-page stylesheet) so every surface that
   mounts the helper picks up identical interaction. */
.ws-icon-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    text-decoration: none;
    color: inherit;
    border-radius: inherit;
    cursor: pointer;
    transition: transform 120ms ease, filter 120ms ease;
    flex: 0 0 auto;
    /* Renders as either <a> (admin → detail link) or <button>
       (non-admin → team-access modal). Reset the UA button chrome so
       the two render identically: no fill, no border, no padding. */
    background: none;
    border: none;
    padding: 0;
    font: inherit;
}
.ws-icon-link:hover { transform: translateY(-1px); filter: brightness(1.05); text-decoration: none; }
.ws-icon-link:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}
/* Per-consumer .has-svg / .has-emoji overrides DELETED here.
   The canonical .bf-icon paint owns the bubble (gradient at
   the workspace's --bf-color); the canonical .bf-icon.has-svg
   rule keeps the gradient while making the inner SVG white; and
   .bf-icon.has-photo paints --surface-2 ONLY because the
   uploaded image covers it edge-to-edge. The previous build had
   per-consumer rules that repainted the bubble pale violet
   (var(--primary-bg)) for SVG / emoji icons - which made every
   non-letter workspace icon read as a generic violet pill
   regardless of the workspace's accent. Transparent / pale fill
   is reserved for has-photo and nothing else. */
.app-archive-meta {
    display: flex; flex-direction: column; gap: 2px; min-width: 0; line-height: 1.3; flex: 1 1 auto;
    /* Reserve the count line's height ALWAYS, so a row whose stats haven't
       been computed yet (count subtitle dropped per the "never render 0"
       rule) is the SAME height as a row that has its count. Without this the
       menu items collapse together until the background stats sweep fills the
       broadcast, then jump taller. Centred so the single name line sits on the
       icon's centre line until the count appears below it. 2.35rem covers the
       name line (.88rem font x 1.3 ~= 1.14rem) + 2px gap + count line
       (.76rem x 1.3 ~= .99rem) ~= 2.26rem, with a hair to spare. */
    min-height: 2.35rem;
    justify-content: center;
}
.app-archive-email {
    color: var(--text);
    font-weight: 600;
    font-size: 13px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    max-width: 240px;
}
.app-archive-count {
    color: var(--text-subtle);
    font-size: 11px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    max-width: 240px;
    display: inline-flex;
    gap: 4px;
    align-items: baseline;
}
.app-archive-count-total {
    color: var(--text-subtle);
}
/* "N today" pill - brand-coloured + semi-bold so the operator's
   eye lands on fresh activity. Renders only when the cached
   today count is > 0 and the cached date matches the current
   UTC day (the layout's roll-over guard). */
.app-archive-count-today {
    color: var(--primary);
    font-weight: 600;
}
/* Open-milestone count spelled out in a tracker subtitle ("N issues · M in
   flight"). Shown ALONGSIDE the flag badge: the badge is the glanceable
   marker, this is the readable count. Quiet, separated by a middot. */
.app-archive-count-flight {
    color: var(--text-subtle);
}

/* Trailing chevron reveals on hover/focus. Keeps the item clean at rest
   but gives the user feedback that it's navigable. */
.app-archive-arrow {
    flex: 0 0 auto;
    color: var(--text-subtle);
    opacity: 0;
    transform: translateX(-4px);
    transition: opacity 120ms ease, transform 120ms ease, color 120ms ease;
    display: inline-flex;
}
.app-archive-item:hover .app-archive-arrow,
.app-archive-item:focus-visible .app-archive-arrow {
    opacity: 1;
    transform: translateX(0);
    color: var(--text);
}

/* Trailing row actions reveal on hover / focus, matching the chevron above:
   at rest a tracker row reads as just icon + name. A bare roadmap flag and an
   UNpinned star fade out; what STAYS at rest is a PINNED star (its amber fill
   is status, not merely an action) and a roadmap flag carrying an open-
   milestone badge - that in-flight count is a glanceable signal worth seeing
   without hovering. This calms the two-column list, where the trailing
   controls would otherwise double up across both columns. opacity (not
   display) so revealing never reflows the row, and :focus-within keeps the
   controls keyboard-reachable; the fade rides each control's own transition
   (opacity added to it). */
.app-archive-menu .app-archive-scroll-list .app-archive-roadmap-link:not(:has(.app-archive-roadmap-badge)),
.app-archive-menu .app-archive-scroll-list .nav-pin-toggle:not(.is-pinned) {
    opacity: 0;
}
.app-archive-menu .app-archive-scroll-list > li:hover .app-archive-roadmap-link,
.app-archive-menu .app-archive-scroll-list > li:focus-within .app-archive-roadmap-link,
.app-archive-menu .app-archive-scroll-list > li:hover .nav-pin-toggle,
.app-archive-menu .app-archive-scroll-list > li:focus-within .nav-pin-toggle {
    opacity: 1;
}

/* Check-mark on the currently-open workspace. Replaces the chevron -
   tells the user "you're already here" without making them read labels. */
.app-archive-current {
    flex: 0 0 auto;
    width: 20px; height: 20px; border-radius: 50%;
    background: var(--primary-bg);
    color: var(--primary);
    display: inline-flex; align-items: center; justify-content: center;
}

/* Active-row treatment - subtle tinted background + primary label.
   Paint on the parent LI so the highlight spans the full row width
   (anchor + pin-toggle form), matching the hover behaviour. */
.app-archive-menu .app-archive-scroll-list > li:has(> .app-archive-item.is-current) {
    /* Selected workspace = a persistent highlight, the SAME treatment as the
       hover state (no left accent bar). The primary-coloured name (rule below)
       is what tells the selected row apart from one that's merely hovered. */
    background: var(--surface-2);
}
.app-archive-item.is-current { background: transparent !important; }
.app-archive-item.is-current .app-archive-email { color: var(--primary); }

.app-archive-manage { color: var(--text-muted) !important; }
.app-archive-manage svg { color: currentColor; }

/* Right-side tools (search / theme / prefs / user menu).
   The vertical divider that visually separates the primary nav
   from the tool strip used to live on the LEFT edge of this
   container. With the presence cluster ("buddies" stack) now
   sitting at the start of the tools strip, that put the divider
   AT THE LEFT of the buddies - which read as "buddies belong with
   the workspace nav" instead of "buddies + tools form the right
   group". The presence-cluster <li> now owns its own border-right
   (see .nav-presence-li below), so the divider correctly sits to
   the RIGHT of the buddies, separating them from Actions / bell /
   profile. We keep a small left padding here for breathing room
   from the primary nav. */
.app-nav .nav-tools {
    display: flex;
    align-items: center;
    gap: 2px;
    padding-left: .5rem;
    margin-left: .5rem;
}
.app-nav .nav-icon-btn {
    width: 34px;
    height: 34px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: 1px solid transparent;
    border-radius: 8px;
    cursor: pointer;
    color: var(--text-muted);
    transition: background .12s ease, color .12s ease, border-color .12s ease;
    padding: 0;
    /* Symmetric breathing space so adjacent icons (bell, chat,
       approvals, theme, super-admin) sit ~6px apart without
       their hover backgrounds touching. Inline margins compose
       with the parent .navbar-nav's default flex layout and
       compose cleanly in RTL. Widened from 3px to 5px so the
       cluster reads as discrete tappable targets, Apple-style,
       not a packed strip. */
    margin-inline: 5px;
}
/* Slightly thinner icon strokes inside the right-hand nav cluster.
   The default stroke-width="2" on inline SVGs reads heavy compared
   to Apple's SF Symbols at the same size; pulling to 1.6 unifies
   the icons visually without redrawing each one. */
.app-nav .nav-icon-btn svg {
    stroke-width: 1.6;
}
.app-nav .nav-icon-btn:hover,
.app-nav .nav-icon-btn:focus-visible {
    background: var(--surface-2);
    color: var(--text);
    border-color: rgba(0, 0, 0, .06);
    outline: none;
}
html[data-theme="dark"] .app-nav .nav-icon-btn:hover,
html[data-theme="dark"] .app-nav .nav-icon-btn:focus-visible {
    border-color: rgba(255, 255, 255, .08);
}
.app-nav .nav-icon-btn:active { transform: translateY(0.5px); }
.app-nav .nav-icon-btn svg { display: block; }

/* Bell with unread-mention count. Anchors the little pill that floats
   above the top-right of the icon, matching the visual weight of the
   other nav icons. When no mentions are unread the pill is hidden and
   the bell stays the same muted color as the theme-toggle next to it. */
/* ===== Mobile navbar drawer (below the lg breakpoint) =================
   Default Bootstrap stacks every list-item vertically and centers them
   weirdly - the icons end up floating in the middle of the page next to
   the left-aligned link list. Below the .navbar-expand-lg breakpoint we
   restyle the collapse target as a proper drawer:
     - Brand stays on the top bar with the toggler.
     - Drawer items each get full row: icon + readable label.
     - The "icon strip" (Activity / Bell / Approvals / Theme) stays in a
       single horizontal row at the bottom so they don't take a full
       row each just to expose 4 icons.
     - User menu becomes a clear full-width row that opens a dropdown
       in place. */
/* ============================================================
   Mobile navbar drawer (below lg breakpoint).

   Replaces Bootstrap's inline collapse with a proper slide-in
   drawer from the right: full-height fixed panel, dimmed
   backdrop, body-scroll lock, full-width row treatment for every
   nav item, inline dropdowns. Driven by the same .show class
   Bootstrap toggles on the collapse target - we just override
   the visual completely.

   The drawer is ALWAYS present in the DOM (display: flex !important
   to override Bootstrap's display: none) but lives off-screen via
   transform: translateX(100%) plus visibility: hidden. The .show
   class slides it in. The collapsing intermediate state gets the
   same transform so Bootstrap's height animation doesn't fight
   the slide.
   ============================================================ */
/* Drawer chrome (head, backdrop) is JS-injected into the navbar
   collapse element for mobile. On desktop the navbar renders
   inline and these extras would leak into the bar - hide them by
   default and re-show inside the mobile media query below.
   Same goes for the user-row enhancements (email subline,
   chevron) - hidden on desktop, surfaced in the drawer. */
.nav-drawer-head,
.nav-drawer-backdrop,
.app-nav .user-menu .user-email-sub,
.app-nav .user-menu .user-chev { display: none; }
/* user-stack is a transparent wrapper on desktop so the inline
   .user-email span retains its layout. */
.app-nav .user-menu .user-stack { display: contents; }

/* The 768-991px range used to keep the navbar expanded
   (navbar-expand-md) with a static label-hiding block here. Even
   icon-only, the bar's floor (org switcher + primary pills + full
   tools strip, ~910px) exceeds most of that range, so it overflowed
   the viewport, widened the document, and gave the home page a
   phantom horizontal scroll. The navbar is navbar-expand-lg now:
   below 992px the hamburger drawer owns the range, so no expanded-
   mode shrink rules are needed there. Pinned by
   TopNavRespectsTabletWidthTests. */

/* Adaptive overflow fallback for EXPANDED mode (>=992px): any size
   where the full bar doesn't fit (iPad Pro landscape, narrow
   desktop windows, font-scale on 4K) needs dynamic adaptation. The
   `nav-overflow` class is toggled by adaptiveNavOverflow() in
   _Layout.cshtml whenever the navbar's scrollWidth exceeds its
   clientWidth - meaning the trailing icons (avatar, theme toggle)
   would otherwise be cropped. Pinned by
   TopNavOverflowAdaptsToAnySizeTests. */
.app-nav .navbar.nav-overflow .app-primary-nav .nav-link-pill > span:not([class]) {
    display: none;
}
.app-nav .navbar.nav-overflow .nav-link-pill {
    padding: .45rem .55rem !important;
}

/* Trailing-cluster guard for EXPANDED mode (>=992px). The trailing
   tools cluster holds the user avatar; pin it so it is the LAST thing to
   yield, and let the primary-nav be the flexible element. When the labels
   don't fit, the adaptiveNavOverflow() optimiser collapses them to icons
   (the content-aware mechanism) - that keeps the avatar on-screen.

   CRITICAL: do NOT put `overflow` (auto/hidden/scroll) on
   .app-primary-nav or any element that is an ANCESTOR of a nav dropdown
   menu. Overflow on such a container clips the menus, which then open but
   render invisibly - "clicking a menu does nothing", a catastrophic
   regression shipped 2026-07-15 and reverted the same day. Pinned by
   TopNavPrimaryNavDoesNotClipMenusTests. */
@media (min-width: 992px) {
    .app-nav .navbar > .container-fluid { min-width: 0; }
    .app-nav .navbar-collapse { min-width: 0; }
    /* Tools cluster (notifications ... avatar) never shrinks or gets
       pushed off the right edge - it is the last thing to go. */
    .app-nav .nav-tools { flex: 0 0 auto; }
}

/* Icon-only floor for the whole EXPANDED-but-tight band (768-1280px):
   the inline tablet bar (768-991, navbar-expand-md) AND iPad-Pro-portrait
   (1024px) both sit inside it. The primary-nav labels collapse to icons
   deterministically in pure CSS so the bar fits (and the avatar is on
   screen) from the very first paint, with no dependence on the JS
   optimiser. Below 768 the hamburger drawer owns the range (labels show
   there, stacked). Above 1280 the labels fit for common toolbars.

   This uses display:none on the label TEXT span only - it hides text; it
   does NOT put overflow on any menu container, so nav dropdowns stay
   clickable (the 2026-07-15 regression was overflow-clipping, never this).
   Pinned by TopNavTabletBandCollapsesLabelsWithoutClipping. */
@media (min-width: 768px) and (max-width: 1279.98px) {
    .app-nav .app-primary-nav .nav-link-pill > span:not([class]) { display: none; }
    .app-nav .app-primary-nav .nav-link-pill { padding: .45rem .55rem; }
}

/* ── CONTENT-AWARE progressive de-clutter ────────────────────────────────
   The inline bar sheds low-priority chrome so the useful workspace
   dropdowns + bell + avatar stay on-screen and the row NEVER overflows the
   viewport. It is driven by adaptiveNavOverflow() in _Layout, NOT by fixed
   width breakpoints - fixed widths only ever fit ONE toolbar and left the
   avatar clipped for any viewer with a wider bar (a longer org name, extra
   tool icons) or on a desktop window narrower than that bar (the exact
   defect the user kept reporting). The JS measures real overflow and adds
   nav-declutter-N escalating levels until the bar fits, at ANY width. Each
   level is cumulative and drops the next tier in the user's stated priority:
     level >=1  labels -> icons              (`nav-overflow`, rule above)
     level >=2  Workspaces + People pills    (`nav-declutter-1`)
     level >=3  Announcements + Quick notes  (`nav-declutter-2`)
     level >=4  Chat + Calendar              (`nav-declutter-3`)
     level >=5  Theme + Automation           (`nav-declutter-4`)
   The tools cluster that holds the avatar is pinned (flex:0 0 auto) so it is
   the last thing to go. Local time + presence are already desktop-only
   (<=991.98). Below 768 the hamburger drawer carries the FULL menu, so these
   hide-classes never strip anything the user can't otherwise reach. Pinned by
   TopNavContentAwareDeclutterTests. */
.app-nav .navbar.nav-declutter-1 .nav-drop-t1 { display: none; }
.app-nav .navbar.nav-declutter-2 .nav-announce-li,
.app-nav .navbar.nav-declutter-2 .nav-notes-li { display: none; }
.app-nav .navbar.nav-declutter-3 .nav-chat-li,
.app-nav .navbar.nav-declutter-3 .nav-calendar-li { display: none; }
.app-nav .navbar.nav-declutter-4 .nav-drop-t4 { display: none; }

/* Hamburger drawer takes over below 768px (navbar-expand-MD). Between 768
   and 991px the bar stays inline and de-clutters via the nav-drop ladder
   below instead of collapsing. This breakpoint moved from 991.98 -> 767.98
   in lockstep with the navbar-expand-lg -> -md change in _Layout and the
   matching JS matchMedia checks; keep all three in sync. */
@media (max-width: 767.98px) {
    /* Make sure the toggler stays on top of the drawer. */
    .app-nav .navbar-toggler {
        position: relative;
        z-index: 1052;
        border: 1px solid var(--border);
        border-radius: 10px;
        padding: .35rem .55rem;
        color: var(--text);
        background: var(--surface);
    }
    .app-nav .navbar-toggler:hover { background: var(--surface-2); }
    /* Kill the Bootstrap default toggler-icon raster - we render
       a clean SVG inside the button instead. */
    .app-nav .navbar-toggler-icon { display: none; }

    .app-nav .navbar-collapse {
        position: fixed;
        top: 0; right: 0; bottom: 0; left: 0;
        width: 100vw;
        margin: 0;
        padding: 0;
        background: var(--surface);
        display: flex !important;
        flex-direction: column;
        overflow-y: auto;
        overflow-x: hidden;
        z-index: 1051;
        transform: translateX(100%);
        transition: transform .26s cubic-bezier(.32, .72, 0, 1), visibility .26s;
        visibility: hidden;
        height: auto !important;
    }
    .app-nav .navbar-collapse.show,
    .app-nav .navbar-collapse.collapsing {
        transform: translateX(0);
        visibility: visible;
    }
    .app-nav .navbar-collapse.collapsing {
        transition: transform .26s cubic-bezier(.32, .72, 0, 1);
    }

    /* Backdrop injected by JS (see _Layout.cshtml mobileDrawer block). */
    .nav-drawer-backdrop {
        position: fixed; inset: 0;
        background: rgba(15, 23, 42, .55);
        z-index: 1050;
        opacity: 0;
        transition: opacity .25s ease;
        pointer-events: none;
    }
    .nav-drawer-backdrop.is-visible {
        opacity: 1;
        pointer-events: auto;
    }
    /* Scroll lock: via Burofolk.ScrollLock (bf-scroll-locked). */

    /* Drawer header: brand mark on the left, close X on the right.
       Sticky so it stays visible while the user scrolls a long
       item list. Width forced to 100% with align-self stretch
       guards against any parent that fails to stretch us. */
    .nav-drawer-head {
        display: flex !important; align-items: center; justify-content: space-between;
        align-self: stretch;
        width: 100%;
        padding: .9rem 1.1rem;
        border-bottom: 1px solid var(--border);
        background: var(--surface);
        position: sticky; top: 0; z-index: 2;
    }
    .nav-drawer-brand {
        display: inline-flex; align-items: center; gap: .55rem;
        text-decoration: none;
        color: var(--text);
    }
    .nav-drawer-brand span {
        font-family: "Plus Jakarta Sans", "Inter", -apple-system, system-ui, sans-serif;
        font-weight: 800;
        font-size: 15px;
        letter-spacing: -.02em;
    }
    .nav-drawer-close {
        background: transparent;
        border: 0;
        border-radius: 50%;
        width: 36px; height: 36px;
        display: inline-flex; align-items: center; justify-content: center;
        color: var(--text-muted);
        cursor: pointer;
        transition: background .15s ease, color .15s ease;
    }
    .nav-drawer-close:hover,
    .nav-drawer-close:focus-visible {
        background: var(--surface-2);
        color: var(--text);
        outline: none;
    }

    /* Navbar lists become column-stack rows. Force align-self
       stretch + width 100% so the container never shrink-wraps
       to its content (which made everything look right-shifted
       in the unbalanced layout the user reported). */
    .app-nav .navbar-collapse .navbar-nav {
        display: flex !important;
        flex-direction: column;
        align-self: stretch;
        width: 100%;
        gap: 2px;
        margin: 0 !important;
        padding: .35rem .75rem;
        border-left: 0 !important;
        align-items: stretch;
        list-style: none;
    }
    .app-nav .navbar-collapse .navbar-nav.ms-auto {
        flex-direction: column;
        align-items: stretch;
        border-top: 1px solid var(--border);
        margin-top: .5rem !important;
        padding-top: .75rem;
    }
    .app-nav .navbar-collapse .navbar-nav .nav-item {
        width: 100%;
    }

    /* Universal full-width row treatment for every link/button kind. */
    .app-nav .navbar-collapse .nav-link-pill,
    .app-nav .navbar-collapse .nav-icon-btn,
    .app-nav .navbar-collapse .user-menu,
    .app-nav .navbar-collapse .nav-inboxes-toggle {
        display: flex !important;
        width: 100%;
        align-items: center;
        justify-content: flex-start !important;
        gap: .85rem;
        padding: .7rem .9rem !important;
        border-radius: 10px;
        font-size: 13px !important;
        font-weight: 500 !important;
        height: auto !important;
        color: var(--text) !important;
        background: transparent !important;
        border: 0 !important;
        text-decoration: none;
    }
    .app-nav .navbar-collapse .nav-link-pill:hover,
    .app-nav .navbar-collapse .nav-icon-btn:hover,
    .app-nav .navbar-collapse .user-menu:hover,
    .app-nav .navbar-collapse .nav-inboxes-toggle:hover {
        background: var(--surface-2) !important;
    }
    .app-nav .navbar-collapse .nav-link-pill.is-active {
        background: color-mix(in srgb, var(--text) 7%, transparent);
        color: var(--text);
    }

    /* Icons sized identically across all rows for visual rhythm. */
    .app-nav .navbar-collapse .nav-link-pill svg,
    .app-nav .navbar-collapse .nav-icon-btn svg,
    .app-nav .navbar-collapse .user-menu svg {
        width: 18px;
        height: 18px;
        flex: 0 0 auto;
    }
    /* Counts sit inline NEXT to their label (not floated to the
       far right edge) - the user noted "2 far away from
       Workspaces" looked disconnected. The right edge is
       reserved for genuine row chevrons / actions. */
    .app-nav .navbar-collapse .nav-pill-count,
    .app-nav .navbar-collapse .nav-bell-count,
    .app-nav .navbar-collapse .nav-approvals-count {
        position: static;
        margin-left: .15rem;
        transform: none;
        box-shadow: none;
    }

    /* The icon-strip's icons normally don't have labels - inject one
       via ::after using the title attribute so each row reads
       cleanly in the drawer ("Activity", "Mentions", "Approvals"). */
    .app-nav .navbar-collapse .nav-icon-btn::after {
        content: attr(title);
        flex: 1 1 auto;
        text-align: left;
    }

    /* User menu: avatar + display name on a single row, dropdown
       opens inline below it. */
    .app-nav .navbar-collapse .user-menu .user-email {
        display: inline-block !important;
        font-weight: 500;
        font-size: 13px;
        color: var(--text) !important;
        max-width: none !important;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        flex: 1 1 auto;
        min-width: 0;
    }
    .app-nav .navbar-collapse .user-menu .user-avatar {
        width: 32px !important;
        height: 32px !important;
        font-size: 12px !important;
        flex: 0 0 auto;
    }
    /* Drawer has its own close X - the page-level hamburger is
       redundant once the drawer is up. Hide it while the drawer
       is open so the brand wordmark is the only thing in that
       corner. */
    body.nav-drawer-open .app-nav .navbar-toggler { display: none; }
    /* Bootstrap's dropdown-toggle ::after triangle adds visual
       noise next to "Inboxes" in the drawer. Items expand inline
       below their parent so the affordance is obvious. */
    .app-nav .navbar-collapse .dropdown-toggle::after { display: none !important; }
    /* The nav-workspaces icon wrapper is 14px on desktop; the
       drawer rule blows the svg up to 18px which leaves the
       spinner (positioned on the wrapper) sized smaller and
       off-center. Resize the wrapper so spinner + icon stay
       aligned on a single visual ring. */
    .app-nav .navbar-collapse .nav-workspaces-icon {
        width: 18px;
        height: 18px;
    }
    /* Mentions row reads as "Mentions  No unread mentions" - the
       muted "0" pill in between is redundant noise. Hide the
       count badge when the bell isn't in its has-unread state. */
    .app-nav .navbar-collapse .nav-bell:not(.has-unread) .nav-bell-count {
        display: none;
    }

    /* The icon strip in the navbar uses `title` attributes for
       hover tooltips on desktop - we render those as labels in
       the drawer via `::after { content: attr(title) }`. But the
       desktop tooltip text ("No unread mentions", "Toggle dark
       mode", "n items awaiting your decision") is verbose for a
       menu row. Override per-icon with semantic, terse labels. */
    .app-nav .navbar-collapse .nav-bell::after {
        content: 'Mentions';
    }
    .app-nav .navbar-collapse #themeToggle::after {
        content: 'Dark mode';
    }
    html[data-theme="dark"] .app-nav .navbar-collapse #themeToggle::after {
        content: 'Light mode';
    }
    .app-nav .navbar-collapse .nav-approvals::after {
        content: 'Approvals';
    }
    /* `.nav-chat` collapsed-drawer rules moved to chat.css. */

    /* The Workspaces nav-link sync pulse was retired across the app;
       no responsive override needed anymore. */

    /* Premium user row: avatar + name (top) + email (bottom) +
       chevron at the right edge. Slightly inset card feel via
       the surface-2 background and rounded corners - it visually
       anchors the bottom of the drawer. */
    .app-nav .navbar-collapse .user-menu {
        margin: .25rem .35rem;
        padding: .75rem 1rem !important;
        background: var(--surface-2) !important;
        border-radius: 12px;
        gap: .85rem;
    }
    .app-nav .navbar-collapse .user-menu:hover {
        background: var(--surface-2) !important;
    }
    .app-nav .navbar-collapse .user-stack {
        display: flex;
        flex-direction: column;
        flex: 1 1 auto;
        min-width: 0;
        gap: 1px;
    }
    .app-nav .navbar-collapse .user-stack .user-name {
        display: block !important;
        font-weight: 600;
        font-size: 13px;
        color: var(--text) !important;
        line-height: 1.3;
        overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    .app-nav .navbar-collapse .user-stack .user-email-sub {
        display: block;
        font-size: 11px;
        color: var(--text-muted);
        line-height: 1.3;
        overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    .app-nav .navbar-collapse .user-menu .user-chev {
        display: inline-flex;
        flex: 0 0 auto;
        color: var(--text-muted);
        margin-left: auto;
    }
    /* The user-menu nav-item now has its own card treatment, so
       drop the hairline border and extra padding-top added
       earlier. */
    .app-nav .navbar-collapse .navbar-nav .nav-item.dropdown:has(.user-menu) {
        margin-top: .65rem !important;
        border-top: 0;
        padding-top: 0;
    }

    /* Inline dropdowns - no float, no shadow, just an indented list. */
    .app-nav .navbar-collapse .dropdown-menu {
        position: static !important;
        transform: none !important;
        width: 100%;
        margin: .15rem 0 .35rem;
        padding: .15rem 0 .15rem 2.4rem;
        background: transparent !important;
        border: 0 !important;
        box-shadow: none !important;
    }
    .app-nav .navbar-collapse .dropdown-menu .dropdown-item {
        padding: .55rem .75rem;
        border-radius: 8px;
        font-size: 13px;
        color: var(--text);
    }
    .app-nav .navbar-collapse .dropdown-menu .dropdown-item:hover {
        background: var(--surface-2);
    }
    .app-nav .navbar-collapse .dropdown-divider {
        margin: .25rem 0;
        border-color: var(--border);
    }
    /* Approvals dropdown body: full-width like other dropdowns. */
    .app-nav .navbar-collapse .approvals-menu {
        width: 100% !important;
        padding-left: 0 !important;
        margin-top: .35rem;
    }

    /* Pin the user-menu dropdown to the bottom of the drawer so the
       sign-out path is always reachable without scrolling. The
       `:has()` selector is supported in every modern browser at
       this breakpoint. */
    .app-nav .navbar-collapse .navbar-nav .nav-item.dropdown:has(.user-menu) {
        margin-top: .5rem !important;
        border-top: 1px solid var(--border);
        padding-top: .75rem;
    }
}

.app-nav .nav-bell { position: relative; text-decoration: none; }
.app-nav .nav-bell.has-unread { color: var(--primary); }

/* Live "new invitation arrived" toast. Pops from the bottom-right when
   the approvals poll detects an invitation arrived since the last
   tick. Click Refresh to reload (so the new invite card renders on
   the page they're already on); dismiss to ignore - the badge in
   the navbar still counts the items. */

/* Toast notifications (server flashes + client Burofolk.toast(...))
   live in their own concern-isolated module: paint in
   `wwwroot/css/toast.css` (.bf-toast*), behaviour in
   `wwwroot/js/toast.js`. Deliberately NOT here, and deliberately a
   frosted NEUTRAL surface - never a kind-tinted "pasted background"
   banner. See CONVENTIONS.md "Notifications & status surfaces". */

.invite-toast {
    position: fixed; bottom: 1.25rem; right: 1.25rem;
    display: flex; align-items: center; gap: .65rem;
    padding: .65rem .85rem;
    background: var(--surface);
    border: 1px solid color-mix(in srgb, var(--primary) 30%, var(--border));
    border-radius: 12px;
    box-shadow: 0 14px 36px rgba(15, 23, 42, .14);
    color: var(--text);
    font-size: 13px;
    z-index: 2000;
    opacity: 0;
    transform: translateY(8px);
    transition: opacity .22s ease, transform .22s ease;
    max-width: min(420px, calc(100vw - 2rem));
}
.invite-toast.is-visible { opacity: 1; transform: translateY(0); }
.invite-toast > svg { color: var(--primary); flex: 0 0 auto; }
.invite-toast .btn { padding: .25rem .65rem; font-size: 12px; }
.invite-toast-dismiss {
    background: transparent; border: 0; padding: 0;
    width: 22px; height: 22px;
    color: var(--text-muted);
    cursor: pointer; font-size: 14px; line-height: 1;
    border-radius: 6px;
}
.invite-toast-dismiss:hover { background: var(--surface-2); color: var(--text); }
/* Activity: same icon-button skin as the bell. Becomes primary-colored
   while on the /Activity page so users know they're on it. */
.app-nav .nav-activity { text-decoration: none; }
.app-nav .nav-activity.is-active { color: var(--text); background: color-mix(in srgb, var(--text) 7%, transparent); border-color: color-mix(in srgb, var(--text) 25%, transparent); }
/* The pill anchors to the top-right CORNER of the icon button (negative
   offsets so it sits outside the bell glyph, not on top of it). At
   16x16 the bell only has ~9px of padding around it inside a 34x34
   button - placing the pill INSIDE that padding made it cover the
   bell's clapper and rendered the "0" unreadable. Shifting it outward
   by a couple of pixels keeps the bell fully visible while still
   visually attaching the pill to that icon. */
.app-nav .nav-bell-count {
    position: absolute;
    /* INSIDE the button vertically (top: 0, never negative): the old
       -3px overhang poked above the bar, and anything that sits or
       paints above it - announcement band, frame edge, tablet
       browser chrome - cut the number in half (user, iPad:
       "partial bell number"). The corner overlap over the glyph is
       the standard badge grammar; the surface ring separates them. */
    top: 0; right: -2px;
    min-width: 14px; height: 14px;
    padding: 0 4px;
    border-radius: 999px;
    /* Monochrome ink pill, clean edge (user settled here after
       trying red: "use the black color for the number, this was
       nice"). text/surface swap per theme so both modes carry the
       same high-contrast figure; NO surface ring - the fat white
       halo interpolating with the bg was the ugly part - and the
       inside-the-button placement keeps it clear of the glyph. */
    background: var(--text);
    color: var(--surface);
    font-size: 10px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    line-height: 14px;
    text-align: center;
    pointer-events: none;
    letter-spacing: 0;
    z-index: 1;
}
/* Reserve breathing room to the right of any nav-icon-btn carrying
   a count pill. The pill is positioned at right: -4px (outside the
   button so it doesn't cover the glyph), which means the visual
   rightmost edge of the icon-with-pill sits ~11px beyond the
   button's box. Without an explicit margin, the next nav button
   lands right against the pill and gets visually overlapped by it.
   :has() generalises the clearance to every pill-carrier (bell,
   chat, future signals) so the toolbar stays consistent without
   per-icon overrides. */
.app-nav .nav-icon-btn:has(.nav-bell-count) {
    margin-right: 6px;
}
/* Zero state: NO pill. A "0" badge reads as noise - it draws the eye
   to a corner that has nothing to report. The pill renders only when
   the icon enters its has-unread state (a real count arrived); at rest
   the corner is clean. The element stays in the DOM (display:none, not
   removed) so the `:has(.nav-bell-count)` clearance below keeps the
   right margin reserved and the toolbar never shifts horizontally when
   a signal appears.

   Generalised across EVERY nav-icon-btn that carries a count pill (bell,
   chat, future signals) so both surfaces hide their zero state with one
   rule. */
.app-nav .nav-icon-btn:not(.has-unread) .nav-bell-count {
    display: none;
}

/* Approvals icon: same anchor + pill geometry as the bell, but the
   accent color is reserved for when something's actually pending (the
   icon is hidden entirely otherwise, so the .has-pending state is the
   only one that ever renders). */
.app-nav .nav-approvals { position: relative; text-decoration: none; color: var(--primary); }
.app-nav .nav-approvals.has-pending { color: var(--primary); }
.app-nav .nav-approvals .nav-approvals-count {
    background: #ef4444;
    box-shadow: 0 0 0 2px var(--surface);
}

/* Approvals dropdown menu - card with a small header, scrollable list of
   pending items, and a row hover that highlights the whole entry. Width
   is comfortable for the longest workspace label without forcing
   wrapping; max-height keeps a 20-item queue scrollable rather than
   running off the bottom of the viewport. */
.approvals-menu {
    width: 460px;
    max-width: calc(100vw - 1rem);
    padding: 0;
    overflow: hidden;
}
.approvals-menu-header {
    display: flex; align-items: center; justify-content: space-between;
    padding: .6rem .8rem;
    border-bottom: 1px solid var(--border);
}
.approvals-menu-header h6 { margin: 0; font-size: 11px; font-weight: 600; color: var(--text); letter-spacing: .02em; }
.approvals-menu-count {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 20px; height: 20px; padding: 0 .4rem;
    border-radius: 999px;
    background: #ef4444; color: #fff;
    font-size: 10px; font-weight: 700;
}
/* Zero-CLS: the dropdown opens on a one-line "Loading…" and the
   decision cards land after the ?handler=Decisions fetch. Reserve a
   realistic body height so the open menu never grows when the
   cards arrive; loading / empty states center inside the frame. The
   body is also a .nbx-pop-body, so a card here is the popover card. */
.approvals-menu-body { min-height: 180px; max-height: 400px; overflow-y: auto; display: flex; flex-direction: column; }
.approvals-menu-body > * { flex: 0 0 auto; }
.approvals-menu-body .nbx-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.approvals-menu-loading,
.approvals-menu-empty { padding: .9rem .8rem; text-align: center; flex: 1 1 auto; display: flex; align-items: center; justify-content: center; }

/* Chat widget styles moved to wwwroot/css/chat.css. */

/* (The approvals menu's own row rules are gone - 2026-09-15: the menu
   renders the ONE decision card, .nbx-card, at the popover scale.) */

/* Theme toggle: show sun in light mode, moon in dark mode */
.app-nav .nav-icon-btn#themeToggle .moon { display: none; }
html[data-theme="dark"] .app-nav .nav-icon-btn#themeToggle .sun { display: none; }
html[data-theme="dark"] .app-nav .nav-icon-btn#themeToggle .moon { display: block; }
html[data-theme="dark"] .app-nav .nav-icon-btn#themeToggle { color: var(--warning); }
.app-nav .user-menu { display: inline-flex; align-items: center; gap: .55rem; padding: .25rem .5rem !important; border-radius: 8px; }

/* Live typing indicator. Tiny pill rendered under a comment
   composer when another viewer is composing on the same page.
   Hidden by default - the JS owner reveals it once the hub
   broadcasts at least one matching typer. Stays out of the
   compose flow visually (small text, muted colour, no border)
   so it doesn't compete with the textarea. */
.bf-typing-indicator {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    padding: .25rem .6rem;
    margin-top: .35rem;
    font-size: 11px;
    color: var(--text-muted);
    line-height: 1.2;
    /* Zero-CLS: the pill's box is reserved from the first frame. */
    min-height: 1.44rem;
}
/* Zero-CLS: the hub reveals the pill mid-view, after paint. Keep
   the hidden slot IN FLOW (invisible, same box) so toggling it can
   never push the content below the composer around. */
.bf-typing-indicator[hidden] { visibility: hidden; }
.bf-typing-dots {
    display: inline-flex;
    align-items: center;
    gap: 2px;
}
.bf-typing-dots span {
    display: inline-block;
    width: 4px; height: 4px;
    border-radius: 50%;
    background: currentColor;
    opacity: .35;
    animation: bf-typing-bounce 1.2s infinite ease-in-out;
}
.bf-typing-dots span:nth-child(2) { animation-delay: .15s; }
.bf-typing-dots span:nth-child(3) { animation-delay: .30s; }
@keyframes bf-typing-bounce {
    0%, 80%, 100% { opacity: .25; transform: translateY(0); }
    40%           { opacity: .85; transform: translateY(-2px); }
}

/* LAYOUT + size only. Fill / hue / weight / color come from the
   canonical `.bf-avatar-letter` rule. */
/* 30px matches the strip's 30px icon buttons - at 28 the circle read as
   the one odd one out in the chrome row (control-height ladder pass). */
.app-nav .user-avatar { width: 30px; height: 30px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size: 11px; flex: 0 0 auto; }
/* User name in the navbar pill is hidden by default - the avatar
   is enough on desktop, and the dropdown header repeats the name
   + email anyway. The drawer override (mobile rules) re-shows it
   alongside the email subline. */
.app-nav .user-email { display: none; }
.app-nav .dropdown-menu-end .dropdown-header { line-height: 1.4; padding: .5rem 1rem; }

/* Main shell. position:relative makes .app-main a containing block, so any
   absolutely-positioned page content is held inside the shell instead of
   leaking up to the document.

   .app-main IS the view frame (2026-07-29): one framed card per page,
   equal breathing room on all four sides (the single-value margin
   shorthand is the contract - AppFrameTests pins it), surface paint,
   radius, hairline ring + very subtle drop shadow. Because the locked
   shell already scrolls every page INSIDE this element (or a child),
   the frame never moves while content scrolls and the scrollbar stays
   inside the card. Paint properties ONLY here: transform / filter /
   backdrop-filter / contain would re-anchor every position:fixed
   popover into the frame's clip (the fixed-popover doctrine). */
.app-main {
    position: relative;
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    margin: var(--app-frame-gap);
    /* The page tone: EVERY page stands on the ladder's L1 (owner, 2026-09-24:
       "apply style to all pages"); its content sits in L2 cards. */
    --bf-page: var(--bf-l1);
    background: var(--bf-page);
    border-radius: var(--app-frame-radius);
    box-shadow: var(--app-frame-shadow);
}
/* The canvas the frame floats on. Scoped to the app-shell bodies
   (_Layout stamps is-app / is-inbox on every page it renders) so
   _AuthLayout pages and Layout=null fragment/popup documents keep
   their own paint. */
body.is-app,
body.is-inbox {
    /* Depth, the Apple way (Loïc 2026-07-29: flat gray read
       "sovietic"): two barely-there brand auras kissing the top of the
       shell over a soft vertical falloff onto the --app-canvas base.
       Alphas stay whisper-low - the aura must read as expensive air,
       never as color blocks. The chrome (strip / rail / footer) is
       TRANSPARENT over this so the whole shell shares ONE continuous
       paint; a per-element fill would restart the gradient inside
       every chrome box and reinvent the seams this replaced.
       AppChromeCanvasTests pins the stack.
       The ladder (2026-09-24): the auras are LIGHT, not cyan, and the falloff
       stays inside the ladder's own slate hue - a cyan-leaning chrome beside
       a neutral page read as two greys that never belonged together. */
    background:
        radial-gradient(1100px 560px at 16% -10%, rgba(255, 255, 255, .55), transparent 60%),
        radial-gradient(900px 500px at 94% -14%, rgba(255, 255, 255, .4), transparent 55%),
        linear-gradient(180deg, color-mix(in srgb, #fff 25%, var(--bf-l0)) 0%, var(--bf-l0) 100%) var(--app-canvas);
}
html[data-theme="dark"] body.is-app,
html[data-theme="dark"] body.is-inbox {
    background:
        radial-gradient(1100px 560px at 16% -10%, rgba(148, 163, 184, .06), transparent 60%),
        radial-gradient(900px 500px at 94% -14%, rgba(148, 163, 184, .045), transparent 55%),
        linear-gradient(180deg, color-mix(in srgb, #1e293b 35%, var(--bf-l0)) 0%, var(--bf-l0) 100%) var(--app-canvas);
}
/* ── Pinned page headers ──────────────────────────────────────────
   The platform grammar for "any scrollable page keeps its orientation
   header" (Loïc 2026-07-29: scrolling /Work or /Workspaces lost every
   control past the first screen). Wrap the page's header block in
   .bf-pinned-head (or tag an existing one): it sticks to the top of
   the frame's internal scroller and rows dissolve under its surface
   backdrop (--surface, never --bg - the tokens differ in dark mode).

   flow-root: a child's top margin must not collapse through the
   wrapper, or the sticky box pins with a transparent gap above it and
   rows ghost through while scrolled. z-index 6 clears page content
   (rows, chips, fades at 3) and stays below the nav (200) and every
   popover band. Pinned by PinnedPageHeadersTests. */
/* A pinned head sits ON the page: it reads the page tone (--bf-page, the
   surface ladder's L1), never the card's white. */
.bf-pinned-head {
    position: sticky;
    top: 0;
    z-index: 6;
    display: flow-root;
    background: var(--bf-page, var(--surface));
}
/* Phone cut (720px, the platform's mobile cut): the moat is wasted
   space on a small screen - zeroing the tokens sends the frame
   full-bleed everywhere at once, including the rail composition in
   app-shell.css that consumes the same gap token. 0px, not 0: the
   rail's calc(72px + var(--app-frame-gap)) needs a length. */
@media (max-width: 720px) {
    :root {
        --app-frame-gap: 0px;
        --app-frame-radius: 0px;
        --app-frame-shadow: none;
    }
}
/* `scrollbar-gutter: stable` reserves the scrollbar's width
   ALWAYS, regardless of whether the page is tall enough to need
   one. Without this, every navigation from a short page to a
   tall page (or vice versa) shifts the entire layout by ~17px
   - the user's CLS complaint. Reserving the gutter is a
   one-line non-negotiable for any scroll container that holds
   page-level content. Pinned by AppPageNoLayoutShiftTests.

   position:relative is the load-bearing invariant: this is the ONLY scroll
   container for page content, and it MUST be a containing block. Without it an
   absolutely-positioned descendant (a visually-hidden checkbox, a popover, any
   `position:absolute` element that lacks a positioned ancestor) anchors to the
   document instead, lands at its deep in-flow position, and stretches html's
   scroll height past the viewport. The locked shell then has a phantom scroll
   runway: the moment such an element takes focus the browser scrolls the whole
   nav/main/footer out of view to "reveal" it, and an in-place form swap re-pins
   that bad offset. Anchoring every absolute descendant to this scroller keeps
   it clipped and scrolling with the content where it belongs. */
/* The frame owns ONE vertical scroller and never a horizontal one (owner,
   2026-09-16, a rail under the Documents grid: "we are not supposed to have a
   horizontal scrollbar at the bottom. Same rule for all workspaces and all
   pages we render in this frame"). With overflow-y set, an unset overflow-x
   computed to auto and any stray pixel of width painted a rail; wide content
   (a table, a lane row, a code block) scrolls inside its own container. */
.app-page { position: relative; flex: 1 1 auto; min-height: 0; overflow-x: hidden; overflow-y: auto; scrollbar-gutter: stable; }
.app-page-inner { max-width: 1440px; margin: 0 auto; padding: 2rem 2rem; }

/* "Shared" badge on viewer account rows - low-weight pill right next to
   the account's email so the user instantly sees which accounts are theirs
   vs. which are ones they were invited to. */
.account-shared-badge {
    display: inline-block;
    margin-left: .55rem;
    padding: .05rem .45rem;
    font-size: 9px;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    background: var(--primary-bg);
    color: var(--primary);
    border-radius: 999px;
    vertical-align: middle;
}

/* Access rows (pending invites / teams / send audit). Born in the
   manage-access modal, now rendered by the Detail page's inline
   Access & permissions category - the modal itself retired
   2026-08-05, and its section-head/count pill styles went with it. */
.share-rows {
    display: flex; flex-direction: column;
}
/* No card inside a card (owner, 2026-10-07): the rows stand in the Access chapter's card, so each is a row of it - no
   plate, no frame - parted from the next by ONE hairline. */
.share-row {
    display: flex; align-items: center; gap: .75rem;
    padding: .55rem 0;
    background: transparent;
    border: 0;
    border-radius: 0;
}
.share-row + .share-row { border-top: 1px solid var(--border); }
/* Share row user avatar: LAYOUT + size only. The fill / hue /
   typography come from the canonical `.bf-avatar-letter` rule
   the avatar helper stamps on every letter bubble. Pinned by
   AvatarSingleSourceOfTruthTests. */
.share-row-avatar {
    flex: 0 0 auto;
    width: 36px; height: 36px;
    display: inline-flex; align-items: center; justify-content: center;
    border-radius: 50%;
    font-size: 12px;
    text-transform: uppercase;
    overflow: hidden;
}
.share-row-avatar-img { object-fit: cover; }
/* Team-grant row icon. NOT a user avatar - team icons take the
   workspace-icon visual language (tinted brand chip), so the
   reserved "avatar" naming MUST be avoided. The JS renders an
   <img> when the team has an uploaded icon (icon == ":upload");
   otherwise a first-letter initial. */
.share-row-team-icon {
    flex: 0 0 auto;
    width: 36px; height: 36px;
    border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    background: color-mix(in srgb, var(--primary) 18%, var(--surface));
    color: var(--primary);
    border: 1px solid color-mix(in srgb, var(--primary) 30%, var(--border));
    font-weight: 700; font-size: 12px;
    text-transform: uppercase;
    overflow: hidden;
}
.share-row-team-icon img {
    width: 100%; height: 100%;
    object-fit: cover;
    display: block;
}
.share-row-badge-team {
    color: var(--primary);
    background: var(--primary-bg);
    border-color: color-mix(in srgb, var(--primary) 30%, var(--border));
}
.share-row-text {
    flex: 1 1 auto; min-width: 0;
    display: flex; flex-direction: column; gap: 2px;
    line-height: 1.3;
}
.share-row-label {
    font-weight: 600; color: var(--text); font-size: 13px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    display: inline-flex; align-items: center; gap: .45rem;
}
.share-row-meta {
    font-size: 11px; color: var(--text-muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.share-row-badge {
    flex: 0 0 auto;
    font-size: 9px; font-weight: 700; letter-spacing: .04em;
    text-transform: uppercase;
    color: #b45309;
    background: color-mix(in srgb, #f59e0b 18%, var(--surface));
    border: 1px solid color-mix(in srgb, #f59e0b 35%, var(--border));
    padding: 1px 7px;
    border-radius: 999px;
    line-height: 1.4;
}
.share-row-badge-good {
    color: #065F46;
    background: color-mix(in srgb, #10B981 14%, var(--surface));
    border-color: color-mix(in srgb, #10B981 35%, var(--border));
}
.share-row-badge-bad {
    color: #991B1B;
    background: color-mix(in srgb, #DC2626 14%, var(--surface));
    border-color: color-mix(in srgb, #DC2626 35%, var(--border));
    cursor: help;
}
html[data-theme="dark"] .share-row-badge-good { color: #6EE7B7; }
html[data-theme="dark"] .share-row-badge-bad { color: #FCA5A5; }

/* "Admin" role badge in the Manage modal - primary tint to mark a
   row that carries admin rights, distinct from the warmer "pending"
   amber and the green/red status badges. */
.share-row-badge-admin {
    color: var(--primary);
    background: var(--primary-bg);
    border-color: color-mix(in srgb, var(--primary) 35%, var(--border));
}
html[data-theme="dark"] .share-row-badge-admin { color: var(--primary); }

/* ── Dashboard pending-admin-promotion cards ────────────────────
   Action-required banner pinned at the top of the home page.
   Each card stacks identity (who proposed) + copy + Accept/Decline.
   Accent color is primary so it reads as "important + welcoming",
   not "alert" - admin promotion is a positive event. */
.admin-promo-banner {
    display: flex; flex-direction: column; gap: .75rem;
    margin: 0 0 1.5rem;
}
.admin-promo-card {
    display: flex; align-items: center; gap: 1rem;
    padding: .9rem 1.1rem;
    background: linear-gradient(135deg,
        color-mix(in srgb, var(--primary) 9%, var(--surface)),
        var(--surface));
    border: 1px solid color-mix(in srgb, var(--primary) 30%, var(--border));
    border-radius: 12px;
    box-shadow: 0 4px 12px -4px color-mix(in srgb, var(--primary) 22%, transparent);
}
.admin-promo-card-icon { flex: 0 0 auto; }
.admin-promo-avatar {
    width: 40px; height: 40px;
    border-radius: 50%;
}
.admin-promo-card-text { flex: 1 1 auto; min-width: 0; line-height: 1.4; }
.admin-promo-card-title {
    font-size: 13px;
    color: var(--text);
}
.admin-promo-card-sub {
    margin-top: .2rem;
    font-size: 11px;
    color: var(--text-muted);
}
.admin-promo-card-actions {
    flex: 0 0 auto;
    display: inline-flex; gap: .4rem; align-items: center;
}
@media (max-width: 720px) {
    .admin-promo-card { flex-direction: column; align-items: flex-start; gap: .55rem; }
    .admin-promo-card-actions { align-self: stretch; justify-content: flex-end; }
}
.share-row-actions {
    flex: 0 0 auto;
    display: inline-flex; align-items: center; gap: .35rem;
}
.share-invite-form { /* nothing extra - the modal-body baseline is enough */ }

/* New-workspace (type picker) page ------------------------------------ */
/* 1440px lets the workspace-type grid fit THREE generous-
   width cards per row (each ~440px after gaps + padding) -
   the layout the page is now meant to express. The old
   880px cap forced two columns regardless of viewport size.
   Narrow viewports still collapse gracefully because the
   inner grid uses repeat(auto-fit, minmax(380px, 1fr)). */
/* ── Workspace pickers: /Workspaces/New (the kinds), /Workspaces/New/Inbox
   (the providers), /Workspaces/New/Documents (the storage posture). ONE
   grammar (owner, 2026-09-13, on the kinds page: "make the page more
   designed ... user onboarding & discovery"): a centred hero - eyebrow,
   title, ONE lead line -, then a 3-column grid of cards. A card that can
   act IS the door (an <a>): the bare type glyph, the name, one tagline,
   ONE short description line, and the ink "Set up" chip pinned to its
   foot; the whole card lifts on hover. A kind that cannot act yet sits in
   the quiet "Coming soon" strip below, a tile with no control. */
.ws-new-page { max-width: 1120px; margin: 0 auto; padding: 2.25rem 1.25rem 3rem; }
.ws-new-hero { text-align: center; max-width: 600px; margin: 0 auto 1.75rem; }
.ws-new-eyebrow {
    margin: 0 0 .45rem;
    font-size: 11px; font-weight: 700; letter-spacing: .06em;
    text-transform: uppercase; color: var(--text-muted);
}
.ws-new-hero .page-title { font-size: 28px; letter-spacing: -.02em; line-height: 1.15; }
.ws-new-lead { margin: .55rem 0 0; font-size: 13px; color: var(--text-muted); }
/* The sibling pickers keep a left-aligned header with a back link to the
   kinds page. */
.ws-new-header { margin-bottom: 1.5rem; }
.ws-new-header .btn-back {
    display: inline-flex; align-items: center; gap: .25rem;
    color: var(--text-muted); text-decoration: none;
    font-size: 12px; margin-bottom: .85rem;
}
.ws-new-header .btn-back:hover { color: var(--text); }
.ws-new-grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 16px;
}
@media (max-width: 1100px) { .ws-new-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 640px)  { .ws-new-grid { grid-template-columns: minmax(0, 1fr); } }
/* Canonical workspace-TYPE glyph - the type's shape (Inbox, Issues, Wiki,
   Documents, ...) as a clean stroked icon, emitted by the ONE renderer
   WorkspaceTypeCatalog.Glyph for every surface (picker card, tenant
   create-gate checkbox, type chips). NEVER a filled tile: the premium look
   is the glyph + its colour, no pasted background. Colour rides
   currentColor (so chips / checkboxes inherit their own muted / active /
   checked state). Size is owned by the host surface's `<scope> svg` rule;
   the value here is only the unscoped fallback. Sibling of .bf-icon (the
   per-workspace instance bubble). */
.bf-type-glyph { flex: 0 0 auto; display: block; width: 1.25rem; height: 1.25rem; }
/* Nav-pill consumers (e.g. the Contacts cluster) render the type glyph at the
   14px pill size, matching the inline glyphs in the sibling nav dropdowns. */
.nav-link-pill .bf-type-glyph { width: 14px; height: 14px; }

.ws-type-card {
    display: flex; flex-direction: column; gap: 12px;
    padding: 20px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    color: inherit; text-decoration: none;
    transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease;
}
a.ws-type-card:hover {
    color: inherit; text-decoration: none;
    border-color: color-mix(in srgb, var(--text) 28%, transparent);
    box-shadow: 0 14px 32px rgba(15, 23, 42, .08);
    transform: translateY(-2px);
}
a.ws-type-card:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
.ws-type-card.is-coming-soon { color: var(--text-muted); }
.ws-type-card.is-coming-soon .ws-type-icon,
.ws-type-card.is-coming-soon .ws-type-text h3,
.ws-type-card.is-coming-soon .ws-type-tagline { color: var(--text-muted); }
.ws-type-head { display: flex; align-items: center; gap: 14px; }
/* The picker-card icon: a bare monochrome glyph, NO filled tile (colour
   signals STATE, not decoration; pinned by WorkspaceTypeGlyphUnificationTests). */
.ws-type-icon {
    flex: 0 0 auto;
    width: 40px; height: 40px;
    color: var(--text);
    display: inline-flex; align-items: center; justify-content: center;
}
.ws-type-icon svg { width: 28px; height: 28px; }
.ws-type-text { min-width: 0; }
.ws-type-text h3 {
    margin: 0;
    font-size: 15px; font-weight: 700;
    color: var(--text); letter-spacing: -.01em;
    display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
}
.ws-type-tag {
    font-size: 9px; font-weight: 700; letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--text-muted);
    background: var(--surface-2);
    border: 1px solid var(--border);
    padding: 1px 7px; border-radius: 999px;
    line-height: 1.4;
}
.ws-type-tagline { margin: 2px 0 0; font-size: 13px; font-weight: 500; color: var(--text); }
/* ONE short line under the name: two lines of room reserved so the cards
   of a row keep one foot, and the text may still grow - never clipped. */
.ws-type-desc {
    margin: 0;
    font-size: 12px; line-height: 1.5; color: var(--text-muted);
    min-height: 36px;
}
.ws-type-cta {
    display: inline-flex; align-items: center; gap: 6px;
    align-self: flex-start; margin-top: auto;
    height: 30px; padding: 0 12px;
    border-radius: 8px;
    background: var(--text); color: var(--surface, #fff);
    font-size: 12px; font-weight: 600; line-height: 1;
    transition: background .15s ease;
}
.ws-type-cta svg { width: 14px; height: 14px; transition: transform .15s ease; }
a.ws-type-card:hover .ws-type-cta { background: color-mix(in srgb, var(--text) 84%, var(--surface)); }
a.ws-type-card:hover .ws-type-cta svg { transform: translateX(2px); }
/* Article cards (the storage-posture picker) keep a button row at the foot
   and say a locked state in ONE muted line, never a disabled control. */
.ws-type-action { margin-top: auto; }
.ws-type-action .btn { font-size: 12px; padding: .4rem .9rem; }
.ws-type-locked { display: inline-flex; align-items: center; min-height: 30px; font-size: 12px; color: var(--text-muted); }

/* The "Coming soon" strip: a section label in the console's eyebrow
   grammar, then tiles - glyph, name, tagline - in the muted register. */
.ws-new-soon { margin-top: 2rem; }
.ws-new-soon-head {
    margin: 0 0 .6rem .2rem;
    font-size: 11px; font-weight: 700; letter-spacing: .05em;
    text-transform: uppercase; color: var(--text-muted);
}
.ws-new-soon-grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 12px;
}
@media (max-width: 1100px) { .ws-new-soon-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 640px)  { .ws-new-soon-grid { grid-template-columns: minmax(0, 1fr); } }
.ws-soon-tile {
    display: flex; align-items: center; gap: 12px;
    padding: 12px 14px;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: color-mix(in srgb, var(--text) 2%, var(--surface));
}
.ws-soon-tile .ws-type-icon { width: 32px; height: 32px; color: var(--text-muted); }
.ws-soon-tile .ws-type-icon svg { width: 20px; height: 20px; }
.ws-soon-text { display: flex; flex-direction: column; min-width: 0; }
.ws-soon-name { font-size: 13px; font-weight: 600; color: color-mix(in srgb, var(--text) 72%, transparent); }
.ws-soon-tagline { font-size: 12px; color: var(--text-muted); }

/* New-workspace form (issues today; future types reuse). Sits inside
   .ws-new-page so it picks up the centered max-width and header
   spacing automatically. */
.ws-new-form {
    display: flex; flex-direction: column;
    gap: .25rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 1.25rem 1.5rem;
}
.ws-new-form .form-label { font-weight: 600; margin-bottom: .25rem; color: var(--text); }
.ws-new-form .form-control { margin-bottom: .25rem; }
.ws-new-form .form-hint { color: var(--text-muted); font-size: 12px; margin: 0 0 1rem; }
.ws-new-actions {
    display: flex; gap: .5rem; justify-content: flex-end;
    margin-top: .5rem;
}
.ws-new-error {
    margin-bottom: 1rem;
    padding: .65rem .85rem;
    background: color-mix(in srgb, #ef4444 8%, var(--surface));
    border: 1px solid color-mix(in srgb, #ef4444 30%, var(--border));
    border-radius: 8px;
    color: var(--text);
    font-size: 12px;
}

/* The create-knowledge-base ASSISTANT (/Workspaces/NewWiki, issue #451):
   the house assistant grammar (assistant.css) hosted INLINE on the page -
   the railed frame (#582: the journey on the left, the step the one
   scroller, the foot pinned) at ONE height whatever the step, every foot
   control on ONE height. */
.ws-wikiassist {
    max-width: 860px;
    height: min(520px, calc(100vh - 220px)); height: min(520px, calc(100dvh - 220px));
    min-height: 420px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 18px 20px 20px;
}
.ws-wikiassist .ws-wikiassist-name { max-width: 100%; margin: 0; }
.ws-wikiassist .documents-incassist-foot > .btn {
    height: 32px; display: inline-flex; align-items: center; margin: 0;
}
/* Type badge on the workspace list/manage card. Compact pill that
   tells the user "this is an Inbox" at a glance - prepares the UI
   for mixed-type lists once more connectors land. */
.ws-type-badge {
    display: inline-flex; align-items: center; gap: .3rem;
    padding: 1px 8px;
    font-size: 9px; font-weight: 700; letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--primary);
    background: var(--primary-bg);
    border: 1px solid color-mix(in srgb, var(--primary) 25%, var(--border));
    border-radius: 999px;
    line-height: 1.5;
    vertical-align: middle;
}
.ws-type-badge svg { width: 11px; height: 11px; }

/* The local-time pill left the strip for the Home hero (its rules
   live with the hero in home-waiting.css). */

/* Provider badge sibling to .ws-type-badge. Quieter (slate, not
   primary) so the type badge stays the focal chip; the provider
   badge reads as supplementary "and the source of this inbox is
   X" context. The icon is the inline brand mark from
   InboxProviderCatalog so Gmail / Microsoft / IMAP read at a glance. */
.ws-provider-badge {
    display: inline-flex; align-items: center; gap: .3rem;
    margin-left: .3rem;
    padding: 1px 8px;
    font-size: 9px; font-weight: 600; letter-spacing: .02em;
    color: var(--text-muted);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 999px;
    line-height: 1.5;
    vertical-align: middle;
}
.ws-provider-badge-icon { display: inline-flex; align-items: center; }
.ws-provider-badge-icon svg { width: 11px; height: 11px; }

/* Team invitation chip-picker rendered inside each owned team card. Same
   chip-input as Collaborators / Workspaces - the layout glue around it
   is per-host. */
.team-invite-form { margin-top: .65rem; }
.team-invite-chip-input { width: 100%; }
.team-invite-submit-row {
    display: flex; align-items: center; justify-content: space-between;
    gap: .5rem; margin-top: .5rem;
}
.team-invite-submit-row .form-text { margin: 0; flex: 1 1 auto; font-size: 11px; }

/* Per-team pending-outgoing-invitations strip. Reads as a tertiary
   panel - quieter than the member list, but visible enough to remind
   the owner who they're still waiting on. */

/* Leave-team button on member-of cards. Right-aligned at the bottom
   of the card body so it doesn't visually compete with the member
   roster above. */
/* Leave-team footer: separated from the member list by a soft rule and
   right-aligned so the action reads as a card-level operation, not a
   per-row one. The button itself is a quiet ghost so it doesn't compete
   with the primary content. */
/* Layout for the chip-picker invite row inside the Manage modal. The
   outer `.chip-input.share-chip-input` reuses the same chip styling as
   the Collaborators page; this just stacks the action button + form-text
   below it so the chip area can wrap freely. */
.share-chip-input { width: 100%; }
.share-invite-submit-row {
    display: flex; align-items: center; justify-content: space-between;
    gap: .75rem; margin-top: .5rem;
}
.share-invite-submit-row .form-text { margin: 0; flex: 1 1 auto; }
@media (max-width: 540px) {
    .share-invite-submit-row { flex-direction: column-reverse; align-items: stretch; }
    .share-invite-submit-row .btn { width: 100%; }
}

/* Legacy classes - kept until any remaining render path is migrated to
   the new .share-row layout above. */
.share-list {
    list-style: none; padding: 0; margin: 0;
    display: flex; flex-direction: column; gap: .35rem;
}
.share-list li {
    display: flex; align-items: center; justify-content: space-between;
    gap: .75rem;
    padding: .5rem .7rem;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 8px;
}
.share-list-main { display: flex; flex-direction: column; min-width: 0; }
.share-list-main strong { font-weight: 600; color: var(--text); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.share-list-meta { font-size: 10px; color: var(--text-muted); }
.share-list-actions { display: inline-flex; gap: .35rem; flex: 0 0 auto; }

/* The invite "link copied / delivery failed" flash now goes through
   the canonical toast (`Burofolk.toast.success/.error`) - one
   notification surface, never a parallel filled-background banner. */

/* ── Bootstrap .alert variants, re-skinned to the house style ──────
   Legacy inline `@Model.Flash` / form-error banners on Admin + Auth
   pages still render through Bootstrap's `.alert alert-*` classes,
   which paint a pasted pastel fill. Re-skin them ONCE here (site.css
   loads after bootstrap, so this wins) to the premium look: a NEUTRAL
   surface + hairline border, with the kind colour carried by a thin
   left accent + the text only - never a tinted card fill. New flashes
   should go through the canonical toast (`this.ToastSuccess(...)`).
   See CONVENTIONS.md "Notifications & status surfaces". */
.alert-success, .alert-danger, .alert-warning, .alert-info {
    --bs-alert-bg: var(--surface);
    --bs-alert-border-color: var(--border);
    --bs-alert-color: var(--text);
    background: var(--surface);
    border: 1px solid var(--border);
    border-left-width: 3px;
    color: var(--text);
    border-radius: 10px;
    box-shadow: var(--shadow-sm, 0 1px 2px rgba(15, 23, 42, .04));
}
.alert-success { border-left-color: var(--success); }
.alert-danger  { border-left-color: var(--danger); }
.alert-warning { border-left-color: var(--warning); }
.alert-info    { border-left-color: var(--primary); }
.alert-success .alert-heading { color: var(--success); }
.alert-danger  .alert-heading { color: var(--danger); }
.alert-warning .alert-heading { color: var(--warning); }
.alert-info    .alert-heading { color: var(--primary); }

/* Bootstrap's list-group default is 0.375rem = 5.25px at our 14px root -
   the one vendor corner off the even radius ladder (RadiusScaleTripwireTests).
   Pin it to the neighbouring ladder step. */
.list-group { --bs-list-group-border-radius: 6px; }

/* The app's public shell (_AuthLayout: sign in, sign up, the doors that come
   before the app). Minimal (owner, 2026-10-25): the page's own colour, ONE
   quiet door back to the website at the top, the mark, the form standing
   on the page - no card chrome, no footer. */
.auth-body {
    min-height: 100vh;
    display: flex; flex-direction: column;
    background: var(--bg);
}
.auth-top {
    flex: 0 0 auto;
    display: flex; align-items: center;
    padding: 16px 20px;
}
.auth-back {
    display: inline-flex; align-items: center; gap: 6px;
    height: 30px; padding: 0 10px;
    border: 1px solid var(--border); border-radius: 8px;
    color: var(--text-muted);
    font-size: 12px; font-weight: 500; line-height: 1.3;
    text-decoration: none;
    transition: color .12s ease, border-color .12s ease;
}
.auth-back:hover { color: var(--text); border-color: var(--border-strong); }
.auth-back svg { flex: 0 0 auto; display: block; }
[dir="rtl"] .auth-back svg { transform: scaleX(-1); }
.auth-main {
    flex: 1 1 auto;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    padding: 0 16px 72px;
    /* html/body are `overflow: hidden` globally (the authenticated app
       shell manages its own internal scroll regions), so THIS is the
       only place an auth-shell page can scroll from. */
    overflow-y: auto;
}
.auth-brand-icon {
    flex: 0 0 auto;
    display: block;
    margin: 24px 0 20px;
}
.auth-card {
    width: 100%; max-width: 360px;
    padding: 0 0 8px;
}
.auth-card h1 {
    margin: 0 0 .4rem;
    font-size: 1.35rem; font-weight: 700;
    letter-spacing: -.02em;
    text-align: center;
}
/* A heading with no lead line under it (sign in) keeps the lead's air. */
.auth-card > h1 + :not(.auth-lead) { margin-top: 1.5rem; }
.auth-lead {
    color: var(--text-muted);
    font-size: 12px;
    margin: 0 0 1.5rem;
    line-height: 1.5;
    text-align: center;
}
/* The sign-in providers (#546, _SignInProviders): Continue with Google,
   Continue with Microsoft, Single sign-on - one hairline button each, the
   provider's own mark at the left, stacked 8px apart. Google and Microsoft
   stand only while Burofolk's app for them is configured. */
.auth-providers {
    display: flex; flex-direction: column; gap: 8px;
}
.auth-provider {
    display: inline-flex; align-items: center; justify-content: center;
    text-decoration: none;
    gap: .65rem;
    width: 100%;
    padding: .65rem 1rem;
    background: var(--surface);
    color: var(--text);
    border: 1px solid var(--border-strong);
    border-radius: 10px;
    font-weight: 600; font-size: 13px;
    cursor: pointer;
    transition: border-color .15s ease, background .15s ease, transform .12s ease;
    font-family: inherit;
}
.auth-provider:hover {
    border-color: var(--text-muted);
    background: var(--surface-2);
    color: var(--text);
}
.auth-provider:active { transform: translateY(0.5px); }
.auth-provider-glyph {
    flex: 0 0 auto;
    width: 18px; height: 18px;
    display: inline-flex; align-items: center; justify-content: center;
}

/* The password slot's other state (#546, auth-realm.js): the address belongs
   to an organisation that signs in through its provider. Its door takes the
   password field's place in the SAME box (the script holds the slot's
   height), a glyph and one line, the way back to the password as a quiet
   hairline button when the organisation only offers single sign-on. */
.auth-sso-door { display: flex; flex-direction: column; gap: .35rem; }
.auth-sso-door[hidden], .auth-sso-password[hidden] { display: none; }
.auth-sso-label { font-size: 11px; font-weight: 600; color: var(--text); }
.auth-sso-instead { background: none; border: 0; padding: 0; cursor: pointer; font-family: inherit; }
.auth-sso-target {
    display: flex; align-items: center; gap: 8px; min-width: 0;
    padding: .55rem .75rem;
    border: 1px solid var(--border-strong); border-radius: 8px;
    background: var(--surface-2); color: var(--text);
    font-size: 13px; font-weight: 600; line-height: 1.25; text-decoration: none;
}
.auth-sso-target.is-link:hover { border-color: var(--text-muted); color: var(--text); }
.auth-sso-name { min-width: 0; overflow-wrap: anywhere; }
.auth-sso-password { display: flex; flex-direction: column; gap: .35rem; }

/* The single sign-on doors (#546): /sso, /Auth/SignInRequired, the test result - one glyph over the
   title, the providers as the auth strip, the test's facts as aligned key/value rows. */
.sso-door-card { text-align: center; }
.sso-door-card .auth-providers, .sso-door-card .auth-form, .sso-door-card .alert { text-align: left; }
.sso-door-glyph, .sso-test-mark {
    display: inline-grid; place-items: center; width: 44px; height: 44px; margin-bottom: 10px; color: var(--text);
}
.sso-door-glyph svg, .sso-test-mark svg { width: 26px; height: 26px; }
.sso-test-card { text-align: center; }
.sso-test-mark.is-ok { color: var(--success, #16a34a); }
.sso-test-mark.is-failed { color: var(--danger, #dc2626); }
.sso-test-facts { list-style: none; margin: 14px 0 0; padding: 0; text-align: left; display: flex; flex-direction: column; gap: 8px; }
.sso-test-facts li { display: flex; gap: 12px; justify-content: space-between; font-size: 13px; }
.sso-test-key { color: var(--text-muted); flex: 0 0 auto; }
.sso-test-val { min-width: 0; overflow-wrap: anywhere; text-align: right; color: var(--text); font-weight: 600; }
.sso-test-mono { font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace); font-weight: 500; font-size: 12px; }

/* "or" divider between SSO and email/password. Two thin rules with
   muted text in the middle, gentle and short so it reads as a soft
   alternative rather than a hard mode switch. */
.auth-divider {
    display: flex; align-items: center; gap: .65rem;
    margin: 1rem 0;
    color: var(--text-subtle);
    font-size: 10px;
    text-transform: uppercase; letter-spacing: .08em;
}
.auth-divider::before,
.auth-divider::after {
    content: "";
    flex: 1 1 auto;
    height: 1px;
    background: var(--border);
}

/* The 2FA challenge (/Auth/Login/2fa). A glyph plate over the title, ONE lead
   line, a segmented six-cell code (LTR in every script), the ink Verify, and
   the other doors as quiet hairline buttons under a hairline - never a bare
   underlined word, never a Bootstrap alert. */
.tf-card { text-align: center; }
.tf-card h1 { margin-bottom: .35rem; }
.tf-plate {
    display: inline-flex; align-items: center; justify-content: center;
    width: 52px; height: 52px;
    margin: 0 0 1rem;
    border-radius: 14px;
    color: var(--text);
    background: color-mix(in srgb, var(--text) 6%, var(--surface));
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 10%, transparent);
}
.tf-lead { margin-bottom: 1.35rem; }
.tf-lead strong { color: var(--text); font-weight: 600; }
.tf-form { gap: .9rem; }
.tf-cells {
    display: flex; justify-content: center; gap: 8px;
    direction: ltr;
}
.tf-cell {
    width: 44px; height: 52px;
    padding: 0;
    border: 1px solid var(--border-strong);
    border-radius: 10px;
    background: var(--surface);
    color: var(--text);
    font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
    font-size: 1.35rem; font-weight: 600;
    text-align: center;
    caret-color: var(--primary);
    transition: border-color .12s ease, box-shadow .12s ease;
}
.tf-cell:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 3px var(--primary-bg);
}
.tf-recovery-input {
    width: 100%;
    height: 52px;
    padding: 0 1rem;
    border: 1px solid var(--border-strong);
    border-radius: 10px;
    background: var(--surface);
    color: var(--text);
    font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
    font-size: 16px; letter-spacing: .12em;
    text-align: center; text-transform: uppercase;
    direction: ltr;
}
.tf-recovery-input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-bg); }
.tf-recovery-input::placeholder { color: var(--text-subtle); letter-spacing: .12em; }
.tf-cells.is-hidden, .tf-recovery.is-hidden, .tf-door.is-hidden { display: none; }
.tf-verify { height: 40px; }
/* A status is a glyph and coloured text on a transparent container. */
.auth-status {
    display: flex; align-items: flex-start; justify-content: center; gap: 6px;
    font-size: 12px; line-height: 1.45;
    color: var(--text-muted);
    text-align: start;
}
.auth-status svg { flex: 0 0 auto; margin-top: 2px; }
.auth-status.is-error { color: var(--danger); }
/* The other ways in: ONE-LINE rows under the card's hairline, aligned by
   construction - glyph, words, and a caret where the row unfolds. Never a
   strip of pills (owner, 2026-09-13: "never display stacked buttons like
   that"): three labels in a long language cannot share a 420px line, and a
   ragged column is what wrapping makes of them. A row holds its own words at
   every width, in every language. */
.tf-doors {
    margin-top: 1.25rem; padding-top: .5rem;
    border-top: 1px solid var(--border);
    text-align: start;
}
.tf-door-form { display: contents; }
.tf-door {
    display: flex; align-items: center; gap: 10px;
    width: 100%; min-height: 42px;
    padding: 9px 10px;
    border: 0; border-radius: 8px;
    background: transparent;
    color: var(--text-muted);
    font-family: inherit; font-size: 12px; font-weight: 500;
    text-align: start;
    cursor: pointer;
    transition: color .12s ease, background .12s ease;
}
.tf-door:hover { color: var(--text); background: color-mix(in srgb, var(--text) 4%, transparent); }
.tf-door:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--primary-bg); }
.tf-door-glyph { flex: 0 0 auto; display: inline-flex; color: var(--text-subtle); }
.tf-door:hover .tf-door-glyph { color: var(--text-muted); }
.tf-door-label { flex: 1 1 auto; min-width: 0; line-height: 1.35; }
.tf-door-caret { flex: 0 0 auto; display: inline-flex; color: var(--text-subtle); transition: transform .15s ease; }
/* Open wears THE selection mark: the soft ink wash and its hairline ring. */
.tf-door[aria-expanded="true"] {
    color: var(--text);
    background: color-mix(in srgb, var(--text) 7%, var(--surface));
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 10%, transparent);
}
.tf-door[aria-expanded="true"] .tf-door-caret { transform: rotate(180deg); }
/* "Which app?" unfolds under its own row: nothing at rest, and the cells
   above it never move (the card grows downwards). */
.tf-apps { padding: .35rem 10px .25rem; }
.tf-apps.is-hidden { display: none; }
/* The apps that make the code, THE renderer (Pages/Shared/_AuthenticatorApps).
   ONE muted line, then the names in ONE quiet plate, a line each - no glyph
   repeated down the side (owner, 2026-09-13: "We hate rounded icons like
   that"), nothing to click, nothing to hover. */
.authapps-lead {
    margin: 0 0 .6rem;
    color: var(--text-muted);
    font-size: 12px; line-height: 1.5;
}
.authapps-list {
    margin: 0; padding: 8px 12px;
    list-style: none;
    border-radius: 10px;
    background: color-mix(in srgb, var(--text) 4%, transparent);
}
.authapps-name {
    min-width: 0;
    padding: 2px 0;
    color: var(--text);
    font-size: 12px; font-weight: 500; line-height: 1.5;
}
@media (max-width: 400px) {
    .tf-cell { width: 38px; height: 46px; font-size: 1.2rem; }
}

/* ============================================================
   Two-factor pages: Setup2fa, Disable2fa, RecoveryCodes share the
   same hero + card layout. Styles live here (not page-scoped) so
   /Auth/Profile/RecoveryCodes wasn't rendering as plain HTML when
   the user landed on it without `?fresh=1` (the `<style>` block
   in Setup2fa.cshtml was page-scoped and didn't apply elsewhere).
   ============================================================ */
.totp-setup-page { max-width: 880px; margin: 0 auto; padding: 1.5rem 1.25rem 3rem; }
.totp-back {
    display: inline-flex; align-items: center; gap: .35rem;
    padding: .35rem .55rem .35rem .25rem;
    margin-bottom: 1.25rem;
    border-radius: 8px;
    color: var(--text-muted); text-decoration: none;
    font-size: 12px;
}
.totp-back:hover { color: var(--text); background: var(--surface-2); }

.totp-hero { text-align: center; margin-bottom: 2rem; }
.totp-hero-icon {
    display: inline-flex; align-items: center; justify-content: center;
    width: 56px; height: 56px;
    margin-bottom: 1rem;
    border-radius: 10px;
    background: linear-gradient(135deg, color-mix(in srgb, var(--primary) 18%, var(--surface)),
                                        color-mix(in srgb, #06B6D4 15%, var(--surface)));
    color: var(--primary);
    border: 1px solid color-mix(in srgb, var(--primary) 25%, var(--border));
}
.totp-hero h1 {
    font-family: "Plus Jakarta Sans", "Inter", -apple-system, system-ui, sans-serif;
    font-weight: 800;
    font-size: clamp(1.6rem, 3vw, 2rem);
    letter-spacing: -.025em;
    margin: 0 0 .4rem;
    color: var(--text);
}
.totp-hero-sub {
    margin: 0 auto;
    max-width: 540px;
    color: var(--text-muted);
    line-height: 1.55;
}

.totp-error {
    margin-bottom: 1rem;
    padding: .75rem 1rem;
    border-radius: 12px;
    border: 1px solid color-mix(in srgb, var(--danger) 35%, var(--border));
    background: color-mix(in srgb, var(--danger) 8%, var(--surface));
    color: color-mix(in srgb, var(--danger) 80%, var(--text));
    font-size: 13px;
}

.totp-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(330px, 1fr));
    gap: 1.25rem;
}
.totp-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 16px;
    padding: 1.5rem;
}
.totp-card + .totp-card { margin-top: 1rem; }
.totp-step-label {
    display: inline-block;
    padding: 2px 10px;
    margin-bottom: .9rem;
    border-radius: 999px;
    background: var(--primary-bg);
    color: var(--primary);
    font-size: 10px; font-weight: 700;
    letter-spacing: .06em; text-transform: uppercase;
}
.totp-card h2 {
    font-family: "Plus Jakarta Sans", "Inter", -apple-system, system-ui, sans-serif;
    font-weight: 700; font-size: 15px;
    letter-spacing: -.01em;
    margin: 0 0 .35rem; color: var(--text);
}
.totp-card-sub {
    margin: 0 0 1.1rem;
    color: var(--text-muted);
    font-size: 12px; line-height: 1.55;
}
.totp-form { display: flex; flex-direction: column; gap: .85rem; }
.totp-tip { margin: 0; font-size: 11px; color: var(--text-muted); line-height: 1.5; }

/* Two-factor security card on /Auth/Profile. Same surface treatment
   as the profile-card above it; just adds a header row with the
   Enabled/Off badge, a row layout for the per-control rows, and a
   muted hint line for the "off" state. */
.security-head {
    display: flex; align-items: center; justify-content: space-between;
    gap: 1rem;
    margin-bottom: 1rem;
}
.security-title {
    font-family: "Plus Jakarta Sans", "Inter", -apple-system, system-ui, sans-serif;
    font-weight: 700; font-size: 15px;
    letter-spacing: -.01em;
    margin: 0 0 .15rem;
    color: var(--text);
}
.security-tagline {
    margin: 0;
    color: var(--text-muted);
    font-size: 12px; line-height: 1.5;
}
/* Status indicator - NOT a pill. No filled background, no pill
   radius, no uppercase shout: the state reads through the icon +
   colour + weight, matching the app-wide no-filled-badge
   vocabulary. */
.security-badge {
    display: inline-flex; align-items: center; gap: .35rem;
    padding: 0;
    background: transparent;
    border: 0;
    font-size: 12px; font-weight: 600;
    line-height: 1.4;
    flex: 0 0 auto;
}
.security-badge-on {
    color: var(--success);
}
.security-badge-off {
    color: var(--text-muted);
}
.security-row {
    display: flex; align-items: center; justify-content: space-between;
    gap: 1rem;
    padding: .75rem 0;
    border-top: 1px solid var(--border);
}
.security-row:last-child { padding-bottom: 0; }
.security-row-text { display: flex; flex-direction: column; line-height: 1.4; }
.security-row-text strong { color: var(--text); font-weight: 600; font-size: 13px; }
.security-row-meta { color: var(--text-muted); font-size: 11px; margin-top: 1px; }
.security-hint {
    margin: .8rem 0 0;
    font-size: 11px; color: var(--text-muted); line-height: 1.5;
}

/* ── Profile: MCP token self-service ─────────────────────── */
.profile-mcp-fresh {
    /* Primary-tinted hairline pulls the eye to the one-time secret; the box
       hugs its content instead of stretching across the card. */
    border: 1px solid color-mix(in srgb, var(--primary) 45%, var(--border));
    border-radius: 10px;
    padding: .8rem .9rem;
    margin: .9rem 0;
    width: fit-content;
    max-width: 100%;
}
.profile-mcp-fresh strong { font-size: 13px; }
.profile-mcp-note { color: var(--text-muted); font-size: 11px; margin: .25rem 0 .35rem; }
.profile-mcp-cmd {
    display: flex; align-items: center; gap: .4rem;
    border: 1px solid var(--border); border-radius: 8px;
    padding: .45rem .6rem;
    width: fit-content;
    max-width: 100%;
}
.profile-mcp-cmd + .profile-mcp-note { margin-top: .55rem; }
.profile-mcp-cmd code {
    flex: 1; overflow-x: auto; white-space: nowrap;
    font-size: 11px;
}
/* Copying IS the point of these blocks - the canonical hover-hidden rest
   state would read as "no copy button" to a regular member. */
.profile-mcp-cmd .copy-text-btn { opacity: 1; flex: 0 0 auto; }
.profile-mcp-create { margin: .9rem 0 .35rem; }
.profile-mcp-create-row { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
.profile-mcp-create-row input[type="text"] { flex: 1 1 12rem; max-width: 22rem; }
.profile-mcp-write-opt {
    display: flex; align-items: baseline; gap: .45rem;
    margin: .55rem 0 0;
    font-size: 12px; cursor: pointer;
}
.profile-mcp-write-hint { display: block; color: var(--text-muted); font-size: 11px; }
.profile-mcp-rows { margin-top: .6rem; }
.profile-mcp-inactive { opacity: .55; }
/* Expiring within a week: the date itself warns - no badge, no fill. */
.profile-mcp-expiring { color: var(--warning, #b45309); font-weight: 600; }

/* The on-request connect assistant inside the show-once card. */
.profile-mcp-caret { flex: 0 0 auto; transition: transform .15s ease; color: var(--text-muted); }
.profile-mcp-guide { margin-top: .65rem; }
.profile-mcp-guide > summary {
    list-style: none; cursor: pointer;
    display: inline-flex; align-items: center; gap: .45rem;
    font-size: 12px; font-weight: 600; color: var(--text);
}
.profile-mcp-guide > summary::-webkit-details-marker { display: none; }
.profile-mcp-guide[open] > summary .profile-mcp-caret { transform: rotate(90deg); }
.profile-mcp-guide-body { margin-top: .5rem; }
.profile-mcp-clients { display: flex; flex-direction: column; margin-top: .45rem; }
/* No card inside a card (owner, 2026-10-07): the clients are sections of the profile card, one hairline between two. */
.profile-mcp-client { padding: .55rem 0; }
.profile-mcp-client + .profile-mcp-client { border-top: 1px solid var(--border); }
.profile-mcp-client > summary {
    list-style: none; cursor: pointer;
    display: flex; align-items: center; gap: .55rem;
}
.profile-mcp-client > summary::-webkit-details-marker { display: none; }
.profile-mcp-client[open] > summary .profile-mcp-caret { transform: rotate(90deg); }
.profile-mcp-client-head { display: flex; flex-direction: column; line-height: 1.35; }
.profile-mcp-client-title { font-size: 12px; font-weight: 600; color: var(--text); }
.profile-mcp-client-sub { font-size: 10px; color: var(--text-muted); }
.profile-mcp-client-body { margin: .55rem 0 .1rem; }
.profile-mcp-steps {
    margin: .55rem 0 .1rem; padding-left: 1.15rem;
    display: flex; flex-direction: column; gap: .5rem;
    font-size: 12px; color: var(--text); line-height: 1.5;
}
.profile-mcp-steps .profile-mcp-cmd { margin-top: .35rem; }
.profile-mcp-cmd code.profile-mcp-cmd-block { white-space: pre; line-height: 1.5; }
/* Prose blocks (the paste-into-Claude-Code message) wrap instead of
   scrolling sideways - a one-line command scrolls, a sentence reads. */
.profile-mcp-cmd-wrap { max-width: 46rem; }
.profile-mcp-cmd-wrap code.profile-mcp-cmd-block { white-space: pre-wrap; word-break: break-word; overflow-x: visible; }
/* The terminal alternative sits under the steps, visually secondary. */
.profile-mcp-alt { margin-top: .65rem; padding-top: .6rem; border-top: 1px solid var(--border); }

/* ── /oauth/authorize - the MCP connector consent card ────── */
.mcpauth-card { max-width: 30rem; }
.mcpauth-title { font-size: 1.25rem; font-weight: 700; margin: 0 0 .35rem; }
.mcpauth-sub { color: var(--text); font-size: 13px; margin: 0 0 .5rem; }
.mcpauth-note { color: var(--text-muted); font-size: 11px; line-height: 1.5; margin: .35rem 0; }
.mcpauth-org-label { font-size: 11px; font-weight: 600; color: var(--text); }
.mcpauth-org-single { margin: .75rem 0; display: flex; align-items: baseline; gap: .5rem; font-size: 13px; }
.mcpauth-orgs { border: 0; padding: 0; margin: .75rem 0; }
.mcpauth-orgs legend { padding: 0; margin-bottom: .35rem; }
.mcpauth-org {
    display: flex; align-items: center; gap: .55rem;
    border: 1px solid var(--border); border-radius: 8px;
    padding: .5rem .7rem; margin-bottom: .4rem;
    font-size: 12px; cursor: pointer;
}
.mcpauth-write { margin: .65rem 0 .4rem; }
.mcpauth-error { color: var(--danger); font-size: 11px; line-height: 1.5; margin: .35rem 0 0; }
.mcpauth-actions { display: flex; justify-content: flex-end; gap: .5rem; margin-top: .9rem; }

.auth-form { display: flex; flex-direction: column; gap: 1rem; }
.auth-field { display: flex; flex-direction: column; gap: .35rem; }
.auth-field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
@media (max-width: 480px) { .auth-field-row { grid-template-columns: 1fr; } }
.auth-field label {
    font-size: 11px; font-weight: 600;
    color: var(--text);
}
.auth-label-row {
    display: flex; justify-content: space-between; align-items: center;
}
.auth-minor-link {
    font-size: 11px;
    color: var(--primary);
    text-decoration: none;
    font-weight: 500;
}
.auth-minor-link:hover { text-decoration: underline; }
.auth-optional { font-weight: 400; color: var(--text-muted); }
.auth-field input {
    width: 100%;
    padding: .55rem .75rem;
    border: 1px solid var(--border-strong);
    border-radius: 8px;
    background: var(--surface);
    color: var(--text);
    font-size: 13px;
    transition: border-color .12s ease, box-shadow .12s ease;
}
.auth-field input:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 3px var(--primary-bg);
}
.auth-hint {
    font-size: 10px;
    color: var(--text-muted);
    line-height: 1.4;
}
.auth-alt {
    margin-top: 1.25rem;
    padding-top: 1.25rem;
    border-top: 1px solid var(--border);
    font-size: 12px;
    color: var(--text-muted);
    text-align: center;
}
.auth-alt a { color: var(--primary); font-weight: 600; text-decoration: none; }
.auth-alt a:hover { text-decoration: underline; }
/* ── Service banner ════════════════════════════════════════════════
   Operator-controlled message above the page content. Hairline
   amber stroke (no fill, no rounded pill) per the design-system
   canonical "border + text only" rule for chrome controls. Renders
   only when /Admin/Parameters has a non-empty serviceBannerMessage.
   The text reads as ambient information - if it were a critical
   alert it would be a modal, not a footer-style ribbon. */
.app-service-banner {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 20px;
    border-top: 1px solid rgba(217, 119, 6, .28);
    border-bottom: 1px solid rgba(217, 119, 6, .28);
    background: transparent;
    color: #b45309;
    font-size: 13px;
    font-weight: 500;
    line-height: 1.45;
}
.app-service-banner-glyph {
    flex: 0 0 auto;
    color: var(--warning);
}
.app-service-banner-text {
    flex: 1 1 auto;
    min-width: 0;
    white-space: pre-line;
}
html[data-theme="dark"] .app-service-banner {
    border-top-color: rgba(251, 191, 36, .32);
    border-bottom-color: rgba(251, 191, 36, .32);
    color: var(--warning);
}
html[data-theme="dark"] .app-service-banner-glyph { color: var(--warning); }

/* ── Policy banner ─────────────────────────────────────────────────
   The two-factor requirement's grace notice (_TwoFactorGraceBanner):
   the service banner's strip in the chrome's own ink - hairlines, no
   fill - with ONE ink verb at its end. A notice about the reader's own
   account, not a platform alarm, so it never borrows the amber. */
.app-policy-banner {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 8px 20px;
    border-top: 1px solid var(--border);
    border-bottom: 1px solid var(--border);
    background: transparent;
    color: var(--text);
    font-size: 13px;
    font-weight: 500;
    line-height: 1.45;
}
.app-policy-banner-glyph { flex: 0 0 auto; color: var(--text-muted); }
.app-policy-banner-text { flex: 1 1 auto; min-width: 0; }
.app-policy-banner-verb { flex: 0 0 auto; white-space: nowrap; }
@media (max-width: 600px) {
    /* The verb drops under the sentence and lines up with its first letter,
       never under the glyph. */
    .app-policy-banner { flex-wrap: wrap; row-gap: 8px; }
    .app-policy-banner-text { flex-basis: calc(100% - 27px); }
    .app-policy-banner-verb { margin-inline-start: 27px; }
}

/* ── "View as" (#545) ──────────────────────────────────────────────
   The banner joins the policy strip's grammar: the viewed person's avatar,
   one sentence, the read-only mark (glyph + word, never a pill), the spaces
   door and the ONE ink verb out. A tinted wash marks the borrowed view so it
   can never be mistaken for the reader's own session. */
.app-viewas-banner { background: color-mix(in srgb, var(--text) 5%, transparent); }
.app-viewas-avatar { --bf-avatar-size: 24px; width: 24px; height: 24px; flex: 0 0 auto; font-size: 11px; }
.app-viewas-mark {
    flex: 0 0 auto; display: inline-flex; align-items: center; gap: 6px;
    color: var(--text-muted); font-size: 12px; font-weight: 600; white-space: nowrap;
}
.app-viewas-spaces { flex: 0 0 auto; white-space: nowrap; }
@media (max-width: 600px) {
    .app-viewas-banner .app-policy-banner-text { flex-basis: calc(100% - 36px); }
    .app-viewas-mark { margin-inline-start: 36px; }
    .app-viewas-banner .app-policy-banner-verb { margin-inline-start: 0; }
}
/* The view's page: the person, one line, their spaces as doors (each by ITS icon). */
.viewas-page { display: flex; flex-direction: column; max-width: 720px; }
.viewas-head { display: flex; align-items: center; gap: 14px; margin-bottom: 20px; }
.viewas-head-avatar { --bf-avatar-size: 44px; width: 44px; height: 44px; font-size: 18px; flex: 0 0 auto; }
.viewas-head-lead { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.viewas-head-title { margin: 0; font-size: 22px; font-weight: 700; letter-spacing: -.01em; line-height: 1.2; color: var(--text); }
.viewas-head-line { margin: 0; font-size: 13px; color: var(--text-muted); }
.viewas-spaces { display: flex; flex-direction: column; gap: 8px; }
.viewas-space {
    display: flex; align-items: center; gap: 12px; min-width: 0;
    padding: 12px 14px; border: 1px solid var(--border); border-radius: 12px;
    background: var(--surface); color: var(--text); text-decoration: none;
}
.viewas-space:hover { border-color: var(--border-strong); color: var(--text); background: var(--surface-2); }
.viewas-space-icon { --bf-icon-size: 28px; width: 28px; height: 28px; flex: 0 0 auto; }
.viewas-space-name { flex: 1 1 auto; min-width: 0; font-size: 14px; font-weight: 600; overflow-wrap: anywhere; }
.viewas-space-chevron { width: 16px; height: 16px; flex: 0 0 auto; color: var(--text-muted); }
.ws-person-viewas { margin: 0 0 14px; }
.ws-person-viewas .btn { display: inline-flex; align-items: center; gap: 8px; }

/* ── App footer ════════════════════════════════════════════════════
   Three-zone layout: brand identity (left), utility links (center,
   auto-margin), credit + copyright (right). Chrome-is-the-canvas
   (2026-07-29, AppChromeCanvasTests): the bar paints the same
   --app-canvas the view frame floats on, with NO top seam - the strip,
   the rail, the footer and the moat are ONE material, and the content
   card is the only surface. The old border-top + brand-gradient
   hairline died with the seams (in rail mode the gradient STARTED at
   x=72, a hard mid-screen stop).

   CONTRACT: the footer renders on EVERY signed-in page and stays
   VISIBLE at all times, exactly like the top nav. The locked-viewport
   shell (html/body overflow:hidden + body flex column) pins both by
   construction; every page scrolls INSIDE a container between them
   (.app-page, .archive-layout panes, or .app-main for the
   is-issues-*-flow modes) so the scrollbar always ends at the
   footer's top edge - it never runs the full screen height over the
   footer. No page may hide the footer, let it scroll out of view,
   or move its scrolling to the document. */
/* The footer PARSES before <main> (streaming a large page used to let the
   flexed main fill this band mid-parse, then shrink one footer height when
   the footer arrived - the whole framed card jumped); this order rule keeps
   it rendered last, at the bottom of the locked body column, exactly where
   it always was. Pinned by AppFooterStreamingOrderSourcePinTests.
   And the footer rests at the column's END by construction: the auto top
   margin takes whatever the column has free - the whole band while main is
   not parsed yet (a throttled load painted the footer right under the strip,
   then dropped it 820px when main landed), nothing once main (flex 1 1 auto)
   is there. Pinned by AppFooterRestsAtTheBottomBeforeMainTests. */
body > footer.app-footer { order: 1; margin-top: auto; }

.app-footer {
    flex: 0 0 auto;
    position: relative;
    display: flex;
    /* Content is vertically centred and the bar keeps a stable, comfortable
       height. min-height guarantees the brand mark + credit sit centred even
       if one zone is momentarily shorter - on tablet portrait the bar used to
       read as if the (small) logo was stuck to the top edge. */
    align-items: center;
    min-height: 48px;
    gap: 1.25rem;
    padding: .8rem 1.5rem;
    font-size: 11px;
    color: var(--text-muted);
    /* Transparent, not a fill: the body's layered canvas (gradient +
       auras) must run continuously under strip, rail, moat AND bar. */
    background: transparent;
}

.app-footer-zone {
    display: inline-flex;
    align-items: center;
    gap: .65rem;
    flex: 0 0 auto;
    min-width: 0;
}
/* No center zone on purpose: bottom-center belongs to the Quick Bar's
   collapsed tab, so the footer keeps brand (left) + credit (right) with a
   flexible gap between them. */

/* Brand zone: 3-bar mark + Burofolk wordmark + italic tagline. */
.app-footer-brand {
    display: inline-flex; align-items: center; gap: .5rem;
    color: var(--text);
    text-decoration: none;
    font-family: 'Plus Jakarta Sans', 'Inter', system-ui, sans-serif;
    font-weight: 800;
    font-size: 13px;
    letter-spacing: -.015em;
    transition: opacity .12s ease;
}
.app-footer-brand:hover { color: var(--text); }
.app-footer-mark {
    display: inline-flex;
    width: 26px; height: 26px;
    flex: 0 0 auto;
    /* Springy tilt on hover - the same playful skew the old top-nav brand
       had, now that the brand lives only here. */
    transition: transform .35s cubic-bezier(.34, 1.56, .64, 1);
}
.app-footer-brand:hover .app-footer-mark { transform: rotate(-6deg) scale(1.06); }
.app-footer-mark svg { display: block; width: 100%; height: 100%; }
.app-footer-wordmark { line-height: 1; }
.app-footer-tagline {
    color: var(--text-subtle);
    font-size: 11px;
    font-style: italic;
    letter-spacing: .005em;
    padding-left: .35rem;
    border-left: 1px solid var(--surface-2);
    margin-left: .15rem;
}

/* Footer status indicator - ICON-ONLY (2026-07: the user asked for a
   discreet, textless status after the old centered label collided with
   the Quick Bar's collapsed tab; the full message rides title +
   aria-label, synced by the layout's poll). Earlier styles stay
   rejected and must not creep back: the tinted pill badge, the green
   dot with pulse/halo, AND the centered text label
   (AppFooterStatusVisualTests pins the absences). The GLYPH alone
   carries the color signal. */
.app-footer-status {
    /* The status indicator is an <a> for click-through to the
       SystemStatus page, but it must read as INFO, not as a
       call-to-action. The default browser underline + the
       global anchor color would otherwise make it look like a
       link button shoved into the footer rhythm. We strip the
       underline, inherit the muted footer color, and surface
       interactivity only on hover (text darkens, cursor pointer).
       Background stays transparent so the glyph + label sit in the
       same rhythm as the rest of the footer links. */
    display: inline-flex; align-items: center; gap: .4rem;
    padding: 0;
    background: transparent;
    color: var(--text-muted);
    font-weight: 500;
    font-size: 10px;
    line-height: 1;
    text-decoration: none;
    cursor: pointer;
    transition: color .12s ease;
}
.app-footer-status:hover,
.app-footer-status:focus-visible {
    color: var(--text);
    text-decoration: none;
    outline: none;
}
.app-footer-status:focus-visible {
    /* Keyboard focus must surface SOMETHING, but the default
       browser outline collides with the rest of the footer's
       quiet visual contract. A soft underline on focus only is
       the standard pattern: visible to keyboard users, invisible
       to mouse users. */
    text-decoration: underline;
    text-underline-offset: 3px;
    text-decoration-thickness: 1px;
}
/* Status icons: small stroke SVGs (checkmark / triangle / cross).
   All three ship in the markup, all three hidden by default.
   The is-* class on the container reveals exactly one. No fill,
   no pulse, no halo - the icon is a clean static glyph. */
.app-footer-status-icon { display: none; flex-shrink: 0; }
.app-footer-status.is-up   .app-footer-status-icon-up   { display: inline-block; color: #16a34a; }
.app-footer-status.is-warn .app-footer-status-icon-warn { display: inline-block; color: #b45309; }
.app-footer-status.is-down .app-footer-status-icon-down { display: inline-block; color: #b91c1c; }
html[data-theme="dark"] .app-footer-status.is-up   .app-footer-status-icon-up   { color: #4ade80; }
html[data-theme="dark"] .app-footer-status.is-warn .app-footer-status-icon-warn { color: var(--warning); }
html[data-theme="dark"] .app-footer-status.is-down .app-footer-status-icon-down { color: #fca5a5; }

/* Hovering the glyph slightly enlarges its hit feedback via color only
   (the base rule already darkens); the checking state renders no icon
   at all, which reads as "nothing to report yet" - quieter than a
   spinner in a strip this small. */

/* The right-hand credit zone (operator mark + copyright) was removed
   2026-08-08 at the user's request - the footer carries brand + status
   only, and its right side is deliberately empty. */
.app-footer-sep {
    width: 3px; height: 3px;
    border-radius: 50%;
    background: currentColor;
    opacity: .35;
    flex: 0 0 auto;
}

/* Responsive collapse: at narrow widths drop the tagline so the brand
   mark + status glyph fit on one row. */
@media (max-width: 900px) {
    .app-footer-tagline { display: none; }
}
@media (max-width: 720px) {
    /* ONE line on phones, brand pinned left. flex-wrap used to let a
       second zone drop to a second row, doubling the footer's height on
       the screens with the least room. */
    .app-footer { padding: .65rem .9rem; gap: .85rem; flex-wrap: nowrap; }
}
/* Theme toggle in footer */
.app-footer .theme-toggle {
    border: 1px solid var(--border); background: var(--surface); color: var(--text-muted);
    padding: .15rem .5rem; border-radius: 6px; cursor: pointer; font-size: 11px; margin-left: .5rem;
}
.app-footer .theme-toggle:hover { color: var(--text); border-color: color-mix(in srgb, var(--text) 25%, transparent); }
.app-footer kbd { font-family: ui-monospace, monospace; font-size: 10px; background: var(--surface-2); padding: 0 .25rem; border-radius: 4px; }

/* Thread count badge - minimal pill next to sender name. Keeps a low
   visual weight so it never competes with the sender/subject typography. */
.thread-count {
    display: inline-flex; align-items: center;
    flex: 0 0 auto;
    font-size: 10px; font-weight: 700;
    background: var(--surface-2); color: var(--text-muted);
    padding: 1px 7px; border-radius: 999px;
    line-height: 1.45;
    letter-spacing: .01em;
}
.mail-row.unread .thread-count { background: var(--primary-bg); color: var(--primary); }

/* Keyboard cursor on mail row */
.mail-row.is-cursor {
    background: var(--primary-bg);
    box-shadow: inset 3px 0 0 var(--primary);
}

/* Advanced search toggle + panel */
.adv-toggle {
    display: inline-flex; align-items: center; gap: .35rem;
    padding: 0 .85rem; height: 42px;
    background: var(--surface); color: var(--text-muted);
    border: 1px solid var(--border-strong); border-radius: 10px;
    font-size: 12px; font-weight: 500; cursor: pointer;
}
.adv-toggle:hover, .adv-toggle.active {
    color: var(--text); border-color: color-mix(in srgb, var(--text) 25%, transparent); background: color-mix(in srgb, var(--text) 5%, transparent);
}
.adv-panel {
    flex: 1 1 100%;
    background: var(--surface); border: 1px solid var(--border);
    border-radius: 12px; padding: 1rem 1.25rem;
    margin-top: .25rem; box-shadow: var(--shadow-sm);
}
.adv-panel .adv-grid {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: .75rem 1rem; margin-bottom: .75rem;
}
.adv-panel .adv-row { display: grid; gap: 1rem; margin-bottom: .75rem; }
.adv-panel .adv-row-2 { grid-template-columns: 1fr 1fr; }
.adv-panel .adv-dates { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; }
@media (max-width: 720px) {
    .adv-panel .adv-row-2 { grid-template-columns: 1fr; }
    .adv-panel .adv-dates { grid-template-columns: 1fr 1fr; }
}
.adv-panel .chk {
    display: inline-flex; align-items: center; gap: .5rem;
    padding: .4rem .85rem;
    background: var(--surface-2); border: 1px solid var(--border);
    border-radius: 999px; cursor: pointer;
    font-size: 12px; color: var(--text);
    transition: all .1s;
}
.adv-panel .chk:hover { border-color: color-mix(in srgb, var(--text) 25%, transparent); }
.adv-panel .chk input[type="checkbox"] { width: 14px; height: 14px; accent-color: var(--primary); margin: 0; }
.adv-panel .chk:has(input:checked) { background: var(--primary-bg); border-color: var(--primary); color: var(--primary); }

/* Sort menu */
.sort-menu { position: relative; }
.sort-menu .adv-toggle { gap: .5rem; min-width: 140px; justify-content: space-between; }
.sort-menu .adv-toggle > span { flex: 1 1 auto; text-align: left; font-weight: 500; color: var(--text); }
.sort-menu .adv-toggle svg:first-child { color: var(--text-muted); flex: 0 0 auto; }
.sort-menu .adv-toggle svg:last-child { color: var(--text-subtle); flex: 0 0 auto; transition: transform .15s ease; }
.sort-menu .adv-toggle.active svg:last-child { transform: rotate(180deg); color: var(--text); }
.sort-menu .adv-toggle.active { border-color: color-mix(in srgb, var(--text) 25%, transparent); background: color-mix(in srgb, var(--text) 7%, transparent); }

/* The sort menu opens under its button's LEFT edge: the button stands at the
   command bar's left end (Inbox · Newest), so a right-aligned 240px menu ran
   past the pane into the rail in a narrow window, its labels cut (owner,
   2026-09-18: "severe regression in mail workspace"). */
.sort-dropdown {
    position: absolute; top: calc(100% + 6px); left: 0; z-index: 30;
    background: var(--surface); border: 1px solid var(--border);
    border-radius: 12px; box-shadow: 0 12px 32px rgba(15,23,42,.15);
    min-width: 240px; padding: .4rem;
    animation: slide-down-sm .12s ease;
}
@keyframes slide-down-sm { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }
.sort-dropdown[hidden] { display: none; }
.sort-item {
    display: flex; align-items: center; gap: .6rem;
    padding: .55rem .75rem; font-size: 12px;
    color: var(--text); text-decoration: none; border-radius: 8px;
    cursor: pointer;
}
.sort-item:hover { background: var(--surface-2); }
.sort-item.active { background: color-mix(in srgb, var(--text) 7%, transparent); color: var(--text); font-weight: 600; }
.sort-item .check { width: 16px; display: inline-flex; align-items: center; justify-content: center; color: var(--primary); font-size: 12px; flex: 0 0 auto; }
.sort-item:not(.active) .check { opacity: 0; }

/* Clickable star button */
.mail-flag.star-btn {
    background: transparent; border: 0; cursor: pointer;
    padding: 4px; border-radius: 6px;
    display: flex; align-items: center; justify-content: center;
    color: inherit;
    transition: transform .12s ease, background .12s ease;
}
.mail-flag.star-btn:hover { background: var(--surface-2); transform: scale(1.15); }
.mail-flag.star-btn .star-off { color: var(--text-subtle); opacity: .4; font-size: 15px; }
.mail-flag.star-btn:hover .star-off { color: #eab308; opacity: .9; }
.mail-flag.star-btn.is-starred .star { color: #eab308; font-size: 15px; }
.mail-flag.star-btn:disabled { opacity: .5; pointer-events: none; }

/* Preferences modal */
/* Preferences - premium redesign.
   Sectioned cards, proper dividers, iOS-style toggle switches, refined selects. */
.prefs-backdrop {
    position: fixed; inset: 0; z-index: 9998;
    background: rgba(15, 23, 42, .58);
    backdrop-filter: blur(8px) saturate(1.1);
    -webkit-backdrop-filter: blur(8px) saturate(1.1);
    display: flex; align-items: center; justify-content: center;
    padding: 1rem;
    animation: fade-in .16s ease;
}
.prefs-backdrop[hidden] { display: none; }
.prefs-panel {
    width: min(580px, 100%); max-height: 88vh; overflow: hidden;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 16px;
    box-shadow: 0 30px 80px -20px rgba(15, 23, 42, .4),
                0 10px 30px -10px rgba(15, 23, 42, .25);
    display: flex; flex-direction: column;
    animation: prefs-rise .22s cubic-bezier(.2, .9, .3, 1);
}
@keyframes prefs-rise {
    from { opacity: 0; transform: translateY(10px) scale(.98); }
    to   { opacity: 1; transform: translateY(0)    scale(1);   }
}
html[data-theme="dark"] .prefs-panel {
    box-shadow: 0 30px 80px -20px rgba(0, 0, 0, .6),
                0 10px 30px -10px rgba(0, 0, 0, .45);
}

.prefs-header {
    display: flex; justify-content: space-between; align-items: center;
    padding: 1.1rem 1.5rem;
    border-bottom: 1px solid var(--border);
}
.prefs-header h3 {
    margin: 0;
    font-size: 15px; font-weight: 700;
    letter-spacing: -.015em;
    color: var(--text);
}
.prefs-close {
    width: 32px; height: 32px;
    display: inline-flex; align-items: center; justify-content: center;
    background: transparent;
    border: 1px solid transparent;
    color: var(--text-muted);
    cursor: pointer;
    border-radius: 8px;
    font-size: 13px;
    line-height: 1;
    transition: background .12s ease, color .12s ease, border-color .12s ease;
}
.prefs-close:hover { background: var(--surface-2); color: var(--text); border-color: var(--border); }

.prefs-body {
    padding: .5rem .6rem 1rem;
    overflow-y: auto;
    background: var(--surface);
}

/* Sections are plain groupings - no card-in-a-card. The outer panel is
   already a surface; sub-cards would fragment the content. A single thin
   top border is enough to separate sections (skipped on the first). */
.prefs-section {
    margin: 0;
    padding: 0;
    background: transparent;
    border: 0;
    border-radius: 0;
    box-shadow: none;
}
.prefs-section + .prefs-section {
    border-top: 1px solid var(--border);
    margin-top: .35rem;
    padding-top: .35rem;
}

/* Inline section header - icon + title. Quiet weight, left-aligned so the
   eye scans down cleanly instead of hitting a full-width gray banner per
   section. */
.prefs-section-head {
    display: flex; align-items: center; gap: .55rem;
    padding: .85rem .9rem .35rem;
    color: var(--text-muted);
}
.prefs-section-head h4 {
    margin: 0;
    font-size: 11px; font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .07em;
    color: inherit;
    line-height: 1;
}
.prefs-section-icon {
    width: 14px; height: 14px;
    opacity: .8;
    flex: 0 0 auto;
}

/* Field rows - uniform padding, subtle divider between rows. Control always
   right-aligned, label fills the remaining width and can wrap + host a
   sub-hint below without breaking the row grid. */
.prefs-field,
.prefs-check {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: .6rem .9rem;
    font-size: 12px;
    color: var(--text);
    margin: 0;
    min-height: 44px;
    border: 0;
    border-radius: 8px;
    transition: background .12s ease;
}
.prefs-field:hover,
.prefs-check:hover { background: var(--surface-2); }

.prefs-field-text {
    flex: 1 1 auto;
    min-width: 0;
    cursor: default;
    font-weight: 500;
    color: var(--text);
    line-height: 1.35;
    display: flex; flex-direction: column; gap: .15rem;
}
.prefs-field-hint {
    font-size: 11px;
    font-weight: 400;
    color: var(--text-muted);
    line-height: 1.4;
}

/* Refined select - custom chevron, focus ring, premium feel. */
.prefs-field select {
    flex: 0 0 auto;
    min-width: 180px;
    padding: .5rem 2.1rem .5rem .75rem;
    border: 1px solid var(--border-strong);
    border-radius: 8px;
    background-color: var(--surface);
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
    background-repeat: no-repeat;
    background-position: right .6rem center;
    background-size: 12px;
    color: var(--text);
    font-size: 12px;
    font-weight: 500;
    appearance: none;
    -webkit-appearance: none;
    cursor: pointer;
    transition: border-color .12s ease, box-shadow .12s ease, background-color .12s ease;
}
.prefs-field select:hover { border-color: var(--text-muted); }
.prefs-field select:focus-visible {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 3px var(--primary-bg);
}

/* Toggle switches in place of utilitarian checkboxes. */
.prefs-check { cursor: pointer; user-select: none; transition: background .12s ease; }
.prefs-check:hover { background: var(--surface-2); }
.prefs-check input[type="checkbox"] {
    appearance: none;
    -webkit-appearance: none;
    position: relative;
    width: 40px; height: 22px;
    flex: 0 0 auto;
    margin: 0;
    border: 0;
    border-radius: 999px;
    background: var(--border-strong);
    cursor: pointer;
    transition: background .18s ease;
}
.prefs-check input[type="checkbox"]::after {
    content: '';
    position: absolute;
    top: 2px; left: 2px;
    width: 18px; height: 18px;
    background: #fff;
    border-radius: 50%;
    box-shadow: 0 1px 2px rgba(15, 23, 42, .15),
                0 1px 3px rgba(15, 23, 42, .2);
    transition: transform .18s cubic-bezier(.3, 1.2, .5, 1);
}
.prefs-check input[type="checkbox"]:checked { background: var(--primary); }
.prefs-check input[type="checkbox"]:checked::after { transform: translateX(18px); }
.prefs-check input[type="checkbox"]:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px var(--primary-bg);
}
html[data-theme="dark"] .prefs-check input[type="checkbox"] { background: #475569; }
html[data-theme="dark"] .prefs-check input[type="checkbox"]::after { background: #f1f5f9; }
html[data-theme="dark"] .prefs-check input[type="checkbox"]:checked { background: var(--primary); }

.prefs-footer {
    display: flex; justify-content: space-between; gap: .75rem;
    padding: 1rem 1.5rem;
    border-top: 1px solid var(--border);
    background: var(--surface);
}
.prefs-footer .btn { font-weight: 600; }

/* Density: compact */
body.density-compact .mail-row { padding: .35rem 1.25rem; }
body.density-compact .mail-row .mail-snippet { display: inline; }
body.hide-snippet .mail-row .mail-snippet { display: none; }

/* Header meta row: labels + notes count pill */
/* Message header metadata row: action chips + assignee pill + labels
   flow tightly together on the LEFT (they're related and should read
   as one cluster); the notes count pill (if any) gets pushed right
   via margin-left:auto so it reads as the secondary "X notes" badge.
   Children that carry their own bottom-margin (assignee, labels) are
   normalized to 0 here so the flex row's gap controls the spacing. */
.msg-header-meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: .5rem .65rem;
    margin-bottom: 1.25rem;
}
/* The .notes-count-pill formerly sat as a direct child of
   .msg-header-meta with margin-left:auto. It now lives inside
   .msg-badge-cluster which carries the right-pinning, so when it's
   nested we cancel this rule to avoid a double-push. */
.msg-header-meta > .notes-count-pill { margin-left: auto; }
.msg-badge-cluster > .notes-count-pill { margin-left: 0; }
.msg-header-meta .msg-labels { margin-bottom: 0; }
.msg-badge-cluster .msg-labels { margin-bottom: 0; }
.msg-header-meta .msg-assignee { margin-bottom: 0; }
.msg-header-meta .msg-trash-btn { margin-bottom: 0; margin-right: 0; }
/* Quiet ink marker, not a chip: the same amber "annotated" grammar
   as the thread-row notes marker on the message page. It is a
   jump-link to the notes panel, so hover shows a plain underline -
   never a fill, never a glow, never a lift. */
.notes-count-pill {
    display: inline-flex; align-items: center; gap: .35rem;
    font-size: 10px; font-weight: 600;
    color: color-mix(in srgb, #b45309 85%, var(--text));
    text-decoration: none;
    white-space: nowrap;
}
.notes-count-pill:hover { text-decoration: underline; text-underline-offset: 3px; }

/* Premium team notes panel (prominent, above attachments + body) */
.notes-panel {
    margin-bottom: 1rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-left: 4px solid #10b981;
    border-radius: 10px;
    box-shadow: 0 1px 2px rgba(15,23,42,.03);
    overflow: hidden;
}
.notes-panel.has-notes { border-left-color: #10b981; }
.notes-panel.empty {
    border-left-color: var(--border-strong);
    /* Empty state is a single-line affordance - no big header, no icon
       tile, no subtitle. The collapsed trigger alone says "add a note"
       and is already self-explanatory. */
    margin-bottom: .75rem;
}
/* Hide the full header when there are no notes - the trigger below IS
   the affordance. Users don't need a header to tell them a panel with
   one line of text is a "notes panel". */
.notes-panel.empty .notes-panel-head { display: none; }

.notes-panel-head {
    display: flex; justify-content: space-between; align-items: center;
    padding: .85rem 1.25rem;
    background: linear-gradient(90deg, rgba(16,185,129,.06) 0%, rgba(6,182,212,.03) 100%);
    border-bottom: 1px solid var(--border);
    cursor: pointer; user-select: none;
    transition: background .12s ease;
}
.notes-panel-head:hover { filter: brightness(1.03); }
.notes-panel-head:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.notes-panel-right { display: flex; align-items: center; gap: .75rem; }
/* Chevron - rotates to indicate collapse state. */
.notes-panel-chev {
    display: inline-flex; align-items: center; justify-content: center;
    width: 22px; height: 22px;
    border-radius: 6px;
    color: var(--text-muted);
    transition: transform .2s ease, background .12s ease;
}
.notes-panel-head:hover .notes-panel-chev { background: rgba(15,23,42,.05); color: var(--text); }
.notes-panel.collapsed .notes-panel-chev { transform: rotate(-90deg); }
/* Body - max-height transition keeps the collapse animated without
   needing to measure content height up-front. 2000px is a generous
   ceiling that accommodates even very long note threads. */
.notes-panel-body {
    max-height: 2000px;
    overflow: hidden;
    transition: max-height .24s ease;
}
.notes-panel.collapsed .notes-panel-body {
    max-height: 0;
}
.notes-panel.collapsed .notes-panel-head { border-bottom-color: transparent; }
.notes-panel-title { display: flex; align-items: center; gap: .75rem; }
.notes-panel-icon {
    width: 34px; height: 34px; border-radius: 10px;
    background: linear-gradient(135deg, #10b981, #06b6d4);
    color: #fff; display: inline-flex; align-items: center; justify-content: center;
    flex: 0 0 auto;
}
.notes-panel.empty .notes-panel-icon { background: var(--border-strong); }
.notes-panel-label { font-size: 13px; font-weight: 700; color: var(--text); letter-spacing: -.01em; }
.notes-panel-sub { font-size: 11px; color: var(--text-muted); margin-top: .1rem; }

/* Overlapping avatar stack of contributors */
.notes-avatar-stack { display: flex; align-items: center; }
.notes-avatar-stack .notes-avatar {
    /* LAYOUT + size + stack-specific ring. Fill / hue / weight /
       color come from the canonical `.bf-avatar-letter` rule. */
    width: 28px; height: 28px; border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 10px; text-transform: uppercase;
    border: 2px solid var(--surface);
    margin-left: -6px; flex: 0 0 auto;
}
.notes-avatar-stack .notes-avatar:first-child { margin-left: 0; }

/* Notes list */
.notes-list { padding: .75rem 1.25rem .25rem; display: flex; flex-direction: column; gap: .85rem; }
/* Notion-style thread group: a root note and its replies share one
   visual container. The replies are indented with a left rule so the
   eye can follow the chain back to its root, and the per-thread reply
   form sits at the bottom of the group. */
.note-thread {
    display: flex; flex-direction: column; gap: .45rem;
    padding: 0;
}
.note-thread .note.is-reply {
    margin-left: 1.25rem;
    padding-left: .75rem;
    border-left: 2px solid var(--border);
    position: relative;
}
.note-thread .note.is-reply::before {
    content: ""; position: absolute;
    left: -2px; top: 0;
    width: 2px; height: 12px;
    background: var(--primary);
    border-radius: 1px;
    opacity: .35;
}
.note-reply-compose { margin-left: 1.25rem; }
.note-reply-btn {
    width: 22px; height: 22px;
    display: inline-flex; align-items: center; justify-content: center;
    background: transparent;
    border: 1px solid transparent;
    border-radius: 6px;
    color: var(--text-muted);
    cursor: pointer;
    padding: 0;
    transition: background .12s ease, color .12s ease, border-color .12s ease;
}
.note-reply-btn:hover { background: var(--surface-2); color: var(--text); border-color: var(--border); }
.note {
    display: flex; gap: .75rem; padding: .75rem .9rem;
    background: var(--surface-2); border-radius: 10px;
    border: 1px solid transparent;
    transition: border-color .12s ease, background .12s ease;
}
.note:hover { border-color: var(--border); background: var(--surface); }
.note.mine { background: rgba(16, 185, 129, .06); border-color: rgba(16, 185, 129, .2); }
.note.mine:hover { background: rgba(16, 185, 129, .08); }
.note-avatar {
    /* LAYOUT + size only. Fill / hue / weight / color come from
       the canonical `.bf-avatar-letter` rule. */
    width: 30px; height: 30px; border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 11px; flex: 0 0 auto; text-transform: uppercase;
}
.note-body { flex: 1 1 auto; min-width: 0; }
.note-meta { display: flex; align-items: center; gap: .5rem; margin-bottom: .2rem; }
.note-author { font-size: 12px; color: var(--text); font-weight: 600; }
.note-date { color: var(--text-muted); font-size: 10px; font-weight: normal; }
.note-actions { margin-left: auto; display: inline-flex; align-items: center; gap: .15rem; }
.note-actions form { margin: 0; }
.note-del-form { display: inline-flex; }
.note-del,
.note-resolve,
.note-react-btn {
    background: transparent; border: 0;
    color: var(--text-subtle); cursor: pointer;
    padding: 3px 5px; border-radius: 4px;
    display: inline-flex; align-items: center;
    opacity: 0; transition: opacity .12s ease, color .12s ease, background .12s ease;
}
.note:hover .note-del,
.note:hover .note-resolve,
.note:hover .note-react-btn { opacity: 1; }
.note-del:hover { color: var(--danger); background: var(--danger-bg); }
.note-resolve:hover { color: var(--success); background: var(--success-bg); }
.note-react-btn:hover { color: var(--text); background: color-mix(in srgb, var(--text) 5%, transparent); }
/* When the note is already resolved, the resolve button stays visible
   so people remember they can re-open it. Same color treatment as the
   resolved-tag for consistency. */
.note.is-resolved .note-resolve { opacity: 1; color: var(--success); }

/* Resolved-thread visual differentiation.
   - Active threads use the default surface (no extra tint), so they
     read as the working space.
   - Resolved threads pick up a clearly-green tinted background +
     border so they look "wrapped up" at a glance. NO strikethrough
     on the body - the user found it harsh and hard to read; the
     green chrome + the explicit "Resolved" tag carries the meaning.
   - Replies in a resolved thread lose hover-revealed action chrome. */
.note.is-thread-resolved {
    background: color-mix(in srgb, var(--success, #10b981) 14%, var(--surface));
    border-color: color-mix(in srgb, var(--success, #10b981) 32%, var(--border));
}
.note.is-thread-resolved .note-author { color: var(--text-muted); }
.note.is-thread-resolved .note-text { color: var(--text); }
html[data-theme="dark"] .note.is-thread-resolved {
    background: color-mix(in srgb, var(--success, #10b981) 22%, var(--surface));
}
.note.is-thread-resolved .note-reaction-add { display: none; }

/* Collapsed resolved thread: replies hidden, toggle button reveals
   them. The toggle sits between the root note and the (hidden)
   replies; default state for newly-resolved threads with replies
   is collapsed so the panel stays scannable. */
.note-thread.is-collapsed .note.is-reply { display: none; }
.note-thread-toggle {
    display: inline-flex; align-items: center; gap: .35rem;
    margin: .15rem 0 .25rem 2.5rem;
    padding: .15rem .55rem;
    background: transparent;
    border: 0;
    border-radius: 999px;
    font-size: 11px; font-weight: 600;
    color: var(--success, #10b981);
    cursor: pointer;
    transition: background .12s ease, color .12s ease;
}
.note-thread-toggle:hover {
    background: color-mix(in srgb, var(--success, #10b981) 14%, transparent);
}
.note-thread-toggle .chev {
    transition: transform .15s ease;
    width: 12px; height: 12px;
}
.note-thread:not(.is-collapsed) .note-thread-toggle .chev {
    transform: rotate(180deg);
}
/* Toggle label swap - one rendered at a time based on the
   wrapper's collapsed state. Saves a JS textContent flip. */
.note-thread.is-collapsed .note-thread-toggle-hide,
.note-thread:not(.is-collapsed) .note-thread-toggle-show {
    display: none;
}
.note-resolved-tag {
    display: inline-flex; align-items: center; gap: .25rem;
    padding: 1px 7px; border-radius: 999px;
    background: var(--success-bg); color: var(--success);
    font-size: 10px; font-weight: 600; letter-spacing: .02em;
    text-transform: uppercase;
}

/* How a team note was read (#619): a REQUEST hands the thread's turn to the
   person it names ("Waiting on Loïc"), INFORMATION leaves it where it is. A
   designed mark - glyph + words on a transparent ground, never a filled pill; the
   author (or the person asked) flips it with one click, so it wears a hairline. */
.note-turn-form { margin: 0; display: inline-flex; }
.note-turn {
    display: inline-flex; align-items: center; gap: .3rem;
    min-height: 22px; padding: 0 .45rem;
    border: 1px solid transparent; border-radius: 6px;
    background: transparent; color: var(--text-muted);
    font: inherit; font-size: 11px; font-weight: 600; line-height: 1;
    white-space: nowrap; max-width: 100%;
}
.note-turn > span { overflow: hidden; text-overflow: ellipsis; }
.note-turn svg { flex: 0 0 auto; }
.note-turn.is-request { color: var(--primary); }
button.note-turn { cursor: pointer; border-color: var(--border); transition: background .12s ease, border-color .12s ease; }
button.note-turn:hover { background: var(--surface-2); border-color: var(--border-strong); }
button.note-turn:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
button.note-turn[disabled] { opacity: .6; cursor: progress; }

/* Reaction chips: little Notion-style emoji + count pills. Mine-toggled
   gets a primary-tinted ring so the user knows their click registered. */
.note-reactions {
    display: flex; flex-wrap: wrap; align-items: center; gap: .25rem;
    margin-top: .35rem;
}
.note-reaction-form { margin: 0; }
.note-reaction-chip {
    display: inline-flex; align-items: center; gap: .3rem;
    padding: 2px 8px; border-radius: 999px;
    background: var(--surface-2); border: 1px solid transparent;
    cursor: pointer;
    font-size: 11px; line-height: 1.3;
    transition: background .12s ease, border-color .12s ease, transform .08s ease;
}
.note-reaction-chip:hover { background: var(--surface); border-color: var(--border); }
.note-reaction-chip:active { transform: scale(.96); }
.note-reaction-chip.is-mine {
    background: var(--primary-bg); border-color: var(--primary);
    color: var(--primary);
}
.note-reaction-emoji { font-size: 13px; line-height: 1; }
.note-reaction-count { font-variant-numeric: tabular-nums; font-weight: 600; }
/* "Add another reaction" chip. Sits next to the existing
   reaction chips so it has to read like another chip, not like
   a generic "+" button. Solid pill (no dashed border), a
   smiley-face glyph + a small superscript "+" badge. Matches
   the chip footprint and tone, scales subtly on hover. */
.note-reaction-add {
    position: relative;
    display: inline-flex; align-items: center; justify-content: center;
    width: 28px; height: 24px;
    padding: 0;
    border-radius: 999px;
    background: var(--surface-2);
    border: 1px solid var(--border);
    color: var(--text-subtle);
    cursor: pointer !important;
    user-select: none;
    -webkit-user-select: none;
    opacity: 0;
    transition: opacity .12s ease, color .12s ease, border-color .12s ease,
                background .12s ease, transform .08s ease;
}
/* Force pointer cursor + non-select on every descendant. The
   "+" badge is a text node and the smiley SVG is its own
   element - both inherited the I-beam from somewhere up the
   tree and overrode the button's pointer. Setting them
   explicitly here makes the whole chip behave as one
   clickable unit. */
.note-reaction-add * {
    cursor: pointer !important;
    user-select: none;
    -webkit-user-select: none;
}
.note:hover .note-reaction-add { opacity: 1; }
.note-reaction-add:hover {
    color: var(--text);
    background: color-mix(in srgb, var(--text) 5%, transparent);
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
    transform: scale(1.05);
}
.note-reaction-add-face { display: block; }
.note-reaction-add-plus {
    position: absolute;
    top: -3px; right: -3px;
    width: 12px; height: 12px;
    border-radius: 50%;
    background: var(--text);
    color: var(--surface, #fff);
    font-size: 9px;
    font-weight: 700;
    line-height: 12px;
    text-align: center;
    box-shadow: 0 0 0 1.5px var(--surface);
    pointer-events: none;
}

/* The picker is a single hidden element per panel; JS positions it
   underneath the note whose react-button was clicked. Absolute so it
   doesn't disturb layout when shown. */
.note-reaction-picker {
    position: absolute;
    z-index: 30;
    background: var(--surface); border: 1px solid var(--border);
    border-radius: 12px; box-shadow: 0 12px 32px rgba(15,23,42,.18);
    padding: 6px;
    display: grid;
    grid-template-columns: repeat(7, 32px);
    gap: 2px;
    max-width: 248px;
}
.note-reaction-picker[hidden] { display: none; }
.note-reaction-pick {
    width: 32px; height: 32px;
    background: transparent; border: 0; border-radius: 6px;
    cursor: pointer;
    font-size: 1.15rem; line-height: 1;
    transition: background .1s ease, transform .08s ease;
}
.note-reaction-pick:hover { background: color-mix(in srgb, var(--text) 5%, transparent); }
.note-reaction-pick:active { transform: scale(.92); }
.note-text { font-size: 12px; color: var(--text); white-space: pre-wrap; word-wrap: break-word; line-height: 1.55; }
/* Auto-linked URLs inside a note body (see linkifyUrls in
   Message.cshtml). Brand-primary + underline so a link reads as a link
   without an explicit cursor declaration here - an <a href> already
   gets the pointer cursor, and the authoritative cursor block at the
   top of this file (which intentionally targets `.note-text`, NOT
   `.note-text *`, so links stay clickable) must stay the single owner
   of cursor rules. break-word keeps a long deep-link from blowing out
   the panel width. */
.note-text .note-link {
    color: var(--primary);
    text-decoration: underline;
    text-underline-offset: 2px;
    word-break: break-word;
}
.note-text .note-link:hover { text-decoration: none; }
/* Inline image + file uploads on a note (see renderAttachments in
   Message.cshtml). The image is wrapped in an <a> so it opens full-size
   in a new tab; capped height keeps a big screenshot from dominating
   the panel. Files render as a compact download chip. No cursor
   declarations here - the wrapping <a> already carries the pointer (see
   the authoritative cursor block at the top of this file, which targets
   `.note-text`, not its children). */
.note-image-link { display: inline-block; margin: .35rem 0; max-width: 100%; }
.note-image {
    display: block; max-width: 100%; max-height: 360px; height: auto;
    border-radius: 8px; border: 1px solid var(--border); background: var(--surface);
}
.note-text .note-file {
    display: inline-flex; align-items: center; gap: .35rem;
    margin: .25rem 0; padding: .25rem .6rem; max-width: 100%;
    border: 1px solid var(--border); border-radius: 8px;
    color: var(--primary); text-decoration: none; font-size: 12px; font-weight: 600;
}
.note-text .note-file:hover { background: color-mix(in srgb, var(--text) 5%, transparent); }
.note-file-icon { flex: 0 0 auto; }
.note-file-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Inline upload error under a note composer (too-large file, transport
   failure). Sits below the textarea inside .note-compose-input. */
.note-upload-error { color: var(--danger, #dc2626); font-size: 11px; margin-top: .4rem; }
/* Drag-a-file-over-the-textarea affordance, mirroring the focus accent. */
.note-compose textarea.note-drop-over {
    border-color: #10b981; box-shadow: 0 0 0 3px rgba(16,185,129,.18);
}
/* Cursor rules for .note / .note-text / .note-thread are owned by the
   authoritative block at the top of this file (rules 5/6/7). Do NOT
   add cursor declarations here - extend the top block instead. */
.note-text .mention, .mention {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 0 9px 0 2px;
    margin: 0 1px;
    /* Explicit height + line-height pin the chip to a stable geometry
       so internal flex centering produces an EVEN bubble-to-baseline
       offset regardless of the avatar's intrinsic size. Without this,
       the chip's height was content-driven (taller than its text)
       and the avatar/text combo read as misaligned. Same geometry
       as `.issues-mention` so user-mention chips look identical
       across notes and issue surfaces. */
    height: 1.4em;
    line-height: 1.4em;
    border-radius: 999px;
    background: var(--primary-bg); color: var(--primary); font-weight: 600;
    text-decoration: none;
    vertical-align: text-bottom;
    white-space: nowrap;
}
/* Bare-text mention (unresolved token, no avatar) - keep the original
   inline-block padding so a chip without an embedded avatar stays
   tight. The flex rule above is overridden via :not() for the
   no-avatar shape. */
.note-text .mention:not(.mention--clickable),
.mention:not(.mention--clickable) {
    padding: 0 6px;
    gap: 0;
}
/* Avatar inside the resolved-mention chip. Sized to the pill's inner
   height so it reads as part of the chip, matching the issue-mention
   shape pinned by IssueMentionCanonicalAvatarTests. The canonical
   paint (per-user gradient) lives on `.bf-avatar-letter` from the
   shared CSS rule; we ONLY own size + radius + alignment here. */
.note-text .mention .mention-avatar,
.mention .mention-avatar {
    width: 1.2em; height: 1.2em;
    border-radius: 50%;
    object-fit: cover;
    flex-shrink: 0;
    /* The bubble inherits the chip's font-size so it scales with the
       surrounding chrome - the previous 9px override produced a tiny
       ball next to a ~14px chip name. Inner letter is sized down via
       a sub-em so it stays legible without dominating the bubble. */
    font-size: .72em;
    font-weight: 600;
    line-height: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.note-text .mention .mention-name,
.mention .mention-name {
    font-size: inherit;
}
/* Resolved mentions are clickable - they open the profile-card modal
   via the global avatar-click handler. The hover state matches the
   workspace pip's hover (subtle lift + saturated bg) so the
   affordance reads consistently across surfaces. */
.mention--clickable {
    cursor: pointer;
    transition: background .12s ease, color .12s ease, transform .08s ease;
}
.mention--clickable:hover {
    background: color-mix(in srgb, var(--text) 5%, transparent);
    color: var(--text);
}
.mention--clickable:active { transform: translateY(1px); }
.mention--clickable:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}
/* Mention autocomplete popup under the note textarea */
.note-compose-input { position: relative; }
.mention-pop {
    position: absolute; top: 100%; left: 0; z-index: 40;
    margin-top: 6px; background: var(--surface); border: 1px solid var(--border);
    border-radius: 10px; box-shadow: 0 12px 32px rgba(15,23,42,.18);
    min-width: 200px; padding: 4px;
    display: flex; flex-direction: column;
}
/* Portaled variant: appended to <body> instead of inside the notes
   panel (whose overflow:hidden was clipping the popup). Coordinates
   are computed at open time, so the rule just bumps the z-index above
   any modal/sticky element and resets margin since "top" is set
   absolutely from JS. */
.mention-pop.is-portal {
    position: absolute;
    z-index: 1080;
    margin: 0;
    max-width: 360px;
}
.mention-pop[hidden] { display: none; }
.mention-pop button {
    display: flex; align-items: center; gap: 0.6rem;
    text-align: left; background: transparent; border: 0;
    padding: .4rem .55rem; border-radius: 6px; cursor: pointer;
    font-size: 12px; color: var(--text);
    min-width: 220px;
}
.mention-pop button.active, .mention-pop button:hover { background: color-mix(in srgb, var(--text) 5%, transparent); color: var(--text); }
.mention-pop-avatar {
    /* LAYOUT + size only. Fill / hue / weight / color come from
       the canonical `.bf-avatar-letter` rule. */
    flex: 0 0 auto;
    width: 26px; height: 26px; border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 10px; overflow: hidden;
}
img.mention-pop-avatar { object-fit: cover; }
.mention-pop-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
.mention-pop-label { font-weight: 600; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mention-pop-sub { color: var(--text-muted); font-size: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Team row in the mention popup. Same shape as a user row but the
   avatar slot is a team-icon glyph and a small "team" tag sits next
   to the name so the user knows picking expands to all members. */
.mention-pop button.is-team .mention-pop-team-icon {
    background: var(--primary-bg); color: var(--primary);
    display: inline-flex; align-items: center; justify-content: center;
}
.mention-pop-team-tag {
    margin-left: .4rem;
    font-size: 9px; font-weight: 600;
    color: var(--primary); background: var(--primary-bg);
    padding: 1px 5px; border-radius: 999px;
    letter-spacing: .04em; text-transform: uppercase;
    vertical-align: middle;
}

/* Compose: slim by default, expands on focus */
/* Collapsed compose trigger - a single-line affordance that takes the
   place of the full form when no one's writing. Keeps the Team notes
   section unobtrusive so message body stays visible without scrolling. */
.note-compose-trigger {
    display: flex; align-items: center; gap: .75rem;
    width: 100%; padding: .55rem 1.25rem;
    background: var(--surface);
    border: 0;
    border-top: 1px solid var(--border);
    color: var(--text-subtle);
    font: inherit; font-size: 12px;
    cursor: pointer;
    text-align: left;
    transition: background .1s ease, color .1s ease;
}
/* Empty-state trigger stands alone - no border to separate from a header
   that isn't there, slightly tighter vertical padding. */
.notes-panel.empty .note-compose-trigger {
    border-top: 0;
    padding: .7rem 1.25rem;
}
.note-compose-trigger:hover { background: var(--surface-2); color: var(--text); }
.note-compose-trigger:hover .note-compose-avatar { box-shadow: 0 0 0 2px var(--primary-bg); }
.note-compose-trigger .note-compose-avatar { width: 26px; height: 26px; font-size: 10px; }
.note-compose-cta { flex: 1 1 auto; }
.note-compose-kbd {
    flex: 0 0 auto;
    font-family: ui-monospace, monospace;
    font-size: 10px;
    padding: 1px 6px;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 4px;
    color: var(--text-muted);
    line-height: 1.2;
}

.note-compose { display: flex; gap: .75rem; padding: .9rem 1.25rem; border-top: 1px solid var(--border); background: var(--surface); align-items: flex-start; }
.notes-panel.empty .note-compose { border-top: 0; }
.note-compose-avatar {
    /* LAYOUT + size only. Fill / hue / weight / color come from
       the canonical `.bf-avatar-letter` rule. */
    width: 30px; height: 30px; border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 11px; flex: 0 0 auto; text-transform: uppercase;
}
.note-compose-input { flex: 1 1 auto; min-width: 0; }
.note-compose textarea {
    width: 100%; padding: .55rem .85rem;
    border: 1px solid var(--border-strong); border-radius: 10px;
    background: var(--surface); color: var(--text);
    font-size: 12px; font-family: inherit; line-height: 1.5;
    resize: none; overflow: hidden;
    transition: border-color .12s ease, box-shadow .12s ease;
}
.note-compose textarea::placeholder { color: var(--text-subtle); }
.note-compose textarea:focus {
    outline: none; border-color: #10b981;
    box-shadow: 0 0 0 3px rgba(16,185,129,.15);
}
.note-compose-actions { display: flex; justify-content: space-between; align-items: center; margin-top: .55rem; gap: .75rem; opacity: 0; max-height: 0; overflow: hidden; transition: opacity .15s ease, max-height .2s ease; }
.note-compose.focused .note-compose-actions, .note-compose textarea:not(:placeholder-shown) ~ .note-compose-actions { opacity: 1; max-height: 60px; }
.note-compose-hint { font-size: 10px; color: var(--text-subtle); }
/* WHO HEARS the note (issue #483): the conversation's faces, then one short muted line. */
.note-compose-audience { display: inline-flex; align-items: center; gap: 6px; min-width: 0; font-size: 11px; color: var(--text-muted); }
.note-compose-audience-pile { display: inline-flex; flex: 0 0 auto; }
.note-compose-audience-avatar {
    width: 18px; height: 18px; border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 9px; text-transform: uppercase;
    box-shadow: 0 0 0 2px var(--surface);
}
.note-compose-audience-avatar + .note-compose-audience-avatar { margin-left: -5px; }
.note-compose-audience-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.note-compose-audience.is-none { color: var(--text-subtle); }
.note-compose-audience.is-none svg { flex: 0 0 auto; }
/* Left cluster of the actions row: the Attach control, then the hint.
   Attach is a quiet hairline icon button - the row's loud element is the
   Post button, and a second emphatic control would compete with it. */
.note-compose-tools { display: inline-flex; align-items: center; gap: .5rem; min-width: 0; }
.note-attach-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 26px; height: 26px; flex: 0 0 auto; padding: 0;
    color: var(--text-subtle); background: none;
    border: 1px solid transparent; border-radius: 6px;
    cursor: pointer;
    transition: color .12s ease, border-color .12s ease;
}
.note-attach-btn:hover { color: var(--text); border-color: var(--border); }
.note-attach-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; color: var(--text); }

/* Topbar .eml action */
.msg-topbar .topbar-action { display: inline-flex; align-items: center; gap: .35rem; padding: .35rem .65rem; font-size: 11px; color: var(--text-muted); text-decoration: none; border-radius: 6px; border: 1px solid var(--border); }
.msg-topbar .topbar-action:hover { background: color-mix(in srgb, var(--text) 5%, transparent); color: var(--text); border-color: color-mix(in srgb, var(--text) 25%, transparent); }

/* Saved searches */
/* Views (saved searches). Container = flex row with a single <a> link for
   navigation + a sibling actions block (real <form>s, outside the anchor so
   the HTML is valid). Count and actions share the trailing slot: count shows
   at rest, actions fade in on hover. Keeps clicks on action buttons cleanly
   scoped to their own forms - no ghost links, no invalid nesting. */
.view-item {
    position: relative;          /* anchor for the absolute .view-actions */
    display: flex;
    align-items: stretch;
    gap: 0;
    padding: 0;
    overflow: hidden;
    border-radius: 8px;
    transition: background .12s ease, color .12s ease, box-shadow .12s ease;
}
.view-item + .view-item { margin-top: .1rem; }

/* View-link owns the full row width so its count pill aligns flush to the
   right edge - the same visual column as system-folder and label rows.
   Padding matches .list-group-item exactly (.5rem .8rem) so all three
   sidebar row types share one measurement and the count pills line up. */
.view-item .view-link {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: .55rem;
    padding: .5rem .8rem;
    color: inherit;
    text-decoration: none;
}
.view-item .view-icon {
    flex: 0 0 auto;
    width: 16px; height: 16px;
    display: inline-flex; align-items: center; justify-content: center;
    color: var(--text-subtle);
    transition: color .12s ease;
}
.view-item.is-shared .view-icon { color: var(--primary); }
.view-item.active .view-icon,
.view-item:hover .view-icon { color: var(--text); }
.view-item .view-name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* Sidebar count placeholders shown until the lazy-load AJAX
   (OnGetSidebarCountsAsync) lands. data-loading="1" is set on
   server-render and removed by JS once the real number arrives. */
.sb-count[data-loading]:empty::before,
.view-count[data-loading]:empty::before {
    content: "";
    display: inline-block;
    width: 18px;
    height: 9px;
    border-radius: 4px;
    background: linear-gradient(90deg,
        var(--surface-2) 0%,
        color-mix(in srgb, var(--primary) 12%, var(--surface-2)) 50%,
        var(--surface-2) 100%);
    background-size: 200% 100%;
    animation: sbCountShimmer 1.1s ease-in-out infinite;
    vertical-align: middle;
    opacity: .6;
}
@keyframes sbCountShimmer {
    0% { background-position: 0 0; }
    100% { background-position: -200% 0; }
}

.view-item .view-count {
    flex: 0 0 auto;
    transition: opacity .12s ease;
}

/* Actions sit outside the link - clicks here never navigate. Positioned
   ABSOLUTE so they don't steal flex space from the link. This keeps the
   count pill aligned with the right edge, matching system-folder rows. On
   hover, the buttons overlay the count (which fades) so both can occupy
   the same visual slot without fighting for space. */
.view-item .view-actions {
    position: absolute;
    top: 50%;
    right: .4rem;
    transform: translateY(-50%);
    display: inline-flex;
    align-items: center;
    gap: .15rem;
    background: var(--surface);
    border-radius: 6px;
    padding: .1rem;
    opacity: 0;
    pointer-events: none;
    transition: opacity .12s ease;
    z-index: 1;
}
/* Action overlay visibility: shown only when the user hovers the
   count of the SELECTED view (.active). Mirrors the user-label
   rule - hovering an unselected view never reveals the actions,
   and an active view stays clean until the cursor lands on the
   count. Keyboard focus on a nested action keeps the affordance
   accessible. */
.view-item.active:has(.view-count:hover, .view-actions:hover) .view-actions,
.view-item .view-actions:focus-within { opacity: 1; pointer-events: auto; }
/* The action overlay takes over the right-hand slot during the
   count-hover so fade the count underneath. Gated on .is-mutable
   AND on the row HAVING a .view-actions child: read-only views
   skip the actions entirely, and fading their count would leave
   an empty slot with nothing to swap in. The :has() check makes
   the rule self-cancel on rows that emit no actions. */
.view-item.is-mutable.active:has(.view-actions):has(.view-count:hover, .view-actions:hover) .view-count { opacity: 0; }
/* When actions appear on an active view, give them the active-state bg so
   they read as part of the same row rather than floating on white. */
.view-item.active .view-actions { background: color-mix(in srgb, var(--text) 7%, transparent); }
html[data-theme="dark"] .view-item.active .view-actions { background: transparent; }

.view-item .view-action-form { margin: 0; padding: 0; display: inline-flex; }
.view-item .view-action {
    width: 26px; height: 26px;
    padding: 0;
    display: inline-flex; align-items: center; justify-content: center;
    background: transparent;
    border: 1px solid transparent;
    border-radius: 6px;
    color: var(--text-muted);
    cursor: pointer;
    transition: background .12s ease, color .12s ease, border-color .12s ease;
}
.view-item .view-action:hover {
    background: var(--surface);
    color: var(--text);
    border-color: var(--border);
}
.view-item .view-action-danger:hover {
    color: var(--danger);
    border-color: #fecaca;
    background: var(--surface);
}

/* Rest / hover / active - active is distinctly primary-tinted with a left
   accent bar; hover is an ink wash (the sidebar IS the second surface, so
   its own colour would show nothing) - the two states never look alike. */
.view-item:hover { background: color-mix(in srgb, var(--text) 7%, transparent); }
.view-item.active {
    background: transparent;
    color: var(--text);
    font-weight: 600;
    box-shadow: inset 3px 0 0 var(--primary);
}
.view-item.active .view-count { background: transparent; color: var(--primary); font-weight: 600; }

html[data-theme="dark"] .view-item:hover { background: rgba(148, 163, 184, .08); }
html[data-theme="dark"] .view-item.active { background: transparent; }
html[data-theme="dark"] .view-item.is-shared .view-icon { color: var(--primary); }
html[data-theme="dark"] .view-item .view-action:hover { background: #1e293b; }
html[data-theme="dark"] .view-item .view-action-danger:hover { border-color: rgba(248, 113, 113, .3); }
/* Save-as-view modal - polished dialog matching the Preferences aesthetic. */
.view-dialog-backdrop {
    position: fixed; inset: 0; z-index: 9999;
    background: rgba(15, 23, 42, .58);
    backdrop-filter: blur(8px) saturate(1.1);
    -webkit-backdrop-filter: blur(8px) saturate(1.1);
    display: flex; align-items: center; justify-content: center;
    padding: 1rem;
    animation: fade-in .16s ease;
}
.view-dialog-backdrop[hidden] { display: none; }
.view-dialog {
    width: min(460px, 100%);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 16px;
    padding: 1.6rem 1.75rem 1.4rem;
    box-shadow: 0 30px 80px -20px rgba(15, 23, 42, .4),
                0 10px 30px -10px rgba(15, 23, 42, .25);
    animation: prefs-rise .22s cubic-bezier(.2, .9, .3, 1);
}
html[data-theme="dark"] .view-dialog {
    box-shadow: 0 30px 80px -20px rgba(0, 0, 0, .6),
                0 10px 30px -10px rgba(0, 0, 0, .45);
}
.view-dialog h4 {
    margin: 0 0 .3rem;
    font-size: 15px;
    font-weight: 700;
    letter-spacing: -.01em;
    color: var(--text);
}
.view-dialog-intro {
    margin: 0 0 1.1rem;
    font-size: 12px;
    color: var(--text-muted);
    line-height: 1.5;
}
.view-dialog label {
    display: block;
    font-size: 11px;
    font-weight: 600;
    color: var(--text);
    margin-bottom: 1rem;
    letter-spacing: .01em;
}
.view-dialog label input[type="text"] {
    display: block;
    width: 100%;
    padding: .65rem .85rem;
    border: 1px solid var(--border-strong);
    border-radius: 10px;
    font-size: 13px;
    font-weight: 500;
    background: var(--surface);
    color: var(--text);
    margin-top: .4rem;
    transition: border-color .12s ease, box-shadow .12s ease;
}
.view-dialog label input[type="text"]:hover { border-color: var(--text-muted); }
.view-dialog label input[type="text"]:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 3px var(--primary-bg);
}
.view-dialog label.chk {
    display: flex;
    align-items: flex-start;
    gap: .75rem;
    font-weight: 500;
    font-size: 12px;
    padding: .75rem .85rem;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 10px;
    cursor: pointer;
    transition: border-color .12s ease, background .12s ease;
}
.view-dialog label.chk:hover { border-color: var(--border-strong); }
.view-dialog label.chk:has(input:checked) {
    background: var(--primary-bg);
    border-color: var(--primary);
}
.view-dialog label.chk > span { display: block; flex: 1 1 auto; line-height: 1.4; }
.view-dialog label.chk strong { display: block; color: var(--text); font-weight: 600; font-size: 12px; }
.view-dialog label.chk em { display: block; font-style: normal; font-weight: 400; font-size: 11px; color: var(--text-muted); margin-top: .15rem; }

/* Toggle switch to match Preferences. Replaces the chunky purple checkbox. */
.view-dialog label.chk input[type="checkbox"] {
    appearance: none;
    -webkit-appearance: none;
    position: relative;
    width: 36px; height: 20px;
    flex: 0 0 auto;
    margin: 0;
    margin-top: 2px;
    border: 0;
    border-radius: 999px;
    background: var(--border-strong);
    cursor: pointer;
    transition: background .18s ease;
}
.view-dialog label.chk input[type="checkbox"]::after {
    content: '';
    position: absolute;
    top: 2px; left: 2px;
    width: 16px; height: 16px;
    background: #fff;
    border-radius: 50%;
    box-shadow: 0 1px 2px rgba(15, 23, 42, .15), 0 1px 3px rgba(15, 23, 42, .2);
    transition: transform .18s cubic-bezier(.3, 1.2, .5, 1);
}
.view-dialog label.chk input[type="checkbox"]:checked { background: var(--primary); }
.view-dialog label.chk input[type="checkbox"]:checked::after { transform: translateX(16px); }
html[data-theme="dark"] .view-dialog label.chk input[type="checkbox"] { background: #475569; }
html[data-theme="dark"] .view-dialog label.chk input[type="checkbox"]::after { background: #f1f5f9; }
html[data-theme="dark"] .view-dialog label.chk input[type="checkbox"]:checked { background: var(--primary); }

.view-dialog-actions {
    display: flex;
    justify-content: flex-end;
    gap: .5rem;
    margin-top: 1.2rem;
}
.view-dialog-actions .btn { font-weight: 600; }

/* Mobile responsive */
@media (max-width: 820px) {
    .archive-toolbar { grid-template-columns: 1fr; gap: .5rem; padding: .65rem .85rem; }
    .archive-search { grid-column: 1; }
    .toolbar-actions { grid-column: 1; flex-wrap: wrap; }
    /* On narrow screens the sidebar becomes a slide-in drawer instead of
       stacking on top of the list, which would otherwise eat half the viewport. */
    .archive-body { flex-direction: column; position: relative; }
    .archive-sidebar {
        position: fixed; top: 0; left: 0; bottom: 0;
        width: 280px; max-width: 85vw;
        margin: 0; border-radius: 0;
        background: var(--surface);
        border-right: 1px solid var(--border);
        box-shadow: none; /* the shadow falls only while the drawer is out, never on the page's edge */
        z-index: 2500;
        transform: translateX(-100%);
        transition: transform .2s ease, box-shadow .2s ease;
        overflow-y: auto;
    }
    .archive-sidebar.is-open { transform: translateX(0); box-shadow: 8px 0 24px rgba(15,23,42,.18); }
    .archive-sidebar-backdrop {
        display: block;
        position: fixed; inset: 0; background: rgba(15,23,42,.35);
        z-index: 2499; opacity: 0; pointer-events: none;
        transition: opacity .2s ease;
    }
    .archive-sidebar-backdrop.is-open { opacity: 1; pointer-events: auto; }
    /* Narrow-screen tweak: tighten the left cluster, shrink typography a
       notch. The row is already two-line; on mobile it stays two-line,
       just more compact. */
    .mail-row { grid-template-columns: 30px 18px 24px minmax(0, 1fr); gap: 0; padding: .55rem .5rem .55rem .65rem; }
    .read-btn { height: 20px; }
    .mail-row > .mail-content { padding-left: .5rem; padding-right: 0; cursor: pointer; }
    .mail-from { font-size: 12px; }
    .mail-body { font-size: 11px; gap: .55rem; }
    .mail-date { font-size: 10px; }
    .msg-topbar { padding: .5rem .85rem; flex-wrap: wrap; }
    .msg-container { padding: 1rem .85rem 2rem; }
    .msg-subject { font-size: 1.2rem; }
    .msg-body { padding: 1rem 1.1rem; }
    .msg-notes { padding: 1rem; margin-top: 1rem; }
    .preview-panel.open { width: 100% !important; position: fixed; inset: 0; z-index: 500; }
    .preview-handle { display: none; }
    .pagination-bar { flex-direction: column; gap: .5rem; padding: .5rem .85rem; }
    .pagination-range, .pagination-jump { order: 2; }
    .pagination-controls { order: 1; flex-wrap: wrap; justify-content: center; }
    .add-grid { grid-template-columns: 1fr !important; }
    .app-nav .user-email { display: none; }
    .app-nav .nav-tools { padding-left: .35rem; margin-left: .35rem; }
    .adv-panel .adv-row-2 { grid-template-columns: 1fr; }
    .prefs-panel { width: 94vw; max-height: 90vh; }
}

/* A phone's row has no room for a label: no label (owner, 2026-09-29: "if we
   don't have enough space, don't display label") - never a name-less dot; the
   conversation shows its labels when opened. .mail-row outranks the base
   .mail-labels rule further down. */
@media (max-width: 640px) {
    .mail-row .mail-labels { display: none; }
}

/* New-messages banner */
.new-msg-banner {
    position: fixed; top: 70px; left: 50%; transform: translateX(-50%);
    background: var(--text); color: var(--surface, #fff);
    padding: .45rem .5rem .45rem 1.1rem; border-radius: 999px;
    box-shadow: 0 6px 20px rgba(15,23,42,.28);
    font-size: 12px; font-weight: 500;
    z-index: 1000; animation: slide-down .25s ease;
    display: inline-flex; align-items: center; gap: .35rem;
}
.new-msg-banner .nmb-link { color: #fff; text-decoration: none; padding: .05rem .35rem; border-radius: 999px; }
.new-msg-banner .nmb-link:hover { background: rgba(255,255,255,.15); }
.new-msg-banner .nmb-close {
    display: inline-flex; align-items: center; justify-content: center;
    width: 22px; height: 22px; border-radius: 50%;
    background: rgba(255,255,255,.18); border: 0; color: #fff;
    font-size: 14px; line-height: 1; cursor: pointer;
    margin-left: .2rem; transition: background .12s ease;
}
.new-msg-banner .nmb-close:hover { background: rgba(255,255,255,.3); }
@keyframes slide-down { from { transform: translate(-50%, -20px); opacity: 0; } to { transform: translate(-50%, 0); opacity: 1; } }
.adv-panel label { display: flex; flex-direction: column; gap: .25rem; margin: 0; }
.adv-panel label span { font-size: 11px; color: var(--text-muted); font-weight: 500; }
.adv-panel input[type="text"], .adv-panel input[type="date"], .adv-panel label input:not([type="checkbox"]) {
    border: 1px solid var(--border-strong); border-radius: 8px;
    padding: .45rem .65rem; font-size: 12px;
    background: var(--surface); color: var(--text);
}
.adv-panel input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(79,70,229,.15); }
.adv-panel .adv-actions { display: flex; justify-content: flex-end; gap: .5rem; margin-top: .5rem; }

/* Filter pills */
.filter-pills {
    display: flex; flex-wrap: wrap; gap: .45rem; align-items: center;
    /* Inline segment of .mail-filter-row now - the row owns the
       background and border; a second tinted stripe was chrome. */
    padding: .4rem 0;
    /* Locked total height so the segment reads at the same vertical
       footprint whether it shows chips, the Save-as-view +
       Clear-all actions, or the "No filters applied" placeholder.
       Without this, a chip's padding + border makes the bar grow
       ~5px when filters get added, the message list jumps, and
       the user perceives CLS. */
    height: 40px;
    box-sizing: border-box;
    font-size: 12px;
    min-width: 0;
}
/* Convert-email-to-issue modal. Standalone (not Bootstrap-modal-
   based) because we need a tight Razor partial that lives in
   _Layout-rendered Mail pages and a small JS surface that doesn't
   pull in jQuery. The visual contract mirrors the Issues/New page
   so the user sees ONE editor everywhere. Sized to feel intimate
   on desktop, full-width on mobile. */
.bf-modal { position: fixed; inset: 0; z-index: 2050; }
.bf-modal[hidden] { display: none; }
/* A window opened FROM another stands stacked over it (#547, rule 2: a window never closes its
   caller): one step above, its veil lighter, so the caller stays visible underneath. */
.bf-modal.is-stacked { z-index: 2051; }
.bf-modal.is-stacked > .bf-modal-backdrop { background: rgba(15, 23, 42, .28); backdrop-filter: none; }
/* ONE WINDOW AT A TIME (bf-modal-stack.js, owner 2026-10-19): a window under the top one keeps its backdrop, its panel is not seen until the top closes. */
.bf-modal[data-bf-under] > :not(.bf-modal-backdrop) { visibility: hidden; }
.bf-modal[data-bf-under] .bf-modal:not([data-bf-under]) { visibility: visible; }   /* a window nested in its caller stays seen */
.bf-modal-backdrop {
    position: absolute; inset: 0;
    background: rgba(15, 23, 42, .55);
    backdrop-filter: blur(2px);
    animation: bf-modal-fade-in .15s ease-out;
}
.bf-modal-card {
    position: absolute; left: 50%; top: 50%;
    transform: translate(-50%, -50%);
    width: min(640px, calc(100vw - 32px));
    max-height: calc(100vh - 32px);
    overflow: auto;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    box-shadow: 0 20px 50px -12px rgba(0,0,0,.35), 0 8px 18px -4px rgba(0,0,0,.18);
    display: flex; flex-direction: column;
    animation: bf-modal-pop .18s cubic-bezier(.2,.7,.2,1.05);
}
@keyframes bf-modal-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes bf-modal-pop {
    from { opacity: 0; transform: translate(-50%, -48%) scale(.97); }
    to   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
/* ONE close mark for every dialog head (owner, 2026-08-19: the text ×
   rendered tiny and sat differently per dialog). A fixed 30px box - the
   size never depends on the glyph - holding the 16px SVG cross, pushed to
   the head's right edge. */
.bf-modal-close {
    appearance: none; background: transparent; border: 0;
    color: var(--text-muted); cursor: pointer;
    flex: 0 0 auto;
    width: 30px; height: 30px; padding: 0;
    margin-left: auto;
    border-radius: 6px;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 18px; line-height: 1;
}
.bf-modal-close svg { width: 16px; height: 16px; }
.bf-modal-close:hover { background: var(--surface-2); color: var(--text); }
/* On a finger the glyph stays 16px and the BOX becomes reachable: closing a
   window is the one verb every window owes its reader (owner, 2026-08-25: every
   surface must be perfectly usable on tablet and on phone). */
@media (pointer: coarse) {
    .bf-modal-close { width: 44px; height: 44px; }
}
/* Scroll lock: the ONE canonical page-scroll lock, owned by scroll-lock.js.
   A lock is a lease on its overlay element and dies with it - no leaked
   body class can freeze the app again (PageScrollInvariantTests). */
body.bf-scroll-locked { overflow: hidden; }
body.bf-scroll-locked .app-page { overflow: hidden; }

/* ── The viewer STAGE token family (2026-08-23 redesign) ─────────────
   ONE room for every full-screen viewer surface (the Documents lightbox,
   the attachment lightbox): a theme-invariant deep-graphite stage in the
   runtime's own forced-dark family (#1c1c1e reading surface, #2c2c2e
   toolbar solid, inverted-ink emphasis). Values are defined HERE only -
   surfaces carry the class and read the vars; no dark-theme twin ever
   re-themes them (a stage is a stage, in both app themes). Pinned by
   AttachmentViewerStageTests + DocumentsLightboxStageTests. */
.bf-stage {
    --stage-bg: #1c1c1e;
    --stage-bar: #2c2c2e;
    --stage-text: #f5f5f7;
    --stage-text-dim: rgba(235, 235, 245, 0.62);
    --stage-text-faint: rgba(235, 235, 245, 0.42);
    --stage-text-off: rgba(235, 235, 245, 0.28);
    --stage-stroke: rgba(255, 255, 255, 0.12);
    --stage-hover: rgba(255, 255, 255, 0.1);
    --stage-active: rgba(255, 255, 255, 0.16);
    --stage-ink: #f5f5f7;
    --stage-ink-fg: #1d1d1f;
    --stage-danger: #ff453a;
}

/* The card never scrolls itself - the header stays put, the form body is
   the SINGLE scroll region, and the action row is pinned at its bottom so
   Cancel / Create are always fully visible regardless of how long the email
   body is. (Removing the editor's own max-height below means there is never
   a second, inner scrollbar.) */
/* Wider than the default modal: the description carries quoted email
   bodies, so the extra width keeps long lines + the source-picker rows
   readable. Still viewport-capped for small screens. */
.create-issue-from-email-modal .bf-modal-card { width: min(820px, calc(100vw - 32px)); }
.cife-card { padding: 0; overflow: hidden; }
.cife-header {
    flex: 0 0 auto;
    display: flex; align-items: center; justify-content: space-between;
    padding: 1rem 1.1rem; border-bottom: 1px solid var(--border);
}
.cife-title { margin: 0; font-size: 15px; font-weight: 600; }

/* The form fills the card and lays out its scrolling body + a fixed footer.
   It does NOT scroll itself - only .cife-form-body does - so the scrollbar
   spans just the fields, never the always-visible action row. */
.cife-form {
    flex: 1 1 auto; min-height: 0;
    display: flex; flex-direction: column;
    overflow: hidden;
}
.cife-form-body {
    flex: 1 1 auto; min-height: 0; overflow-y: auto;
    display: flex; flex-direction: column; gap: 1rem;
    padding: 1rem 1.1rem;
}
/* In the modal the editor grows with its content inside the body's scroll
   instead of capping + scrolling on its own, so the dialog has exactly one
   scrollbar (the body's). Zero-CLS: the email-body draft lands via an async
   fetch after the modal is visible - reserve a typical draft height so the
   card paints at roughly its final size instead of expanding when the
   draft arrives. */
.create-issue-from-email-modal .bf-md-editor-textarea { max-height: none; min-height: clamp(300px, 50vh, 660px); }
.cife-field { display: flex; flex-direction: column; gap: .35rem; }
.cife-label {
    font-size: 11px; font-weight: 600; color: var(--text-muted);
    text-transform: uppercase; letter-spacing: .04em;
}
.cife-input, .cife-select {
    appearance: none;
    background: var(--surface); color: var(--text);
    border: 1px solid var(--border); border-radius: 8px;
    padding: .55rem .7rem;
    font-size: 13px; font-family: inherit;
    transition: border-color .12s, box-shadow .12s;
}
.cife-input:focus, .cife-select:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 22%, transparent);
}

/* Source-message picker. A disclosure (<details>) whose summary shows
   the currently-chosen message and whose panel lists every message in
   the conversation so the user can base the issue on a specific reply.
   Rows follow the .coworker-row convention - full-width, borders BETWEEN
   rows, no pills; the selected row signals via border + weight only
   (no fill), per the app-wide selected-control rule. */
.cife-source { position: relative; }
.cife-source-summary {
    list-style: none;
    display: flex; align-items: center; gap: .5rem;
    padding: .55rem .7rem;
    background: var(--surface); color: var(--text);
    border: 1px solid var(--border); border-radius: 8px;
    font-size: 13px; cursor: pointer;
    transition: border-color .12s;
}
.cife-source-summary::-webkit-details-marker { display: none; }
.cife-source[open] > .cife-source-summary,
.cife-source-summary:hover { border-color: color-mix(in srgb, var(--text) 25%, transparent); }
.cife-source-current { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cife-source-caret { flex: 0 0 auto; color: var(--text-muted); transition: transform .15s; }
.cife-source[open] .cife-source-caret { transform: rotate(180deg); }
.cife-source-list {
    /* Floats OVER the fields below instead of pushing them down: absolute so
       opening the picker never changes the dialog's height. Capped to a
       viewport-relative height (scrolls internally past that) so it always
       fits within the modal on any screen. */
    position: absolute; top: calc(100% + 5px); left: 0; right: 0; z-index: 40;
    list-style: none; margin: 0; padding: 0;
    border: 1px solid var(--border); border-radius: 10px;
    max-height: min(46vh, 360px); overflow-y: auto;
    background: var(--surface);
    box-shadow: 0 14px 34px -10px rgba(15, 23, 42, .32), 0 4px 12px -4px rgba(15, 23, 42, .18);
}
.cife-source-option {
    display: flex; flex-direction: column; gap: .15rem;
    width: 100%; text-align: left;
    padding: .5rem .7rem;
    background: transparent; border: none;
    border-bottom: 1px solid var(--border);
    cursor: pointer; font: inherit; color: var(--text);
}
.cife-source-list > li:last-child .cife-source-option { border-bottom: none; }
.cife-source-option:hover { background: var(--surface-2); }
.cife-source-option.is-selected {
    /* Selected signals via a left accent border + bolder sender, never a
       fill (app-wide rule: no filled background on active controls). */
    box-shadow: inset 3px 0 0 0 var(--primary);
    background: transparent;
}
.cife-source-option.is-selected .cife-source-option-from { font-weight: 600; }
.cife-source-option-head { display: flex; align-items: baseline; gap: .5rem; }
.cife-source-option-from { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; }
.cife-source-option-date { flex: 0 0 auto; font-size: 11px; color: var(--text-muted); }
.cife-source-option-snippet { font-size: 12px; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Workspace picker (custom listbox combobox). Native <select> can't
   render per-option icons across browsers, so we build a tight
   listbox and paint each row with the canonical .bf-icon bubble at
   the left. The user recognises their target workspace by its
   accent + initial / icon, not by reading the name alone.
   Anchored to the button + floats below to avoid pushing form
   fields around when it opens.
   Keyboard accessibility: ArrowUp/Down navigates; Enter picks;
   Escape closes - wired in create-issue-from-email.js. */
.cife-ws-picker { position: relative; }
.cife-ws-button {
    display: flex; width: 100%; align-items: center; gap: .55rem;
    background: var(--surface); color: var(--text);
    border: 1px solid var(--border); border-radius: 8px;
    padding: .45rem .7rem;
    font-size: 13px; font-family: inherit; text-align: left;
    cursor: pointer;
    transition: border-color .12s, box-shadow .12s;
}
.cife-ws-button:hover { border-color: color-mix(in srgb, var(--text) 25%, transparent)); }
.cife-ws-button:focus-visible,
.cife-ws-picker.is-open > .cife-ws-button {
    outline: none;
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 22%, transparent);
}
.cife-ws-button-glyph {
    /* Reuses the canonical .bf-icon paint so the button bubble
       looks identical to the workspace icons everywhere else.
       Size + radius are picker-specific. */
    --bf-icon-size: 26px;
    --bf-icon-radius: 6px;
    flex: 0 0 auto;
}
.cife-ws-button-label {
    flex: 1 1 auto;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cife-ws-button-caret {
    flex: 0 0 auto;
    color: var(--text-muted);
    transition: transform .15s ease;
}
.cife-ws-picker.is-open .cife-ws-button-caret { transform: rotate(180deg); }

.cife-ws-list {
    position: absolute; left: 0; right: 0; top: calc(100% + 4px);
    z-index: 10;
    margin: 0; padding: 4px;
    list-style: none;
    background: var(--surface);
    border: 1px solid var(--border); border-radius: 8px;
    box-shadow: 0 12px 28px -8px rgba(0,0,0,.18);
    max-height: 280px; overflow-y: auto;
}
.cife-ws-list[hidden] { display: none; }
.cife-ws-option {
    display: flex; align-items: center; gap: .55rem;
    padding: .4rem .55rem;
    border-radius: 6px;
    cursor: pointer;
    color: var(--text);
}
.cife-ws-option-glyph {
    --bf-icon-size: 26px;
    --bf-icon-radius: 6px;
    flex: 0 0 auto;
}
.cife-ws-option-label {
    flex: 1 1 auto; font-size: 13px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cife-ws-option-check {
    flex: 0 0 auto; color: var(--primary); opacity: 0;
}
.cife-ws-option[aria-selected="true"] .cife-ws-option-check { opacity: 1; }
.cife-ws-option:hover,
.cife-ws-option[data-active="true"] {
    background: color-mix(in srgb, var(--text) 5%, var(--surface));
}
.cife-ws-option:focus { outline: none; }

/* Type segmented buttons. Same shape as the Issues/New sidebar's
   type picker - one row of pill-edged buttons, only one is active.
   Keyboard: tab to the row, arrow keys move within, Space picks. */
.cife-type-segmented {
    display: inline-flex; gap: 0;
    border: 1px solid var(--border); border-radius: 8px;
    overflow: hidden; background: var(--surface);
}
.cife-type-btn {
    appearance: none; border: 0; background: transparent;
    padding: .4rem .8rem;
    font-size: 12px; font-family: inherit; color: var(--text);
    cursor: pointer;
    border-right: 1px solid var(--border);
    transition: background .12s, color .12s;
}
.cife-type-btn:last-child { border-right: 0; }
.cife-type-btn:hover { background: color-mix(in srgb, var(--text) 5%, var(--surface)); }
.cife-type-btn.is-active {
    background: color-mix(in srgb, var(--text) 7%, transparent); color: var(--text);
    font-weight: 600;
    }
.cife-type-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }

/* Chip rows for Assignees / Labels. Each chip is a click-toggle
   button that flips between is-on and base. The roster includes
   the user's avatar bubble; labels render with the workspace's
   label color as a leading dot. */
.cife-chip-row {
    display: flex; flex-wrap: wrap; gap: .35rem;
    padding: .25rem 0;
    min-height: 26px;
}
.cife-chip {
    display: inline-flex; align-items: center; gap: .35rem;
    appearance: none; border: 1px solid var(--border);
    background: var(--surface); color: var(--text);
    padding: .15rem .5rem;
    border-radius: 999px;
    font-size: 11px; font-family: inherit;
    cursor: pointer;
    transition: background .12s, border-color .12s;
}
.cife-chip:hover {
    border-color: color-mix(in srgb, var(--text) 25%, transparent));
}
.cife-chip.is-on {
    background: color-mix(in srgb, var(--text) 7%, transparent);
    border-color: transparent;
    color: var(--text);
}
.cife-chip-avatar {
    /* Sized container only. The canonical
       Burofolk.renderAvatarInto stamps either an <img> (photo
       path) or attaches the bf-avatar-letter class (letter path,
       per-user hue gradient) directly on this element. We MUST
       NOT pre-set background / color here because that would
       fight with the canonical paint rule. The layout's
       .bf-avatar-letter rule has !important to win unconditionally,
       but keeping this minimal is the right contract. */
    width: 18px; height: 18px; border-radius: 50%;
    flex: 0 0 auto;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 9px; font-weight: 600;
    line-height: 1;
    overflow: hidden;
}
.cife-chip-avatar img { width: 100%; height: 100%; object-fit: cover; }
.cife-chip-label-dot {
    width: 8px; height: 8px; border-radius: 50%;
    flex: 0 0 auto;
    background: var(--text-muted);
}
.cife-chip-empty {
    font-size: 11px; color: var(--text-muted); font-style: italic;
    padding: .15rem .25rem;
}

.cife-hint {
    margin: .25rem 0 0; font-size: 11px; color: var(--text-muted);
}
.cife-hint a { color: var(--primary); text-decoration: none; font-weight: 600; }
.cife-hint a:hover { text-decoration: underline; }
.cife-actions {
    /* Footer row: [Cancel] ... [primary | chevron].
       Top border separates actions from the form fields above so
       the eye groups submit + cancel as a distinct decision area.
       Equal vertical alignment + matched heights via min-height
       on every immediate child. */
    display: flex; justify-content: flex-end; align-items: center;
    gap: .55rem;
    /* Fixed footer below the scroll region: always visible, and because it
       sits OUTSIDE .cife-form-body the body's scrollbar never runs beside
       these buttons. */
    flex: 0 0 auto;
    padding: .85rem 1.1rem;
    background: var(--surface);
    border-top: 1px solid var(--border);
}
/* Footer hint: explains the disabled Create button (no workspace picked yet).
   margin-right:auto pushes Cancel + submit to the right; hidden once a
   workspace is selected. */
.cife-foot-hint {
    margin: 0 auto 0 0;
    display: inline-flex; align-items: center; gap: .5rem;
    font-size: 13px;
    font-weight: 600;
    color: var(--danger, #dc2626);
    line-height: 1.3;
}
.cife-foot-hint[hidden] { display: none; }
.cife-foot-hint svg { flex: 0 0 auto; width: 16px; height: 16px; }
.cife-actions > button {
    min-height: 38px;
    line-height: 1;
    padding: .5rem 1.1rem;
    border-radius: 8px;
    font-weight: 600;
    display: inline-flex; align-items: center; gap: .4rem;
    justify-content: center;
}

/* Split submit. ONE visually-fused control: container owns the
   border-radius + the focus / hover ring; halves are flush
   (no individual borders, no individual radii). The seam
   between primary + caret is a subtle inset shadow line, not a
   border, so it doesn't shift the caret's outer dimensions.
   Disabled / busy / hover all apply to the WHOLE split so the
   two halves never visually disagree. */
.cife-submit-split {
    position: relative;
    display: inline-flex;
    min-height: 38px;
    border-radius: 8px;
    background: var(--text);
    color: var(--surface, #fff);
    transition: background .15s, box-shadow .15s, transform .08s;
}
.cife-submit-split:not(.is-disabled):hover {
    background: color-mix(in srgb, var(--text) 5%, transparent);
    box-shadow: 0 1px 0 color-mix(in srgb, var(--primary) 35%, transparent),
                0 4px 14px -4px color-mix(in srgb, var(--primary) 35%, transparent);
}
.cife-submit-split:not(.is-disabled):active { transform: translateY(0.5px); }
.cife-submit-split.is-disabled,
.cife-submit-split:has(button:disabled) {
    /* Match Bootstrap's disabled feel without per-button opacity
       (which would create the dual-disabled-look that flagged the
       split as two buttons). */
    background: color-mix(in srgb, var(--primary) 38%, var(--surface-2));
    box-shadow: none;
    cursor: not-allowed;
}
.cife-submit-split.is-disabled button { cursor: not-allowed; }
.cife-submit-primary,
.cife-submit-caret {
    appearance: none;
    background: transparent !important;
    border: 0 !important;
    color: inherit !important;
    box-shadow: none !important;
    margin: 0;
    border-radius: 0;
    line-height: 1;
    font-family: inherit; font-weight: 600;
    cursor: pointer;
}
.cife-submit-primary:disabled,
.cife-submit-caret:disabled {
    cursor: not-allowed;
}
.cife-submit-primary {
    flex: 1 1 auto;
    padding: 0 1.1rem;
    min-width: 8.5rem;
    display: inline-flex; align-items: center; justify-content: center; gap: .45rem;
    border-top-left-radius: 8px;
    border-bottom-left-radius: 8px;
    font-size: 13px;
}
.cife-submit-caret {
    flex: 0 0 auto;
    padding: 0 .7rem;
    display: inline-flex; align-items: center; justify-content: center;
    border-top-right-radius: 8px;
    border-bottom-right-radius: 8px;
    /* Inset divider line - 1px, semi-transparent white. Reads as
       a quiet seam, not a button border. */
    box-shadow: inset 1px 0 0 color-mix(in srgb, white 28%, transparent) !important;
}
/* Focus rings: outline on the WHOLE split so keyboard users see
   one focus target, not two. */
.cife-submit-primary:focus-visible,
.cife-submit-caret:focus-visible {
    outline: 2px solid color-mix(in srgb, var(--primary) 65%, white);
    outline-offset: 2px;
    z-index: 1;
}

.cife-submit-spinner {
    width: 14px; height: 14px;
    border-radius: 50%;
    border: 2px solid currentColor;
    border-top-color: transparent;
    animation: cife-spin .7s linear infinite;
    display: none;
    flex: 0 0 auto;
}
.cife-submit-split.is-busy .cife-submit-spinner { display: inline-block; }
.cife-submit-split.is-busy .cife-submit-label { opacity: .85; }
@keyframes cife-spin { to { transform: rotate(360deg); } }

/* Dropdown menu hangs above the split (button row sits at the
   bottom of the modal so the menu opens upward). Slightly wider
   than the button so two-line items breathe; rounded + shadowed
   so it reads as a popover. */
.cife-submit-menu {
    position: absolute; right: 0; bottom: calc(100% + 8px);
    min-width: 240px;
    margin: 0; padding: 4px;
    list-style: none;
    background: var(--surface);
    border: 1px solid var(--border); border-radius: 10px;
    box-shadow: 0 14px 32px -10px rgba(0,0,0,.22),
                0 4px 12px -4px rgba(0,0,0,.10);
    z-index: 30;
    animation: cife-menu-pop .12s ease-out;
}
@keyframes cife-menu-pop {
    from { opacity: 0; transform: translateY(4px); }
    to   { opacity: 1; transform: translateY(0); }
}
.cife-submit-menu[hidden] { display: none; }
.cife-submit-menu-item {
    display: flex; align-items: flex-start; gap: .55rem;
    width: 100%;
    appearance: none; background: transparent; border: 0;
    padding: .55rem .7rem;
    border-radius: 6px;
    text-align: left;
    color: var(--text); font-family: inherit; font-size: 12px;
    cursor: pointer;
    line-height: 1.3;
}
.cife-submit-menu-item:hover,
.cife-submit-menu-item:focus-visible {
    background: color-mix(in srgb, var(--text) 5%, var(--surface));
    outline: none;
}
.cife-submit-menu-glyph {
    flex: 0 0 auto;
    width: 22px; height: 22px;
    margin-top: 1px;
    display: inline-flex; align-items: center; justify-content: center;
    border-radius: 6px;
    background: color-mix(in srgb, var(--primary) 14%, transparent);
    color: color-mix(in srgb, var(--primary) 75%, var(--text));
}
.cife-submit-menu-text {
    flex: 1 1 auto;
    display: flex; flex-direction: column; gap: .1rem;
    min-width: 0;
}
.cife-submit-menu-title { font-weight: 600; color: var(--text); }
.cife-submit-menu-sub { font-size: 10px; color: var(--text-muted); }

/* Split submit: primary button + chevron-toggle for the alternate
   "Create & stay" path. The two halves are visually fused: one
   rounded outline owned by the container, NO individual borders /
   radii on the halves, a 1px inset divider between them. Equal
   padding top + bottom means they match height regardless of
   font / icon. The spinner replaces the label width in-place
   when busy so the button doesn't reflow. */
.cife-submit-split {
    position: relative;
    display: inline-flex;
    border-radius: 8px;
    /* No overflow:hidden because the dropdown menu floats above
       and would get clipped. The halves' edges still align via
       the container's border-radius applied to its own children. */
}
.cife-submit-primary,
.cife-submit-caret {
    /* Drop default Bootstrap radii so the container owns the
       outer corners. Primary is squared on the right, caret is
       squared on the left; together they read as one button. */
    border-radius: 0;
    margin: 0;
    box-shadow: none !important;
}
.cife-submit-primary {
    border-top-left-radius: 8px;
    border-bottom-left-radius: 8px;
    border-right: 0;
    padding: .5rem 1rem;
    display: inline-flex; align-items: center; gap: .45rem;
    min-height: 38px;
    /* Lock minimum width so the spinner-only state doesn't
       shrink the button while loading. */
    min-width: 7.5rem;
    justify-content: center;
}
.cife-submit-caret {
    border-top-right-radius: 8px;
    border-bottom-right-radius: 8px;
    border-left: 0;
    padding: 0 .65rem;
    min-height: 38px;
    display: inline-flex; align-items: center; justify-content: center;
    /* Inset divider line, not a border, so it doesn't change
       the caret's outer dimensions. Half-opacity white-on-primary
       reads as a quiet seam. */
    box-shadow: inset 1px 0 0 color-mix(in srgb, white 28%, transparent) !important;
}
.cife-submit-caret:disabled,
.cife-submit-primary:disabled {
    opacity: .55;
    cursor: not-allowed;
}
.cife-submit-primary:focus-visible,
.cife-submit-caret:focus-visible {
    /* Single focus-ring framework via outline so the seam between
       the two halves stays visually flat. */
    outline: 2px solid color-mix(in srgb, var(--primary) 60%, white);
    outline-offset: 2px;
    z-index: 1;
}
.cife-submit-label {
    /* Label collapses width fairly during spinner mode so the
       button doesn't jump between states. */
    line-height: 1;
}
/* Spinner is hidden by default; .is-busy on the split flips both
   the spinner ON and the label OFF on the primary button. The
   caret stays visible but locked. */
.cife-submit-spinner {
    width: 14px; height: 14px;
    border-radius: 50%;
    border: 2px solid color-mix(in srgb, white 50%, transparent);
    border-top-color: white;
    animation: cife-spin .7s linear infinite;
    display: none;
    flex: 0 0 auto;
}
.cife-submit-split.is-busy .cife-submit-spinner { display: inline-block; }
.cife-submit-split.is-busy .cife-submit-label { opacity: .85; }
@keyframes cife-spin { to { transform: rotate(360deg); } }

.cife-submit-menu {
    position: absolute; right: 0; bottom: calc(100% + 6px);
    min-width: 220px;
    margin: 0; padding: 4px;
    list-style: none;
    background: var(--surface);
    border: 1px solid var(--border); border-radius: 8px;
    box-shadow: 0 12px 28px -8px rgba(0,0,0,.18);
    z-index: 20;
}
.cife-submit-menu[hidden] { display: none; }
.cife-submit-menu-item {
    display: flex; flex-direction: column; align-items: flex-start; gap: .15rem;
    width: 100%;
    appearance: none; background: transparent; border: 0;
    padding: .5rem .65rem;
    border-radius: 6px;
    text-align: left;
    color: var(--text); font-family: inherit; font-size: 12px;
    cursor: pointer;
}
.cife-submit-menu-item:hover,
.cife-submit-menu-item:focus-visible {
    background: color-mix(in srgb, var(--text) 5%, var(--surface));
    outline: none;
}
.cife-submit-menu-title { font-weight: 600; }
.cife-submit-menu-sub { font-size: 11px; color: var(--text-muted); }

/* Modal-wide busy lock: the form contents go non-interactive
   while the request is in flight. Pointer-events off + opacity
   dim signals "waiting"; the spinner inside the submit button
   is the active affordance. The submit button itself stays
   interactive (so its own disabled state shows) by lifting the
   pointer-events filter on the split container. */
.cife-form.is-busy .cife-field {
    pointer-events: none;
    opacity: .65;
}
.cife-form.is-busy .cife-submit-split { pointer-events: auto; opacity: 1; }
.cife-error {
    /* In the fixed footer (outside the scroll body): inset from the card
       edges and sit just above the action row. */
    margin: .75rem 1.1rem 0; padding: .55rem .7rem;
    background: color-mix(in srgb, #ef4444 12%, transparent);
    border: 1px solid color-mix(in srgb, #ef4444 38%, transparent);
    color: #b91c1c; border-radius: 8px;
    font-size: 12px;
}

/* Per-message header trigger (the "Create issue" button on the
   message detail row) + its bulk-toolbar twin. Same visual shape
   so the user learns one affordance and recognises it everywhere. */
.bf-mail-create-issue-trigger {
    display: inline-flex; align-items: center; gap: .35rem;
    background: transparent; border: 1px solid var(--border);
    color: var(--text-muted);
    padding: .25rem .55rem; border-radius: 6px;
    font-size: 11px; font-weight: 500;
    cursor: pointer;
    transition: background .12s, color .12s, border-color .12s;
}
.bf-mail-create-issue-trigger:hover {
    background: color-mix(in srgb, var(--text) 5%, transparent);
    color: var(--text);
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
}
.bf-mail-create-issue-trigger svg { flex: 0 0 auto; }

/* "Linked issues" chips on the message detail header.
   Reads "🐛 Linked issues · #142 Customer can't connect"; each
   issue chip is a clickable link to the issue detail in a new
   tab (different workspace context). */
.bf-mail-linked-issues {
    display: inline-flex; flex-wrap: wrap; gap: .35rem;
    align-items: center; font-size: 11px;
}
.bf-mail-linked-issues-label {
    color: var(--text-muted); font-weight: 600;
    display: inline-flex; align-items: center; gap: .25rem;
}
.bf-linked-issue-chip {
    display: inline-flex; align-items: center; gap: .35rem;
    padding: .15rem .5rem;
    background: color-mix(in srgb, var(--primary) 7%, var(--surface));
    border: 1px solid color-mix(in srgb, var(--primary) 22%, var(--border));
    color: var(--text);
    border-radius: 999px;
    text-decoration: none; font-weight: 500;
    transition: background .12s;
}
.bf-linked-issue-chip:hover {
    background: color-mix(in srgb, var(--text) 5%, var(--surface));
    text-decoration: none;
}
.bf-linked-issue-chip[data-no-access] {
    cursor: not-allowed; opacity: .7;
    background: var(--surface-2);
    border-style: dashed;
}
.bf-linked-issue-chip-num {
    font-variant-numeric: tabular-nums;
    color: var(--primary); font-weight: 600;
}

/* ------------------------------------------------------------
   Linked-issues card on the message-detail page header.
   Stacked layout: one row per linked issue, full-width, with
   state pill on the left (Open/Closed/No-access), the
   #N · Title in the middle (Title gets the remaining space and
   ellipses on overflow), workspace label on the right, and an
   external-link glyph as the call-to-action affordance. The
   whole row is the click target.
   ------------------------------------------------------------ */
.bf-linked-issues-card {
    margin: .65rem 0 .9rem;
    padding: .5rem .65rem .65rem;
    background: color-mix(in srgb, var(--primary) 4%, var(--surface));
    border: 1px solid color-mix(in srgb, var(--primary) 18%, var(--border));
    border-radius: 10px;
}
.bf-linked-issues-eyebrow {
    display: inline-flex; align-items: center; gap: .35rem;
    font-size: 10px; font-weight: 600; letter-spacing: .02em;
    color: var(--text-muted);
    margin-bottom: .45rem;
    text-transform: uppercase;
}
.bf-linked-issues-eyebrow svg { color: var(--primary); }
.bf-linked-issues-count {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 18px; height: 18px; padding: 0 .35rem;
    background: transparent; color: var(--primary);
    border-radius: 10px; font-size: 10px; font-weight: 700;
    margin-left: .15rem;
    text-transform: none; letter-spacing: 0;
}
.bf-linked-issues-list {
    display: flex; flex-direction: column; gap: .3rem;
}
.bf-linked-issue-row {
    display: flex; align-items: center; gap: .65rem;
    padding: .45rem .65rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    text-decoration: none;
    transition: border-color .12s ease, background .12s ease, transform .06s ease;
    min-width: 0;
}
.bf-linked-issue-row:hover:not([data-no-access]) {
    background: color-mix(in srgb, var(--text) 5%, var(--surface));
    border-color: color-mix(in srgb, var(--text) 25%, transparent));
    text-decoration: none;
    color: var(--text);
}
.bf-linked-issue-row:active:not([data-no-access]) {
    transform: translateY(1px);
}
.bf-linked-issue-row[data-no-access] {
    cursor: not-allowed; opacity: .65;
    border-style: dashed;
}
.bf-linked-issue-status {
    display: inline-flex; align-items: center; gap: .25rem;
    font-size: 10px; font-weight: 600; letter-spacing: .01em;
    padding: .15rem .5rem; border-radius: 999px;
    flex-shrink: 0; line-height: 1.4;
}
.bf-linked-issue-status.is-open {
    background: color-mix(in srgb, var(--text) 7%, transparent);
    color: var(--text);
}
.bf-linked-issue-status.is-closed {
    background: color-mix(in srgb, var(--primary) 16%, transparent);
    color: var(--primary);
}
.bf-linked-issue-status.is-no-access {
    background: color-mix(in srgb, var(--text-muted) 14%, transparent);
    color: var(--text-muted);
}
.bf-linked-issue-num {
    font-variant-numeric: tabular-nums;
    color: var(--text-muted); font-weight: 600;
    font-size: 12px;
    flex-shrink: 0;
}
.bf-linked-issue-title {
    flex: 1; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-weight: 500;
}
.bf-linked-issue-ws {
    font-size: 10px; color: var(--text-muted);
    padding: .1rem .45rem;
    background: var(--surface-2); border-radius: 999px;
    flex-shrink: 0;
    max-width: 40%;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.bf-linked-issue-open {
    color: var(--text-muted); flex-shrink: 0;
    transition: color .12s, transform .12s;
}
.bf-linked-issue-row:hover .bf-linked-issue-open {
    color: var(--text);
    transform: translate(1px, -1px);
}
@media (max-width: 540px) {
    .bf-linked-issue-ws { display: none; }
    .bf-linked-issue-open { display: none; }
}

/* Mail row "🐛 N" badge - tiny, sits next to the attachment /
   label badges. Survives soft-nav swap because the row HTML is
   fully re-rendered. */
.bf-mail-row-issue-badge {
    display: inline-flex; align-items: center; justify-content: center; gap: .15rem;
    padding: 0 .35rem; height: 18px;
    /* Zero-CLS: the badge is revealed by the linked-issues hydrator
       AFTER first paint. Reserve its slot width from the first
       frame so un-hiding it never narrows the ellipsizing
       .mail-from track mid-view. */
    min-width: 2.4rem;
    border-radius: 10px; font-size: 10px; font-weight: 700;
    background: color-mix(in srgb, var(--primary) 14%, transparent);
    color: var(--primary);
    line-height: 1;
}
/* Zero-CLS: hidden = invisible but still occupying its reserved
   slot, so the row geometry is identical before and after the
   hydrator's reveal (and for rows with no linked issues). */
.bf-mail-row-issue-badge[hidden] { visibility: hidden; }

/* Issue-side "source card". Premium relationship bar shown on
   the issue detail when it was born from an email. The layout
   reads as a refined card with three clear regions:
     left   - rounded icon tile (40x40)
     middle - eyebrow + subject + from-line (the dominant area)
     right  - "Open email ↗" CTA (refined inline button feel)
   Margin-top separates it from the meta line above (the user's
   complaint about the "stuck" look). NO badge pills - just
   typography + spacing + a tasteful tint. Hover lifts the
   surface; the CTA underline reveals; the whole card is the
   click target. */
.issue-source-card-wrap {
    margin: 18px 0 16px 0;
}
.issue-source-card {
    display: flex; align-items: center; gap: 14px;
    padding: 16px 18px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    color: var(--text);
    text-decoration: none;
    transition: background .14s ease, border-color .14s ease, box-shadow .14s ease;
}
a.issue-source-card:hover {
    background: color-mix(in srgb, var(--text) 5%, var(--surface));
    border-color: color-mix(in srgb, var(--text) 25%, transparent));
    box-shadow: 0 1px 0 color-mix(in srgb, var(--primary) 14%, transparent),
                0 6px 18px -8px color-mix(in srgb, var(--primary) 22%, transparent);
    text-decoration: none;
}
.issue-source-card[data-deleted] {
    background: var(--surface-2);
    border-style: dashed;
}

.issue-source-card-icon {
    flex: 0 0 auto;
    width: 40px; height: 40px;
    border-radius: 10px;
    display: inline-flex; align-items: center; justify-content: center;
    background: color-mix(in srgb, var(--primary) 12%, transparent);
    color: color-mix(in srgb, var(--primary) 75%, var(--text));
}

.issue-source-card-body {
    flex: 1 1 auto; min-width: 0;
    display: flex; flex-direction: column; gap: 2px;
}
.issue-source-card-eyebrow {
    font-size: 10px;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--text-muted);
    line-height: 1.4;
}
.issue-source-card-subject {
    font-size: 14px;
    font-weight: 600;
    color: var(--text);
    line-height: 1.4;
    margin-top: 1px;
    /* Two lines max for very long subjects so the card height is
       bounded; ellipsis after that. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    line-clamp: 2;
    overflow: hidden;
    text-overflow: ellipsis;
    word-break: break-word;
}
.issue-source-card-from {
    margin-top: 4px;
    font-size: 12px;
    color: var(--text-muted);
    line-height: 1.45;
    /* Allow wrap rather than hard ellipsis - the From line carries
       the sender identity and being able to read all of it
       matters more than keeping the card to one row. */
    word-break: break-word;
}
.issue-source-card-from-label {
    font-weight: 600;
    color: color-mix(in srgb, var(--text-muted) 70%, var(--text));
    margin-right: 4px;
}
.issue-source-card-from-addr {
    color: var(--text-muted);
}

.issue-source-card-cta {
    flex: 0 0 auto;
    display: inline-flex; align-items: center; gap: .35rem;
    align-self: center;
    margin-left: 8px;
    color: var(--primary);
    font-size: 12px;
    font-weight: 600;
    white-space: nowrap;
}
a.issue-source-card:hover .issue-source-card-cta {
    text-decoration: underline;
    text-underline-offset: 3px;
}
.issue-source-card-status {
    flex: 0 0 auto;
    align-self: center;
    margin-left: 8px;
    color: var(--text-muted);
    font-size: 11px;
    font-style: italic;
}

@media (max-width: 640px) {
    .issue-source-card {
        align-items: flex-start;
        flex-wrap: wrap;
    }
    .issue-source-card-cta,
    .issue-source-card-status {
        flex: 1 1 100%;
        margin-left: calc(40px + 14px);
        margin-top: 4px;
    }
}

/* Issue-side "Created from email" banner. STANDALONE LINE below
   the meta row so the long subject + sender never overflow into
   "opened … · N comments". Full-width card laid out as:
     [icon]  Created from email · <subject>          sender · workspace [↗]
   Hover lifts the border tint; the whole card is the click
   target when the viewer can open the source. The chevron / open-
   in-new-tab glyph appears on the right only on the clickable
   variant. The metadata-only variant (deleted / no-access)
   renders the same shape with dashed border + no hover. */
.issue-from-email-banner {
    margin: 0 0 14px 0;
}
.issue-from-email-banner-card {
    display: flex; align-items: center; gap: .65rem;
    padding: .55rem .8rem;
    background: color-mix(in srgb, var(--primary) 5%, var(--surface));
    border: 1px solid color-mix(in srgb, var(--primary) 20%, var(--border));
    border-radius: 8px;
    font-size: 12px; color: var(--text);
    text-decoration: none;
    transition: background .12s, border-color .12s;
    line-height: 1.3;
}
a.issue-from-email-banner-card:hover {
    background: color-mix(in srgb, var(--text) 5%, var(--surface));
    border-color: color-mix(in srgb, var(--text) 25%, transparent));
    text-decoration: none;
}
.issue-from-email-banner-card[data-deleted] {
    cursor: default; opacity: .85;
    background: var(--surface-2);
    border-style: dashed;
}
.issue-from-email-banner-icon {
    flex: 0 0 auto;
    display: inline-flex; align-items: center; justify-content: center;
    width: 28px; height: 28px;
    border-radius: 6px;
    background: color-mix(in srgb, var(--primary) 14%, transparent);
    color: color-mix(in srgb, var(--primary) 70%, var(--text));
}
.issue-from-email-banner-body {
    flex: 1 1 auto;
    display: inline-flex; flex-wrap: wrap; align-items: baseline;
    gap: .35rem .55rem;
    min-width: 0;
}
.issue-from-email-banner-label {
    color: var(--text-muted);
    font-weight: 600;
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: .04em;
    flex: 0 0 auto;
}
.issue-from-email-banner-subj {
    font-weight: 600; color: var(--text);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    min-width: 0;
}
.issue-from-email-banner-from {
    flex: 0 0 auto;
    color: var(--text-muted); font-size: 11px;
    display: inline-flex; align-items: center; gap: .35rem;
    max-width: 40%;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.issue-from-email-banner-ws {
    padding: .05rem .4rem;
    background: var(--surface-2);
    border-radius: 999px;
    font-size: 10px;
    color: var(--text-muted);
    flex: 0 0 auto;
}
.issue-from-email-banner-open {
    flex: 0 0 auto;
    color: var(--text-muted);
    display: inline-flex; align-items: center;
}
a.issue-from-email-banner-card:hover .issue-from-email-banner-open {
    color: var(--text);
}

/* Narrow viewport: stack the body + sender vertically so neither
   gets ellipsised into illegibility. The icon + open-glyph stay
   horizontally anchored. */
@media (max-width: 720px) {
    .issue-from-email-banner-card {
        flex-wrap: wrap;
    }
    .issue-from-email-banner-from {
        max-width: 100%;
        flex: 1 1 100%;
        margin-left: calc(28px + .65rem);
    }
}

/* Cross-scope hint banner - rendered below the filter pills when an
   active text search has matches in folders the current scope
   excludes (Spam/Trash/Drafts on All mail, etc.). Inline, single
   line, low-noise; the per-folder counts are clickable links the
   user can follow. Sits at the top of the main region, under the
   toolbar and above the list - the region the lazy fragment fills,
   so its arrival never moves the filter band above (2026-09-07). */
.cross-scope-hint {
    display: flex; align-items: center; gap: .5rem;
    flex: 0 0 auto;
    margin: 0 16px 8px;
    padding: .35rem .6rem;
    border-radius: 8px;
    background: color-mix(in srgb, var(--primary) 6%, var(--surface));
    color: var(--text-muted);
    font-size: 11px;
    line-height: 1.3;
}
.cross-scope-hint-icon {
    display: inline-flex; align-items: center;
    color: color-mix(in srgb, var(--primary) 65%, var(--text-muted));
    flex: 0 0 auto;
}
.cross-scope-hint-text { display: inline; }
.cross-scope-hint-link {
    color: var(--primary);
    text-decoration: none;
    font-weight: 600;
    border-radius: 4px;
    padding: 0 .15rem;
}
.cross-scope-hint-link:hover,
.cross-scope-hint-link:focus-visible {
    background: color-mix(in srgb, var(--text) 5%, transparent);
    text-decoration: underline;
    outline: none;
}

/* "Filters" lead-in - quieter than the chips themselves, with a small funnel
   icon so the bar immediately reads as "active filters" even when a user has
   only one chip. */
/* An empty pills segment yields its space: with no pill, no save,
   no clear to paint, the segment would otherwise reserve a blank
   40px stripe once the band wraps (tablet widths). The element
   stays in the DOM for the soft-nav swap - it just doesn't paint.
   Single-element :has(), not the [src]-style invalidation cliff. */
.filter-pills:not(:has(.filter-pill)) { display: none; }

/* Keyboard visibility for the band's controls: a quiet ink ring,
   same family as the chrome buttons - never the violet accent. */
.qf-chip:focus-visible,
.filter-pill.save-view-pill:focus-visible,
.filter-pill-remove:focus-visible,
.view-def-toggle:focus-visible {
    outline: 2px solid color-mix(in srgb, var(--text) 40%, transparent);
    outline-offset: 1px;
}

/* The view-definition popover settles in rather than appears: a
   140ms fade + 3px drop, the same entrance grammar as the advanced
   panel's slide-down-sm. Decorative, so it switches off under
   prefers-reduced-motion. */
@media (prefers-reduced-motion: no-preference) {
    .view-def-pop:not([hidden]) {
        animation: bf-pop-settle .14s ease-out;
    }
}
@keyframes bf-pop-settle {
    from { opacity: 0; transform: translateY(-3px); }
    to { opacity: 1; transform: none; }
}

/* Filter pill - structured as: [icon] [label] [value] [×].
   label is muted, value is semibold - reads like "from: **alice**" without
   the old awkward ":value" rendering. Body of the pill is informational
   (span), only the × removes. */
.filter-pill {
    display: inline-flex; align-items: center; gap: .35rem;
    padding: 0 .35rem 0 .55rem;
    /* One control family with the quick chips: same 28px height,
       same 6px corner. Three heights and three radii in one strip
       read as three different toolbars. */
    height: 28px;
    box-sizing: border-box;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
    color: var(--text);
    text-decoration: none; font-size: 11px;
    line-height: 1.15;
    cursor: default;
    box-shadow: 0 1px 1px rgba(15, 23, 42, .03);
    transition: border-color .12s ease, box-shadow .12s ease;
    max-width: 320px;
}
.filter-pill:hover { border-color: var(--border-strong); box-shadow: 0 1px 3px rgba(15, 23, 42, .06); }

.filter-pill-icon {
    display: inline-flex; align-items: center; justify-content: center;
    width: 16px; height: 16px;
    /* Stroke-SVG family (same 14×14 as the quick chips) - never an
       emoji: color-font glyphs carry their own baseline metrics and
       float off the text line. Centered by the flex box, so icon,
       label, value and × sit on one line by construction. */
    color: var(--text-muted);
    flex: 0 0 auto;
}
.filter-pill-icon svg { display: block; }
.filter-pill-icon--star { color: #eab308; }

.filter-pill-label {
    color: var(--text-muted);
    font-weight: 500;
    flex: 0 0 auto;
}
.filter-pill-value {
    color: var(--text);
    font-weight: 600;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    /* It clips at its own line box: room for accents and descenders. */
    line-height: 1.3;
    max-width: 200px;
    min-width: 0;
}
/* Toggle-style chips (has attachment, unread only, starred only) have no
   value - bump the label weight so it reads as the chip's identity. */
.filter-pill--toggle .filter-pill-label {
    color: var(--text); font-weight: 600;
}

.filter-pill-remove {
    display: inline-flex; align-items: center; justify-content: center;
    width: 18px; height: 18px;
    border-radius: 4px;
    border: 0; background: transparent; padding: 0;
    color: var(--text-subtle);
    cursor: pointer;
    text-decoration: none;
    transition: background .12s ease, color .12s ease;
    margin-left: .1rem;
    flex: 0 0 auto;
}
.filter-pill-remove svg { width: 12px; height: 12px; display: block; }
.filter-pill-remove:hover { background: var(--danger-bg); color: var(--danger); }
.filter-pill-remove:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 1px;
}

/* "Clear all" is a subtle trailing link-button - not a chip, since it IS
   the wrap-up action, not a filter. */
.filter-pill.clear-all {
    background: transparent; border-color: transparent; box-shadow: none;
    color: var(--text-muted); text-decoration: underline;
    cursor: pointer; padding: 0 .7rem;
    font-size: 11px; font-weight: 500;
    margin-left: .15rem;
}
.filter-pill.clear-all:hover { color: var(--danger); background: transparent; }

/* "Save as view" - a QUIET ghost pill. It only appears when the
   current query differs from every named view, so its presence is
   already the signal; ink fill belongs to the page's primary action
   (Compose), not to a contextual affordance. */
.filter-pill.save-view-pill {
    display: inline-flex; align-items: center; gap: .35rem;
    padding: 0 .7rem;
    background: transparent;
    border: 1px solid var(--border);
    color: var(--text-muted);
    font-weight: 600;
    font-size: 11px;
    border-radius: 6px;
    cursor: pointer;
    transition: background .12s ease, color .12s ease, border-color .12s ease;
    box-shadow: none;
    margin-left: .3rem;
}
.filter-pill.save-view-pill:hover {
    background: var(--surface-2);
    border-color: var(--border-strong);
    color: var(--text);
}
.filter-pill.save-view-pill svg { color: currentColor; }

/* Keyboard help dialog */
dialog.kbd-help {
    border: 1px solid var(--border); border-radius: 10px; padding: 1.5rem 1.75rem;
    background: var(--surface); color: var(--text); box-shadow: var(--shadow);
    max-width: 420px; width: 90vw;
}
dialog.kbd-help::backdrop { background: rgba(15,23,42,.5); backdrop-filter: blur(2px); }
dialog.kbd-help h3 { font-size: 15px; font-weight: 700; margin: 0 0 1rem; }
dialog.kbd-help dl { display: grid; grid-template-columns: auto 1fr; gap: .5rem 1rem; margin: 0 0 1rem; }
dialog.kbd-help dt { font-family: inherit; }
dialog.kbd-help dd { margin: 0; color: var(--text-muted); font-size: 12px; }
dialog.kbd-help kbd {
    background: var(--surface-2); border: 1px solid var(--border);
    border-radius: 4px; padding: 1px 6px; font-family: ui-monospace, monospace;
    font-size: 11px; color: var(--text);
}

/* Typography */
h1, h2, h3, h4, h5, h6 { letter-spacing: -.015em; color: var(--text); }
.page-title,
.page-subtitle,
.section-title { cursor: default; user-select: none; }
.page-title { font-size: 1.5rem; font-weight: 700; margin: 0; }
.page-subtitle { font-size: 12px; color: var(--text-muted); margin-top: .25rem; }
.section-title { font-size: 14px; font-weight: 600; color: var(--text); margin: 0 0 .75rem; }

/* Cards */
.card.surface {
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--surface);
    box-shadow: var(--shadow-sm);
    color: var(--text);
}
html[data-theme="dark"] .card.surface,
html[data-theme="dark"] .card { background: var(--surface); color: var(--text); }
.card.surface .card-body { padding: 1.25rem 1.5rem; }

/* Buttons. PRIMARY IS INK, never a saturated brand fill: the filled violet
   read as loud and cheap next to the calm surfaces (user: "we hate them").
   Solid ink inverts naturally in dark mode (--text flips), hierarchy comes
   from contrast and weight; the accent stays where it belongs - text verbs,
   focus rings, selection, progress. Do not reintroduce a violet-filled
   button anywhere. */
.btn { border-radius: 6px; font-weight: 500; font-size: 12px; padding: .5rem 1rem; }
.btn-primary {
    background: var(--text);
    border-color: var(--text);
    color: var(--surface, #fff);
}
.btn-primary:hover {
    background: color-mix(in srgb, var(--text) 84%, var(--surface));
    border-color: color-mix(in srgb, var(--text) 84%, var(--surface));
    color: var(--surface, #fff);
}
.btn-primary:focus, .btn-primary:focus-visible {
    /* Bootstrap's .btn:focus-visible re-applies ITS blue hover fill; the ink
       stays ink under keyboard focus (charter: primary = ink, never accent) -
       the ring alone carries the focus signal. */
    background: var(--text);
    border-color: var(--text);
    color: var(--surface, #fff);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 25%, transparent);
}
/* Cleaner disabled state than Bootstrap's washed-out violet (which
   reads as "broken primary" rather than "not yet ready"). Neutral
   greys read as a clearly inactive surface; a faint dotted border
   keeps the affordance shape so the button doesn't disappear. */
.btn-primary:disabled,
.btn-primary.disabled,
.btn-primary.is-disabled {
    background: var(--surface-2) !important;
    border-color: var(--border) !important;
    color: var(--text-subtle) !important;
    box-shadow: none !important;
    opacity: 1 !important;
    cursor: not-allowed;
}
.btn-primary:disabled svg,
.btn-primary.disabled svg,
.btn-primary.is-disabled svg { opacity: .55; }
/* Destructive verbs are RED, never ink (owner, 2026-08-23: a deletion's confirm must
   read as the dangerous choice at a glance - a dark fill let "Move to trash" pass for
   an ordinary primary). Same discipline as the ink primary: solid token fill, quiet
   hover mix, ring-only focus - and the tokens carry both themes. */
.btn-danger {
    background: var(--danger);
    border-color: var(--danger);
    color: #fff;
}
.btn-danger:hover {
    background: color-mix(in srgb, var(--danger) 86%, var(--surface));
    border-color: color-mix(in srgb, var(--danger) 86%, var(--surface));
    color: #fff;
}
.btn-danger:focus, .btn-danger:focus-visible {
    background: var(--danger);
    border-color: var(--danger);
    color: #fff;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 30%, transparent);
}
.btn-danger:disabled,
.btn-danger.disabled,
.btn-danger.is-disabled {
    background: var(--surface-2) !important;
    border-color: var(--border) !important;
    color: var(--text-subtle) !important;
    box-shadow: none !important;
    opacity: 1 !important;
    cursor: not-allowed;
}
.btn-lg { padding: .65rem 1.5rem; font-size: 14px; border-radius: 10px; }
.btn-sm { padding: .3rem .7rem; font-size: 11px; border-radius: 6px; }
.btn-outline-secondary { color: var(--text-muted); border-color: var(--border-strong); background: var(--surface); }
.btn-outline-secondary:hover { background: var(--surface-2); color: var(--text); border-color: var(--border-strong); }
/* "Outline primary" joins the neutral family: ink text on a quiet hairline.
   The violet STROKE version was the last button wearing the accent as chrome
   (user: "purple filling and stroke at MANY places"); emphasis among peers
   comes from the semibold ink, not a hue. */
.btn-outline-primary { color: var(--text); border-color: var(--border-strong); background: var(--surface); font-weight: 600; }
.btn-outline-primary:hover { background: var(--surface-2); color: var(--text); border-color: var(--border-strong); }

/* ── THE page-head action cluster ──
   One language for every page header's actions (Contacts, Products, …): peer
   outline buttons with a leading icon + label, consistent height + gap - no
   quiet text links, no filled-primary sibling shouting over its peers. Legacy
   contacts-* selectors fold into the same declarations so surfaces never drift. */
.bf-head-actions,
.contacts-head-actions { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.bf-head-actions .btn,
.contacts-head-actions .btn,
.contacts-empty-actions .btn { display: inline-flex; align-items: center; gap: 0.4rem; }
.bf-head-actions .btn svg,
.contacts-head-actions .btn svg,
.contacts-empty-actions .btn svg { width: 15px; height: 15px; flex: 0 0 auto; }

/* ── THE "?" help affordance ──
   A small glyph carrying its explanation on title=, rendered by the global
   bf-tooltip bubble (hover shows, click pins) - the platform's answer to
   inline description clutter, on every page (Products, Data models, …). */
.pc-help {
    display: inline-flex; align-items: center; justify-content: center;
    width: 15px; height: 15px; border-radius: 50%; border: 1px solid var(--border-strong);
    color: var(--text-subtle); font-size: 9px; font-weight: 700; cursor: default; line-height: 1; /* never the ? cursor over a ? glyph - one signal, once (owner, 2026-08-28) */
    /* ONE spacing + ONE vertical seat everywhere: the markup keeps the glyph
       ADJACENT to its label (no stray whitespace - swept), so this margin is
       the single source of the gap, and middle-alignment keeps every "?" on
       the same line seat whatever the label. */
    margin-left: 5px;
    vertical-align: middle;
    margin-top: -1px;
}
.pc-help:hover, .pc-help:focus-visible { color: var(--text); border-color: var(--text-subtle); outline: none; }

/* Forms */
.form-control { border: 1px solid var(--border-strong); border-radius: 8px; font-size: 13px; padding: .55rem .85rem; }
.form-control:focus { border-color: var(--primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 22%, transparent); }
.form-select:focus { border-color: var(--primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 22%, transparent); }
.form-control-lg { padding: .75rem 1rem; font-size: 14px; }
.form-label { font-size: 12px; font-weight: 500; color: var(--text); margin-bottom: .35rem; }
.form-text { color: var(--text-muted); font-size: 11px; }

/* Progress */
.progress { background: var(--surface-2); border-radius: 999px; overflow: hidden; }
.progress-bar { background: var(--primary); transition: width .3s ease; }

/* Badges & pills */
.pill { display: inline-flex; align-items: center; padding: .2rem .6rem; font-size: 11px; font-weight: 500; border-radius: 999px; background: var(--surface-2); color: var(--text-muted); }
.pill.pill-primary { background: var(--primary-bg); color: var(--primary); }
.pill.pill-success { background: var(--success-bg); color: var(--success); }
.pill.pill-warning { background: var(--warning-bg); color: var(--warning); }
.pill.pill-danger { background: var(--danger-bg); color: var(--danger); }

/* Alerts */
.alert { border-radius: 8px; border: 1px solid transparent; font-size: 12px; padding: .75rem 1rem; }
.alert-danger { background: var(--danger-bg); border-color: #fecaca; color: var(--danger); }
.alert-info { background: var(--primary-bg); border-color: #c7d2fe; color: var(--primary); }

/* Hero (legacy - used in a few places) */
.hero { padding: 4rem 1.5rem 3rem; text-align: center; }
.hero h1 { font-size: 2.5rem; font-weight: 800; letter-spacing: -.03em; margin-bottom: .75rem; }
.hero p { font-size: 15px; color: var(--text-muted); max-width: 560px; margin: 0 auto 1.5rem; }

/* ==============================================================
   Home landing - hero + stats + recent workspaces + activity feed.
   Width widens past the empty-state max so the two-column dashboard
   doesn't feel cramped at desktop sizes; collapses to a single column
   on narrow viewports below.
   ============================================================== */
/* Viewport-fit home (user 2026-08-07: "the page must never be
   scrollable so content must have dynamic layout based on screen
   size"): the landing fills .app-page's scrollport exactly and
   flexes as a column - banners keep their size, the brand stage
   absorbs the remainder, and the hero's sizes scale with the
   dynamic viewport (the dvh clamps in home-waiting.css) - so the
   surface fits any screen height with nothing below the fold.
   min-height and NO overflow declaration, deliberately: a
   pathological banner stack still grows and scrolls honestly
   rather than clipping content. */
.home-landing {
    max-width: 1080px;
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    min-height: 100%;
    padding: 0 1.5rem 16px;
    padding-bottom: clamp(6px, 2vh, 16px);
    padding-bottom: clamp(6px, 2dvh, 16px);
}

.home-hero { text-align: center; padding: 4rem 0 2rem; }
.home-hero-compact { padding: 3rem 0 1.5rem; }
/* Lock the rotating-welcome hero to a fixed minimum height. The
   typewriter clears + retypes the headline/subtitle on JS load, and
   different templates wrap to different line counts; min-height on the
   inner h1/p alone wasn't enough because their `display: inline-flex`
   nested inside an inline parent created subtle baseline shifts that
   the user still saw as CLS. Reserving the worst-case height on the
   <section> itself takes that variability out of the layout entirely -
   nothing below the hero can ever shift due to the welcome message. */
.home-hero-compact.home-hero-rotating {
    min-height: 9.5rem;
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
}
.home-hero h1 {
    font-size: 2.25rem; font-weight: 700; letter-spacing: -.03em;
    color: var(--text); margin: 0 0 .6rem; line-height: 1.15;
    /* No min-height here. The outer .home-hero-compact.home-hero-rotating
       block already reserves 9.5rem total for CLS protection - padding
       this h1 out to 2.3em on top of that left a visible gap below
       single-line headlines like "Stay informed." which the user
       flagged as "big gap between these two lines". */
}
.home-hero-sub {
    font-size: 13px; color: var(--text-muted);
    max-width: 520px; margin: 0 auto 1.5rem; line-height: 1.55;
    /* No subtitle min-height either - same reasoning. The outer
       hero block holds layout stable; padding the subtitle box out
       to 3 lines just for "Click a workspace below to jump into
       its inbox." opened a phantom gap below the hero. */
}

/* ── Birthday celebration banner ═══════════════════════════════════
   Pinned above the Friday banner whenever someone in the user's circle
   has a birthday today. Visual goes louder than the rest of the home
   page on purpose - this is a once-a-year moment per teammate; the
   page should feel like a tiny celebration. Multi-shape confetti,
   tilted cake mark with glowing flickering candles, gradient border
   that subtly shimmers - and one chip per celebrant on the trailing
   edge (profile-card trigger + optional send-a-cookie action). All
   motion is decorative and switches off under prefers-reduced-motion. */
.bday-banner {
    position: relative;
    display: flex; align-items: center; gap: 1.1rem;
    /* The home surface is a height-constrained column flexbox; without
       this the banner is the item that gives way and the cake mark gets
       clipped to a sliver. The banner is rigid - widgets below flex. */
    flex-shrink: 0;
    /* Right padding clears the absolute close button so the celebrant
       chips can hug the trailing edge without colliding with it. */
    padding: 1rem 2.75rem 1rem 1.15rem;
    margin: 0 0 1.25rem;
    border-radius: 16px;
    background:
        radial-gradient(1200px circle at 0% 0%, color-mix(in srgb, #ec4899 14%, transparent), transparent 40%),
        radial-gradient(1200px circle at 100% 100%, color-mix(in srgb, #06b6d4 16%, transparent), transparent 40%),
        linear-gradient(135deg, #fff7ed, #fdf2f8 50%, #f0f9ff);
    border: 1px solid color-mix(in srgb, #ec4899 22%, var(--border));
    overflow: hidden;
    box-shadow: 0 6px 24px -10px rgba(236, 72, 153, .35), 0 2px 6px -2px rgba(15, 23, 42, .08);
    animation: bday-banner-in .6s cubic-bezier(.2,.7,.3,1.2);
}
@keyframes bday-banner-in {
    from { opacity: 0; transform: translateY(-6px) scale(.98); }
    to   { opacity: 1; transform: none; }
}
/* Content blocks pop in staggered after the banner lands - the moment
   reads as "the celebration assembles" instead of one static paste. */
.bday-banner-mark,
.bday-banner-body,
.bday-banner-people {
    animation: bday-pop .55s cubic-bezier(.2,.7,.3,1.2) backwards;
}
.bday-banner-body   { animation-delay: .08s; }
.bday-banner-people { animation-delay: .16s; }
@keyframes bday-pop {
    from { opacity: 0; transform: translateY(7px) scale(.96); }
}
/* Subtle gradient ring around the banner that gently rotates - reads as
   "wrapping paper shimmer" without pulling focus from the message. */
.bday-banner::before {
    content: "";
    position: absolute; inset: -1px;
    border-radius: inherit;
    padding: 1px;
    background: conic-gradient(from var(--bday-angle, 0deg), #06b6d4, #8b5cf6, #ec4899, #f59e0b, #06b6d4);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
    mask-composite: exclude;
    pointer-events: none;
    animation: bday-shimmer 6s linear infinite;
}
@property --bday-angle { syntax: "<angle>"; inherits: false; initial-value: 0deg; }
@keyframes bday-shimmer { to { --bday-angle: 360deg; } }

/* Confetti backdrop - tiny Burofolk 3-bar marks flowing down with a
   gentle leaf-like sway. The mark keeps its brand colors and only
   wobbles a few degrees (a spinning logo reads as chaos; a drifting
   one reads as the brand celebrating). Horizontal spread comes inline
   from Razor (deterministic pseudo-random); size / timing derive from
   --bday-i here so no two marks move in lockstep. Sits under the
   content (z-index 0 vs 1) so text never fights the noise. */
.bday-confetti {
    position: absolute; inset: 0;
    overflow: hidden;
    pointer-events: none;
}
.bday-confetti-speck {
    position: absolute;
    top: -22px;
    opacity: 0;
    animation: bday-flow calc(4.6s + (var(--bday-i, 0) * .28s)) ease-in-out infinite;
    animation-delay: calc(var(--bday-i, 0) * -.6s);
}
.bday-confetti-speck:nth-child(even) { animation-name: bday-flow-sway; }
.bday-confetti-size-0 { width: 12px; height: 12px; }
.bday-confetti-size-1 { width: 16px; height: 16px; }
.bday-confetti-size-2 { width: 20px; height: 20px; }
@keyframes bday-flow {
    0%   { transform: translate3d(0, 0, 0) rotate(-14deg); opacity: 0; }
    12%  { opacity: .6; }
    50%  { transform: translate3d(12px, 66px, 0) rotate(10deg); }
    88%  { opacity: .45; }
    100% { transform: translate3d(-4px, 134px, 0) rotate(-12deg); opacity: 0; }
}
@keyframes bday-flow-sway {
    0%   { transform: translate3d(0, 0, 0) rotate(12deg); opacity: 0; }
    12%  { opacity: .55; }
    50%  { transform: translate3d(-14px, 66px, 0) rotate(-10deg); }
    88%  { opacity: .4; }
    100% { transform: translate3d(6px, 134px, 0) rotate(12deg); opacity: 0; }
}

.bday-banner-close {
    position: absolute;
    top: 8px; right: 8px;
    width: 26px; height: 26px;
    z-index: 2;
    display: inline-flex; align-items: center; justify-content: center;
    border: 0;
    border-radius: 999px;
    background: rgba(255, 255, 255, .55);
    color: #831843;
    cursor: pointer;
    backdrop-filter: blur(2px);
    transition: background .12s ease, color .12s ease, transform .12s ease;
}
.bday-banner-close:hover {
    background: #fff;
    color: #831843;
    transform: scale(1.05);
}
.bday-banner-close:focus-visible {
    outline: 2px solid #ec4899;
    outline-offset: 1px;
}
.bday-banner.is-closing {
    animation: bday-banner-out .24s ease forwards;
}
@keyframes bday-banner-out {
    to {
        opacity: 0;
        transform: translateY(-4px) scale(.98);
        max-height: 0;
        margin-top: 0;
        margin-bottom: 0;
        padding-top: 0;
        padding-bottom: 0;
        border-width: 0;
    }
}

/* The cake sits directly on the wash - no framing box - with a playful
   tilt and a soft pink shadow so it reads as a scene, not an icon. The
   flame glow halos live inside the SVG (radial gradients). */
.bday-banner-mark {
    flex: 0 0 auto;
    width: 64px; height: 64px;
    display: inline-flex; align-items: center; justify-content: center;
    transform: rotate(-4deg);
    filter: drop-shadow(0 6px 10px rgba(236, 72, 153, .3));
    z-index: 1;
}
.bday-banner-mark svg { display: block; }
.bday-flame {
    transform-origin: center bottom;
    animation: bday-flicker 1.4s ease-in-out infinite alternate;
}
.bday-flame-tall { animation-duration: 1.7s; animation-delay: -.4s; }
.bday-banner-mark svg .bday-flame:nth-of-type(3) { animation-duration: 1.2s; animation-delay: -.8s; }
@keyframes bday-flicker {
    from { transform: scaleY(.92) translateY(1px); }
    to   { transform: scaleY(1.05) translateY(-1px); }
}

.bday-banner-body {
    flex: 1 1 auto;
    min-width: 0;
    display: flex; flex-direction: column; gap: .15rem;
    z-index: 1;
}
.bday-banner-headline {
    font-size: 15px;
    font-weight: 800;
    color: #831843;
    letter-spacing: -.01em;
    line-height: 1.25;
}
.bday-banner-sub {
    font-size: 12px;
    color: #4a044e;
    line-height: 1.45;
}

/* Celebrant chips - one glassy pill per person on the trailing edge.
   The left half (avatar + name) opens the shared profile-card modal;
   the right half is the send-a-cookie action when the org's social
   settings allow it. position: relative anchors the shared
   .profile-cookie-burst emoji shower on a successful send. */
.bday-banner-people {
    margin-left: auto;
    display: flex; flex-wrap: wrap; gap: .45rem;
    align-items: center; justify-content: flex-end;
    z-index: 1;
}
.bday-person {
    position: relative;
    display: inline-flex; align-items: center; gap: .2rem;
    padding: .25rem;
    background: rgba(255, 255, 255, .6);
    border: 1px solid color-mix(in srgb, #ec4899 18%, var(--border));
    border-radius: 999px;
    backdrop-filter: blur(4px);
    box-shadow: 0 1px 4px rgba(15, 23, 42, .06);
}
.bday-person-open {
    display: inline-flex; align-items: center; gap: .45rem;
    padding: .15rem .5rem .15rem .15rem;
    border: 0;
    border-radius: 999px;
    background: transparent;
    font: inherit;
    color: #500724;
    cursor: pointer;
    transition: background .12s ease;
}
.bday-person-open:hover { background: rgba(255, 255, 255, .95); }
.bday-person-open:focus-visible,
.bday-cookie-btn:focus-visible {
    outline: 2px solid #ec4899;
    outline-offset: 1px;
}
.bday-person-name {
    font-size: 12px;
    font-weight: 650;
    max-width: 150px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.bday-banner-avatar {
    /* LAYOUT + size only. Fill / hue / weight / color come from the
       canonical `.bf-avatar-letter` rule. */
    width: 26px; height: 26px;
    border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 10px;
    text-transform: uppercase;
    flex: 0 0 auto;
    box-shadow: 0 1px 3px rgba(15, 23, 42, .12);
}
.bday-banner-avatar.is-self {
    box-shadow: 0 0 0 2px #ec4899, 0 1px 3px rgba(15, 23, 42, .15);
}
/* Compact cousin of .profile-cookie-btn (profile-card modal) - same
   amber→pink identity and green "sent" state so the gesture is
   recognisably ONE feature across surfaces. */
.bday-cookie-btn {
    display: inline-flex; align-items: center; gap: .35rem;
    padding: .3rem .7rem;
    background: linear-gradient(135deg, #f59e0b, #ec4899);
    color: #fff;
    border: 0;
    border-radius: 999px;
    cursor: pointer;
    font-family: inherit;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: -.005em;
    white-space: nowrap;
    box-shadow: 0 1px 2px rgba(15, 23, 42, .08), 0 3px 10px -3px color-mix(in srgb, #f59e0b 55%, transparent);
    transition: transform .12s ease, box-shadow .15s ease, filter .15s ease, opacity .15s ease;
}
.bday-cookie-btn:hover {
    transform: translateY(-1px);
    box-shadow: 0 1px 2px rgba(15, 23, 42, .1), 0 6px 14px -3px color-mix(in srgb, #f59e0b 65%, transparent);
    filter: brightness(1.04);
}
.bday-cookie-btn:active { transform: translateY(0); filter: brightness(.96); }
.bday-cookie-btn:disabled { cursor: default; transform: none; filter: none; }
.bday-cookie-btn:disabled:not(.is-sent) { opacity: .55; box-shadow: none; }
.bday-cookie-btn.is-sent {
    background: linear-gradient(135deg, #84cc16, #10b981);
    box-shadow: 0 1px 2px rgba(15, 23, 42, .08), 0 3px 10px -3px color-mix(in srgb, #10b981 55%, transparent);
}
.bday-cookie-glyph { font-size: 13px; line-height: 1; }
.bday-banner-overflow {
    display: inline-flex; align-items: center; justify-content: center;
    width: 26px; height: 26px;
    border-radius: 50%;
    background: rgba(15, 23, 42, .82);
    color: #fff;
    font-size: 10px; font-weight: 700;
}

/* Dark theme: deep plum→ink wash; the confetti, flame halos and chip
   glass pick up far more presence against it - the banner's "lit
   candles in a dim room" moment. */
html[data-theme="dark"] .bday-banner {
    background:
        radial-gradient(1200px circle at 0% 0%, color-mix(in srgb, #ec4899 22%, transparent), transparent 42%),
        radial-gradient(1200px circle at 100% 100%, color-mix(in srgb, #06b6d4 18%, transparent), transparent 42%),
        linear-gradient(135deg, #201427, #2a1424 50%, #0f1f2d);
    border-color: color-mix(in srgb, #ec4899 32%, var(--border));
    box-shadow: 0 8px 28px -12px rgba(236, 72, 153, .45), 0 2px 6px -2px rgba(0, 0, 0, .45);
}
html[data-theme="dark"] .bday-banner-headline { color: #fbcfe8; }
html[data-theme="dark"] .bday-banner-sub { color: #d4b8cb; }
html[data-theme="dark"] .bday-banner-close {
    background: rgba(15, 23, 42, .5);
    color: #fbcfe8;
}
html[data-theme="dark"] .bday-banner-close:hover { background: rgba(15, 23, 42, .85); color: #fff; }
html[data-theme="dark"] .bday-person {
    background: rgba(15, 23, 42, .45);
    border-color: color-mix(in srgb, #ec4899 30%, var(--border));
    box-shadow: 0 1px 4px rgba(0, 0, 0, .3);
}
html[data-theme="dark"] .bday-person-open { color: #fce7f3; }
html[data-theme="dark"] .bday-person-open:hover { background: rgba(255, 255, 255, .1); }
html[data-theme="dark"] .bday-banner-overflow { background: rgba(248, 250, 252, .16); }

/* Decorative motion off for users who asked for calm. The banner still
   celebrates - colors, chips and actions all remain. */
@media (prefers-reduced-motion: reduce) {
    .bday-banner,
    .bday-banner::before,
    .bday-banner-mark,
    .bday-banner-body,
    .bday-banner-people,
    .bday-flame {
        animation: none !important;
    }
    .bday-confetti { display: none; }
}

/* Chips drop to their own row under the copy once the single row gets
   tight; the row left-aligns with the text column (mark width + gap). */
@media (max-width: 900px) {
    .bday-banner { flex-wrap: wrap; }
    .bday-banner-people {
        flex-basis: 100%;
        margin-left: 0;
        justify-content: flex-start;
        padding-left: calc(64px + 1.1rem);
    }
}
@media (max-width: 560px) {
    .bday-banner { gap: .75rem; padding: .85rem 2.4rem .9rem .95rem; }
    .bday-banner-mark { width: 52px; height: 52px; }
    .bday-banner-mark svg { width: 48px; height: 48px; }
    .bday-banner-headline { font-size: 14px; }
    .bday-banner-people { padding-left: 0; }
}

/* Friday celebration banner. Sits between the welcome hero and the
   dashboard. Quiet, on-brand card - the 3-bar gradient mark on the
   left, an inline tagline + joke in the body. No confetti grid
   (felt circus-y), no refresh button (joke is deterministic per
   day so every user sees the same line). The decorative tint on the
   left edge picks up the brand cyan-violet-pink wash. */
.friday-banner {
    display: flex; align-items: center; gap: 1rem;
    padding: .85rem 1.1rem;
    margin: 0 0 1.25rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    position: relative;
    overflow: hidden;
    box-shadow: 0 4px 14px rgba(15, 23, 42, .04);
}
/* Soft brand-color glow on the leading edge. Feels like the 3-bar
   mark is "casting" a tint into the card without painting the whole
   surface. Falls off quickly so the body text reads on a near-white
   background. */
.friday-banner::before {
    content: "";
    position: absolute;
    left: 0; top: 0; bottom: 0;
    width: 35%;
    background: linear-gradient(90deg,
        rgba(6, 182, 212, .10),
        color-mix(in srgb, var(--primary) 6%, transparent) 50%,
        transparent);
    pointer-events: none;
}
html[data-theme="dark"] .friday-banner::before {
    background: linear-gradient(90deg,
        rgba(6, 182, 212, .18),
        color-mix(in srgb, var(--primary) 12%, transparent) 50%,
        transparent);
}
.friday-banner.is-fresh { animation: friday-pop .35s ease-out; }
@keyframes friday-pop {
    0%   { transform: translateY(-4px); opacity: 0; }
    100% { transform: translateY(0);    opacity: 1; }
}
.friday-banner-mark {
    flex: 0 0 auto;
    display: inline-flex; align-items: center; justify-content: center;
    width: 38px; height: 38px;
    border-radius: 10px;
    background: var(--surface-2);
    position: relative;
    z-index: 1;
}
.friday-banner-body {
    flex: 1 1 auto;
    min-width: 0;
    display: flex; align-items: baseline; gap: .65rem;
    flex-wrap: wrap;
    position: relative;
    z-index: 1;
}
.friday-banner-tagline {
    flex: 0 0 auto;
    font-family: "Plus Jakarta Sans", "Inter", -apple-system, system-ui, sans-serif;
    font-weight: 700;
    font-size: 13px;
    color: var(--text);
    letter-spacing: -.005em;
    white-space: nowrap;
}
.friday-banner-joke {
    flex: 1 1 auto;
    font-size: 13px;
    color: var(--text-muted);
    line-height: 1.5;
    overflow-wrap: anywhere;
}
@media (prefers-reduced-motion: reduce) {
    .friday-banner.is-fresh { animation: none; }
}
@media (max-width: 600px) {
    .friday-banner { padding: .75rem .9rem; }
    .friday-banner-body { flex-direction: column; gap: .15rem; align-items: flex-start; }
    .friday-banner-tagline { font-size: 12px; }
    .friday-banner-joke { font-size: 12px; }
}

/* Rotating welcome hero - typewriter effect. The headline text and
   the blinking caret sit on the same baseline; the caret keeps a
   fixed width via inline-block + a 2px wedge so the layout doesn't
   reflow as characters are added. The wrapper reserves room for the
   tallest possible single-line headline so the dashboard below
   doesn't jiggle while typing. */
.home-hero-rotating .home-hero-headline {
    display: inline-flex; align-items: baseline; gap: 1px;
    /* CLS protection comes from the outer .home-hero-compact.home-hero-rotating
       block (min-height: 9.5rem with flex centering) - that's what keeps
       content BELOW the hero from jumping when the typewriter clears
       and re-types. The headline itself doesn't need its own min-height
       (an earlier 2.3em pad here added a visible gap between the title
       and the subtitle on single-line headlines, which the user
       flagged as too much spacing). */
}
.home-hero-text { white-space: pre-wrap; }
.home-hero-caret {
    display: inline-block;
    width: 3px; height: 1.05em;
    background: var(--primary);
    border-radius: 1px;
    transform: translateY(.12em);
    animation: home-hero-caret-blink .9s steps(2, end) infinite;
}
/* When the typewriter finishes the caret fades to opacity:0 then gets
   .is-hidden for "stop the blink animation". `display:none` would
   recover its 3px slot - and the inline-flex parent has `text-align:
   center` upstream, so the headline would re-center and visibly shift
   the whole text by ~2px. visibility:hidden keeps the slot reserved,
   no layout move, just an invisible (and now still) caret. */
.home-hero-caret.is-hidden {
    visibility: hidden;
    animation: none;
}
@keyframes home-hero-caret-blink {
    0%, 50% { opacity: 1; }
    51%, 100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
    .home-hero-caret { animation: none; }
}

/* Empty-state ordered list - plain numbers, no cards */
.home-ol {
    list-style: none; padding: 0; margin: 2rem auto 0;
    max-width: 520px; counter-reset: step;
}
.home-ol li {
    counter-increment: step;
    padding: .75rem 0 .75rem 2.5rem;
    position: relative;
    font-size: 13px; color: var(--text-muted); line-height: 1.5;
    border-bottom: 1px solid var(--border);
}
.home-ol li:last-child { border-bottom: 0; }
.home-ol li::before {
    content: counter(step);
    position: absolute; left: 0; top: .7rem;
    width: 26px; height: 26px; border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 12px; font-weight: 600;
    color: var(--text-subtle); border: 1px solid var(--border-strong);
}
.home-ol li span { color: var(--text); font-weight: 600; }

/* Stats - large numbers, minimal chrome */
.home-stats {
    display: flex; justify-content: center; gap: 3rem;
    margin: 0 0 2rem; flex-wrap: wrap;
}
/* Stats are now <a> elements. Cursor goes pointer, no underline, but
   hover lifts the value subtly so the affordance reads. is-static is
   the fallback for stats that have no destination (viewer's view of
   the messages stat with no recent workspace, etc.). */
.home-stat {
    display: flex; flex-direction: column; align-items: center; gap: .25rem;
    text-decoration: none; color: inherit;
    padding: .25rem .65rem;
    border-radius: 8px;
    transition: background .12s ease, transform .08s ease;
}
a.home-stat { cursor: pointer; }
a.home-stat:hover { background: var(--surface-2); }
a.home-stat:hover .home-stat-value { color: var(--text); }
a.home-stat:active { transform: translateY(1px); }
.home-stat.is-static { cursor: default; }
.home-stat-value {
    font-size: 1.75rem; font-weight: 700; color: var(--text);
    letter-spacing: -.02em; line-height: 1;
    font-variant-numeric: tabular-nums;
    transition: color .12s ease;
}
.home-stat-label { font-size: 11px; color: var(--text-subtle); display: inline-flex; align-items: center; gap: .35rem; }
.home-stat-delta {
    font-size: 10px; font-weight: 600; color: var(--primary);
    background: var(--primary-bg); border-radius: 999px;
    padding: 1px 7px; letter-spacing: .01em;
}

/* Mailbox cards */
.quick-open-grid {
    display: flex; flex-direction: column; gap: .5rem;
    margin: 0;
}
.quick-open-card {
    display: flex; align-items: center;
    gap: var(--bf-dash-row-gap, 12px);
    padding: var(--bf-dash-row-pad-y, 10px) var(--bf-dash-row-pad-x, 16px);
    background: var(--bf-dash-row-bg, transparent);
    border: none;
    border-radius: var(--bf-dash-radius-row, 0);
    text-decoration: none;
    transition: var(--bf-dash-row-transition);
}
.quick-open-card:hover {
    background: var(--bf-dash-row-bg-hover);
}
.quick-open-card:focus-visible {
    outline: none;
    box-shadow: var(--bf-dash-focus-ring);
    background: var(--bf-dash-row-bg-active);
}
.quick-open-card:hover .quick-open-arrow { color: var(--text-muted); transform: translateX(2px); opacity: .8; }
/* Body anchor inside .quick-open-card. The card itself is now a
   non-interactive flex container holding two sibling anchors: the
   icon link (-> /Workspaces/Detail/{id}) and this body link
   (-> workspace landing URL). Body owns the remaining horizontal
   space + the trailing arrow; click anywhere on the body row
   opens the workspace as before. */
.quick-open-card-body {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: var(--bf-dash-row-gap, 12px);
    text-decoration: none;
    color: inherit;
    border-radius: var(--bf-dash-radius-row, 0);
}
.quick-open-card-body:hover { text-decoration: none; }
.quick-open-card-body:focus-visible {
    outline: none;
    box-shadow: var(--bf-dash-focus-ring);
    background: var(--bf-dash-row-bg-active);
}
/* Slot-only: size + roundness. The canonical .bf-icon paint
   (auto-added by Html.WorkspaceIcon) owns background + color so
   the bubble reads the workspace's --bf-color. */
.quick-open-icon {
    --bf-icon-size: 30px;
    --bf-icon-radius: 50%;
    flex: 0 0 auto;
}

/* Workspace icon button on the /Workspaces page. Strips button chrome
   so the avatar bubble looks identical to the static viewer-side render
   - hover just adds a subtle ring to invite the click. */
/* Workspace icon affordance on the row header. Renamed to .bf-icon
   to avoid colliding with the existing .account-icon used by the sync
   indicator (also in this card) - the collision was making owners see
   the wrong glyph because both elements inherited the same styling. */
.account-icon-btn {
    position: relative;
    background: transparent; border: 0; padding: 0;
    flex: 0 0 auto;
    cursor: pointer; line-height: 0;
    border-radius: 50%;
    transition: box-shadow .15s ease, transform .08s ease;
}
.account-icon-btn:hover { box-shadow: 0 0 0 3px var(--primary-bg); transform: translateY(-1px); }
.account-icon-btn:active { transform: translateY(0); }
.account-icon-btn::after {
    /* Tiny pencil overlay - shown on hover to confirm "click to edit". */
    content: ""; position: absolute; right: -2px; bottom: -2px;
    width: 18px; height: 18px; border-radius: 50%;
    background: var(--primary) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><path d='M12 20h9'/><path d='M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z'/></svg>") center/11px no-repeat;
    box-shadow: 0 0 0 2px var(--surface);
    opacity: 0; transition: opacity .15s ease;
    z-index: 2;
}
.account-icon-btn:hover::after { opacity: 1; }
/* Pencil overlay is for owners only - viewers can't change the icon, so
   the affordance shouldn't pretend it's clickable. The non-owner render
   uses a <span class="account-icon-btn"> so the status dot still anchors,
   but the pencil pseudo-element is suppressed. */
span.account-icon-btn::after { content: none; }
span.account-icon-btn:hover { box-shadow: none; transform: none; }

/* Mini inline progress sliver. Replaces the old "Up to date / 100%"
   stripe that duplicated info already in the header status line. Only
   visible while an active sync runs; idle workspaces hide it via the
   [hidden] attribute. Slim and unobtrusive - the header status text is
   what reads first; this just adds visual confirmation that something
   is happening. */
.ws-mini-progress {
    display: flex; align-items: center; gap: .65rem;
    margin-top: .35rem;
    font-variant-numeric: tabular-nums;
    /* Reserve vertical space whether the bar is active or not so
       starting / stopping a sync doesn't push the rest of the card
       up or down. The bar + percent label render at ~16px combined
       height; min-height matches that so the empty state holds the
       same row. */
    min-height: 1rem;
}
/* When idle, keep the layout slot but hide visuals. visibility:hidden
   reserves the box; display:none would collapse it. The [hidden]
   attribute (HTML default = display:none) is intentionally NOT
   applied; the JS toggles `is-poll-hidden` instead. */
.ws-mini-progress[hidden],
.ws-mini-progress.is-poll-hidden {
    visibility: hidden;
}
.ws-mini-progress-bar {
    flex: 1 1 auto;
    height: 4px;
    background: var(--surface-2);
    border-radius: 999px;
    overflow: hidden;
}
.ws-mini-progress-bar .account-bar {
    height: 100%;
    background: var(--primary);
    border-radius: 999px;
    transition: width .35s ease;
}
.ws-mini-progress .account-pct {
    flex: 0 0 auto;
    font-size: 10px; font-weight: 700;
    color: var(--text-muted);
    min-width: 2.4rem;
    text-align: right;
}

/* The inline header status sub-line picks up a state-tinted dot. Using
   data-state instead of class swaps so the JS tick can update both the
   text content and the visual treatment in one assignment. */
.account-state-sub[data-state="syncing"]::before { background: var(--primary); }
.account-state-sub[data-state="paused"]::before { background: var(--warning); }
.account-state-sub[data-state="error"]::before { background: var(--danger); }
.account-state-sub[data-state="error"] { color: var(--danger) !important; }

/* =============================================================
   Canonical workspace-icon paint - SINGLE SOURCE OF TRUTH.
   =============================================================

   Mirrors the avatar pattern (`.bf-avatar-letter`): one rule
   defines the gradient + shadow + letter color; surfaces only
   override the size + radius via the CSS variables
   `--bf-icon-size` and `--bf-icon-radius`. NO parent-scope
   override of the background, border, or shadow.

   Why: the previous setup had THREE parent-scoped variants
   (`.ws-row .bf-icon`, `.ws-identity-plate .bf-icon`, bare
   `.bf-icon`) plus has-svg / has-emoji modifiers that ALL
   redefined the background. Same workspace icon rendered
   visibly different across surfaces - the "catastrophic"
   asymmetry the user kept flagging.

   Modifiers tweak the inner glyph (svg vs emoji vs letter)
   without touching the bubble. Letter uses white on the
   gradient; SVG glyphs draw in white via `currentColor`; emoji
   bubble keeps the gradient too so the glyph sits on a
   colored surface (the platform-consistent treatment, NOT
   the previous neutral-grey override). Pinned by
   WorkspaceGlyphSingleSourceOfTruthTests. */
.bf-icon {
    width: var(--bf-icon-size, 56px);
    height: var(--bf-icon-size, 56px);
    border-radius: var(--bf-icon-radius, 14px);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* HOLLOW identity (user 2026-07-30: no fill-generated avatars -
       "change the stroke color instead and keep transparent
       background"): the bubble keeps a transparent fill and the
       workspace's --bf-color paints the STROKE - the ring and the
       glyph/letter. Lists of these read as a quiet colour-coded
       index instead of a wall of saturated swatches; the retired
       identity gradient lives only in git history. */
    background: transparent;
    color: var(--bf-color, var(--primary));
    font-weight: 700;
    /* ONE weight for the hollow identity: a letter fills its slot like the
       neutral letter (0.9), an emoji and a glyph like theirs (0.83), a
       photo edge to edge - so a "B" beside a photo beside a 🔨 reads as ONE
       size, never a speck beside a tile (owner, 2026-09-12: "some workspace
       icons are tiny. some other are big. Find a radical solution"). The
       bubble-era 0.46 sized a letter floating inside a plate that no longer
       exists. !important: a per-surface font-size can never shrink it again
       (the same guard the neutral state carries). Pinned by
       BfIconAccentNeutralScaleParityTests. */
    font-size: calc(var(--bf-icon-size, 56px) * 0.9) !important;
    line-height: 1 !important;
    /* NO ring either (Loïc, 2026-07-30: "stop rendering outer
       rectangle, we don't want that. Just the icon or the image, don't
       add borders please"). The hollow pass replaced the identity
       gradient with a 1.5px inset stroke; this drops that too, so the
       identity is the GLYPH - a letter, a preset, an emoji, a photo -
       in the accent colour, with nothing drawn around it. */
    box-shadow: none;
    flex: 0 0 auto;
    transition: transform .15s ease;
    overflow: hidden;
}
/* Accent glyphs scale EXACTLY like their neutral siblings (0.83 -
   the clip-safe fill the neutral pass tuned): since the hollow pass
   dropped the gradient plate, an accent icon and a neutral icon are
   the same object - a bare glyph on the page surface - differing in
   PAINT only. The bubble-era ratios (0.5 svg / 0.55 emoji, sized for
   a glyph floating INSIDE a plate) made every user-picked icon
   render at half its un-styled neighbour - a 10px speck beside 17px
   fallbacks in the wiki tree's 20px slots ("this looks broken").
   Pinned by BfIconAccentNeutralScaleParityTests (as equality with
   the neutral factors, not as a number). */
.bf-icon.has-emoji {
    font-size: calc(var(--bf-icon-size, 56px) * 0.83) !important;
    line-height: 1 !important;
}
.bf-icon.has-svg svg {
    display: block;
    width: calc(var(--bf-icon-size, 56px) * 0.83);
    height: calc(var(--bf-icon-size, 56px) * 0.83);
    /* The glyph IS the stroke now - it carries the accent. */
    color: var(--bf-color, var(--primary));
    stroke: var(--bf-color, var(--primary));
}
.bf-icon.has-photo {
    /* Photo uploads fill the slot edge-to-edge: the image IS the
       icon, so there is no plate behind it and no frame around it
       ("Just the icon or the image, don't add borders"). Transparent
       stays transparent - a logo with an alpha background sits on the
       surface it was dropped on. */
    background: transparent;
}
.bf-icon.has-photo img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: inherit;
}
/* Neutral bubble - used by wiki pages with NO user-picked
   colour. No background, no shadow, no border: just the
   glyph itself painted in solid text colour. Without the
   gradient backdrop reading "I'm a bubble", the inner glyph
   gets MORE of the slot - bumped from the default 0.45× to
   0.78× of the slot size - so it carries the visual weight
   that the missing bubble would have. Reads as a pure
   outline icon on the page surface. */
.bf-icon.is-neutral {
    background: transparent;
    color: var(--text, #1f2328);
    box-shadow: none;
    /* Neutral glyphs render WITHOUT a gradient backdrop, so
       the glyph itself must carry all the visual weight the
       coloured bubble would have. The `!important` flags are
       intentional: per-surface rules like
       `.wiki-tree-item .wiki-tree-page-icon { font-size: 12px }`
       hardcode a small letter sized for the bubble case; they
       MUST NOT win against the neutral state's "fill the slot"
       rule. Equal-specificity-source-order cascade was
       silently picking the smaller per-surface value before
       this guard. */
    font-size: calc(var(--bf-icon-size, 56px) * 0.9) !important;
    line-height: 1 !important;
    letter-spacing: -0.03em;
}
.bf-icon.is-neutral.has-svg svg {
    color: var(--text, #1f2328);
    stroke: var(--text, #1f2328);
    /* SVGs paint their viewBox edge-to-edge; 0.83 of slot
       (0.92 × 0.9) avoids `overflow:hidden` clipping the stroked
       outline and matches the user-tuned neutral scale. */
    width: calc(var(--bf-icon-size, 56px) * 0.83) !important;
    height: calc(var(--bf-icon-size, 56px) * 0.83) !important;
}
.bf-icon.is-neutral.has-emoji {
    /* Emoji glyphs include their own padding by default;
       0.83 (0.92 × 0.9) keeps them from being clipped by the
       slot's overflow:hidden while staying in lockstep with
       the rest of the neutral scale. */
    font-size: calc(var(--bf-icon-size, 56px) * 0.83) !important;
    line-height: 1 !important;
}

/* Icon picker grid. .icon-grid is the legacy class (kept so any inline
   uses still work); .icon-picker-grid is the partial's class - both
   share the same 8-column layout and tile shell. The previous version
   only declared the grid on .icon-grid, so when the partial moved to
   .icon-picker-grid every tile fell to width:100% aspect-ratio:1 and
   filled the entire modal vertically with a single square. */
.icon-grid,
.icon-picker-grid {
    display: grid;
    /* minmax(0, 1fr) breaks the default min-content sizing - without it
       a tile with an emoji glyph that has a wide intrinsic font-metric
       can blow the column past 1fr and the whole grid overflows the
       modal. The user reported the picker bleeding outside the modal
       when packs were full; this is the load-bearing fix. */
    grid-template-columns: repeat(8, minmax(0, 1fr));
    gap: .4rem;
}
.icon-grid-svg { grid-template-columns: repeat(8, minmax(0, 1fr)); }
.icon-tile {
    width: 100%;
    /* min-width:0 tells the layout engine the tile may shrink below its
       intrinsic content - belt-and-suspenders alongside minmax(0,1fr). */
    min-width: 0;
    aspect-ratio: 1;
    background: var(--surface-2); border: 1px solid var(--border);
    border-radius: 10px;
    font-size: 1.4rem; line-height: 1;
    display: inline-flex; align-items: center; justify-content: center;
    cursor: pointer;
    color: var(--text-muted);
    transition: border-color .12s ease, transform .08s ease, background .12s ease, color .12s ease, box-shadow .12s ease;
}
.icon-tile:hover {
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
    background: color-mix(in srgb, var(--text) 5%, transparent);
    color: var(--text);
}
.icon-tile:active { transform: scale(.94); }
/* Premium selection style: glyph stays itself (no aggressive fill that
   recolors emojis into pure-white silhouettes). The selected tile gets
   a subtle primary-tinted backdrop, a 2px primary border, a corner
   check badge, and a soft elevation lift. Reads as "selected" without
   shouting. */
.icon-tile.is-selected {
    border-color: var(--primary);
    background: var(--primary-bg);
    color: var(--primary);
    box-shadow:
        0 0 0 1px var(--primary),
        0 8px 20px color-mix(in srgb, var(--primary) 22%, transparent);
    transform: translateY(-1px);
}
.icon-tile.is-selected::after {
    content: "";
    position: absolute;
    top: 5px; right: 5px;
    width: 16px; height: 16px;
    border-radius: 50%;
    background: var(--primary) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'><polyline points='20 6 9 17 4 12'/></svg>") center/10px no-repeat;
    box-shadow: 0 0 0 2px var(--surface);
}
.icon-tile { position: relative; }
.icon-tile svg { display: block; }
/* Emoji tile uses the same shell but a bigger glyph - emoji intrinsically
   carry color, so we drop the monochrome stroke treatment. */
.icon-tile-emoji-glyph {
    font-size: 1.55rem;
    line-height: 1;
    /* Don't let the emoji's intrinsic font-metric set the tile width. */
    min-width: 0;
    overflow: hidden;
}
/* Photo tiles: full-bleed image cropped to the rounded tile shape.
   The "no padding" treatment gives photos as much pixel area as the
   tile allows; the tile's existing border + selection ring still
   read clearly because they're drawn ON TOP of the image. */
.icon-tile-photo {
    padding: 0;
    overflow: hidden;
    background: var(--surface-2);
}
.icon-tile-photo-img {
    width: 100%; height: 100%;
    object-fit: cover;
    display: block;
    border-radius: 10px; /* match tile border-radius minus the 1px border */
}
/* Avatar bubble showing a photo (catalog photo token). Class-agnostic:
   ANY surface that wraps the icon (workspace card .bf-icon, home page
   .quick-open-icon, dropdown entries, ...) gets the photo treatment
   automatically. The wrapper supplies the size and round shape; the
   inner img is forced to fill exactly its container so a 256x256
   source can't blow out a 30x30 avatar slot. */
.has-photo {
    background: var(--surface-2);
    color: transparent;
    overflow: hidden;
    padding: 0;
}
.has-photo > img {
    width: 100%; height: 100%;
    object-fit: cover;
    display: block;
    border-radius: inherit;
}
.icon-tile.is-selected .icon-tile-emoji-glyph { /* stays full color on
    selection - the primary fill behind it is the selection cue. */ }

/* ═══════════════════════════════════════════════════════════════════
   Icon picker - sidebar layout (Apr 2026 redesign).
   ═══════════════════════════════════════════════════════════════════
   Modal is a fixed-height flex column. Body splits into:
     ┌─ shell ──────────────────────────────────────────┐
     │ [sidebar:packs] │ [grid + search]                 │
     └──────────────────────────────────────────────────┘
     ┌─ extras (paste emoji + upload) ──────────────────┐
     └──────────────────────────────────────────────────┘
   Footer (Reset / Cancel / Save).

   Why sidebar over the previous horizontal tab strip:
     · 14 packs no longer overflow off-screen.
     · Active pack is unmistakable (primary accent left edge).
     · Each pack name + count readable in a single column scan.
     · No tab clicks resize the modal - the right grid scrolls.

   On narrow viewports the sidebar collapses to a horizontal scroll
   strip stacked above the grid (same data, mobile-friendly form). */

.icon-picker-dialog {
    height: min(740px, 92vh);
    max-height: 92vh;
}
.icon-picker-content { display: flex; flex-direction: column; height: 100%; min-height: 0; }
.icon-picker-form { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.icon-picker-head { flex: 0 0 auto; }
.icon-picker-body {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
    padding: 0;
    gap: 0;
}

/* ── Shell: sidebar + grid region ─────────────────────────────── */
.picker-shell {
    display: grid;
    grid-template-columns: 220px minmax(0, 1fr);
    flex: 1 1 auto;
    min-height: 0;
    border-bottom: 1px solid var(--border);
}

/* ── Sidebar (left column) ─────────────────────────────────────
   Each pack is a full-width clickable row. Active state uses a
   left-edge accent + tinted background so it pops without being
   noisy. The whole list scrolls vertically if the user adds enough
   packs to overflow (14 fits at default modal height; not a
   concern today but the sidebar is robust to growth). */
.picker-sidebar {
    display: flex;
    flex-direction: column;
    gap: 1px;
    padding: .5rem .35rem;
    background: var(--surface-2);
    border-right: 1px solid var(--border);
    overflow-y: auto;
    min-height: 0;
}
.picker-sidebar::-webkit-scrollbar { width: 6px; }
.picker-sidebar::-webkit-scrollbar-thumb { border-radius: 4px; }
.picker-nav-item {
    display: flex; align-items: center;
    width: 100%;
    padding: .5rem .75rem;
    background: transparent;
    border: 0;
    border-left: 3px solid transparent;
    border-radius: 0 8px 8px 0;
    font-size: 12px; font-weight: 500;
    color: var(--text-muted);
    text-align: left;
    cursor: pointer;
    transition: background .12s ease, color .12s ease, border-color .12s ease;
    white-space: nowrap;
}
.picker-nav-item:hover {
    background: color-mix(in srgb, var(--text) 5%, transparent);
    color: var(--text);
}
.picker-nav-item.is-active {
    background: color-mix(in srgb, var(--text) 7%, transparent);
    color: var(--text);
    border-left-color: color-mix(in srgb, var(--text) 25%, transparent);
    font-weight: 600;
}
.picker-nav-label {
    flex: 1 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
}
.picker-nav-count {
    flex: 0 0 auto;
    margin-left: .5rem;
    font-size: 10px;
    font-weight: 600;
    color: var(--text-subtle);
    background: var(--surface);
    padding: 1px 7px;
    border-radius: 999px;
    line-height: 1.45;
    font-variant-numeric: tabular-nums;
}
.picker-nav-item.is-active .picker-nav-count {
    background: transparent;
    color: var(--primary);
}

/* ── Grid region (right column) ────────────────────────────────
   Search input pinned to the top, then the grid stack below it.
   The whole region is the scroll viewport so the search bar stays
   visible while the user pages through 90+ creatures. */
.picker-grid-region {
    display: flex;
    flex-direction: column;
    min-height: 0;
    min-width: 0;
    overflow-y: auto;
    overflow-x: hidden;
    padding: 1rem 1.1rem 1.1rem;
    scrollbar-gutter: stable;
}
.picker-grid-region::-webkit-scrollbar { width: 8px; }
.picker-grid-region::-webkit-scrollbar-thumb { border-radius: 4px; }

.picker-search-row {
    position: sticky; top: 0;
    z-index: 1;
    display: flex; align-items: center;
    gap: .5rem;
    margin: -.5rem -.4rem .9rem;
    padding: .5rem .65rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
}
.picker-search-icon {
    color: var(--text-subtle);
    flex: 0 0 auto;
}
.picker-search-input {
    flex: 1 1 auto;
    border: 0; outline: 0; background: transparent;
    font-size: 12px;
    color: var(--text);
    min-width: 0;
}
.picker-search-input::placeholder { color: var(--text-subtle); }
.picker-search-empty {
    flex: 0 0 auto;
    font-size: 11px;
    color: var(--text-muted);
    font-style: italic;
}

.picker-grid-stack {
    flex: 0 0 auto;
}
.picker-grid {
    display: grid;
    grid-template-columns: repeat(8, minmax(0, 1fr));
    gap: .45rem;
    min-width: 0;
}
.picker-grid.is-hidden { display: none; }
.icon-tile.is-filtered-out { display: none; }

/* Narrow viewports: fewer columns so tiles stay clickable and the
   sidebar collapses to a horizontal strip above the grid. */
@media (max-width: 800px) {
    .picker-shell { grid-template-columns: minmax(0, 1fr); }
    .picker-sidebar {
        flex-direction: row;
        gap: .25rem;
        padding: .45rem;
        border-right: 0;
        border-bottom: 1px solid var(--border);
        overflow-x: auto;
        overflow-y: hidden;
    }
    .picker-nav-item {
        flex: 0 0 auto;
        width: auto;
        padding: .4rem .65rem;
        border-left: 0;
        border-bottom: 2px solid transparent;
        border-radius: 6px;
    }
    .picker-nav-item.is-active {
        border-left: 0;
        border-bottom-color: color-mix(in srgb, var(--text) 25%, transparent);
    }
    .picker-grid { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}
@media (max-width: 540px) {
    .picker-grid { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}

/* ── Extras row (paste emoji + upload) ─────────────────────────
   Pinned below the shell on a faint background so the user knows
   these are alternatives to the main grid above, not part of it.
   Two compact columns. */
.picker-extras {
    flex: 0 0 auto;
    display: grid;
    grid-template-columns: 1fr 1.4fr;
    gap: 1.1rem;
    padding: .85rem 1.1rem;
    background: var(--surface-2);
}
.picker-extras-label {
    display: block;
    font-size: 10px;
    font-weight: 600;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: .04em;
    margin-bottom: .35rem;
}
.picker-extras-input { font-size: 13px; }
.picker-extras-emoji,
.picker-extras-upload { min-width: 0; }
@media (max-width: 600px) {
    .picker-extras { grid-template-columns: 1fr; gap: .75rem; }
}
.icon-upload-hint { font-size: 10px; }

/* Team icon button on the team card head - reuses the same affordance
   pattern as workspace .account-icon-btn (status-dot replaced by a
   subtle lift + ring on hover). The .team-card-icon span the button
   wraps inherits its size from the existing class. */
.team-icon-btn {
    background: transparent; border: 0; padding: 0;
    flex: 0 0 auto;
    cursor: pointer; line-height: 0;
    border-radius: 8px;
    transition: box-shadow .15s ease, transform .08s ease;
    position: relative;
}
.team-icon-btn:hover { box-shadow: 0 0 0 3px var(--primary-bg); transform: translateY(-1px); }
.team-icon-btn:active { transform: translateY(0); }
.team-icon-btn::after {
    content: ""; position: absolute; right: -3px; bottom: -3px;
    width: 16px; height: 16px; border-radius: 50%;
    background: var(--primary) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><path d='M12 20h9'/><path d='M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z'/></svg>") center/10px no-repeat;
    box-shadow: 0 0 0 2px var(--surface);
    opacity: 0; transition: opacity .15s ease;
}
.team-icon-btn:hover::after { opacity: 1; }

/* Team-card-icon variants. The shared WorkspaceIcon helper emits one of:
     .team-card-icon            (first-letter fallback - default gradient)
     .team-card-icon.has-emoji  (typed emoji - lighter background)
     .team-card-icon.has-svg    (catalog token - primary-tinted, SVG)
   Sized 28x28 to fit beside the team name. */
.team-card-icon.has-emoji {
    background: var(--surface);
    color: var(--text);
    border: 1px solid var(--border);
    font-size: 15px;
    box-shadow: none;
}
.team-card-icon.has-svg {
    background: var(--surface);
    color: var(--primary);
    border: 1px solid color-mix(in srgb, var(--primary) 30%, var(--border));
    box-shadow: none;
}
.team-card-icon.has-svg svg { display: block; width: 16px; height: 16px; }

/* ================== Collaborator profile card modal ==================
   Shown when an avatar pip in a workspace's collaborator strip is
   clicked. Read-only for now: avatar + name + email + role + which
   workspace this membership refers to. Banner-with-overlap pattern
   matches modern dashboards (Linear, Notion). All data is populated
   from the clicked pip's data-* attrs - no extra fetch.
   ===================================================================== */
/* The profile card is a body-level popover triggered by any avatar, including
   ones inside app dialogs (share, manage-access) that sit at z-index 1200. Its
   Bootstrap default (1055) rendered it BEHIND those dialogs; lift it above so a
   clicked avatar always pops on top, and carry a light scrim on the modal itself
   so whatever it opens over is dimmed (Bootstrap's backdrop sits at 1050, below
   those dialogs, so it cannot dim them). */
#profileCardModal { z-index: 2600; background: color-mix(in srgb, var(--text) 30%, transparent); }
.collab-profile-card {
    border-radius: 16px;
    border: 1px solid var(--border);
    overflow: hidden;
    position: relative;
    box-shadow: 0 24px 60px rgba(15, 23, 42, .25);
}
/* Close affordance now sits as a proper top-right control on the
   gradient banner. The previous "faint white pill behind a tiny X"
   read as floating debris over the banner art - replaced with a
   28x28 dark-translucent disc that has visible contrast against the
   gradient AND against a future light-banner variant. */
.collab-profile-close {
    position: absolute; top: 16px; right: 16px;
    z-index: 3;
    width: 28px; height: 28px;
    padding: 0;
    border-radius: 50%;
    background: rgba(15, 23, 42, .35);
    border: 0;
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    /* Bootstrap btn-close ships its own SVG via background-image. We
       swap to a white-tinted variant so it reads on the dark backdrop. */
    background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23ffffff'%3E%3Cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414Z'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: center;
    background-size: 12px 12px;
    opacity: 1;
    transition: background-color .15s ease, transform .08s ease;
}
.collab-profile-close:hover { background-color: rgba(15, 23, 42, .55); transform: scale(1.08); }
.collab-profile-close:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.collab-profile-banner {
    /* Per-user deterministic gradient. JS sets --profile-banner-h1
       and --profile-banner-h2 from a hash of the display name so each
       person's card has its own hue. The defaults below are a sensible
       fallback for the loading / error states where JS hasn't run yet. */
    --profile-banner-h1: 260deg;
    --profile-banner-h2: 320deg;
    height: 108px;
    background:
        radial-gradient(circle at 20% 0%, rgba(255,255,255,.18) 0%, transparent 55%),
        radial-gradient(circle at 80% 100%, rgba(255,255,255,.14) 0%, transparent 50%),
        linear-gradient(135deg,
            hsl(var(--profile-banner-h1), 70%, 58%),
            hsl(var(--profile-banner-h2), 72%, 60%));
    position: relative;
}
/* Birthday ribbon overlaying the banner. Sits at the top center
   above the avatar, calls attention with a 🎂 + warm copy. The
   ribbon's pill shape and gold gradient cuts through the banner
   without needing to compete with it. */
.collab-profile-birthday {
    position: absolute;
    top: 14px; left: 50%;
    transform: translateX(-50%);
    z-index: 2;
    display: inline-flex; align-items: center; gap: .4rem;
    padding: .3rem .85rem;
    border-radius: 999px;
    background: linear-gradient(135deg, var(--warning), #f97316);
    color: #fff;
    font-size: 11px; font-weight: 700;
    letter-spacing: -.005em;
    box-shadow:
        0 1px 2px rgba(15, 23, 42, .15),
        0 6px 18px -4px rgba(249, 115, 22, .55);
    animation: profileBirthdayPop .35s ease-out both;
}
.collab-profile-birthday-emoji { font-size: 13px; line-height: 1; }
@keyframes profileBirthdayPop {
    from { opacity: 0; transform: translate(-50%, -8px) scale(.92); }
    to   { opacity: 1; transform: translate(-50%, 0)    scale(1); }
}
/* Subtle dot grid layered over the gradient for visual texture - keeps
   the banner from feeling like a flat blob without competing with the
   avatar/name below. */
.collab-profile-banner::after {
    content: "";
    position: absolute; inset: 0;
    background-image: radial-gradient(rgba(255,255,255,.18) 1px, transparent 1.2px);
    background-size: 14px 14px;
    opacity: .5;
    mix-blend-mode: overlay;
    pointer-events: none;
}
.collab-profile-body {
    padding: 0 1.75rem 1.75rem;
    text-align: center;
    margin-top: -52px;
}
.collab-profile-avatar {
    width: 104px; height: 104px; border-radius: 50%;
    background: var(--avatar-bg);
    color: var(--avatar-color);
    display: inline-flex; align-items: center; justify-content: center;
    font-weight: 700; font-size: 2.5rem;
    box-shadow:
        0 0 0 5px var(--surface),
        0 0 0 6px color-mix(in srgb, var(--primary) 18%, transparent),
        0 14px 28px rgba(15, 23, 42, .22);
    margin: 0 auto 1rem;
    overflow: hidden;
    position: relative;
}
.collab-profile-avatar img {
    width: 100%; height: 100%; object-fit: cover; border-radius: 50%;
}
/* Avatar wrapper hosts the online dot indicator. The avatar itself
   keeps its existing inline-flex centering so the letter glyph
   (e.g. "L") stays optically centered inside the circle; the
   wrapper acts only as a positioning context for the dot.
   Earlier this override forced display: block on the avatar which
   broke the align-items/justify-content centering set up at the
   base rule, leaving the letter pinned to the top-left of the box. */
.collab-profile-avatar-wrap {
    position: relative;
    display: inline-block;
    margin: 0 auto 1rem;
}
.collab-profile-avatar-wrap .collab-profile-avatar { margin: 0; }
/* ── Admin identity card ───────────────────────────────────────
   The "Admin" actor's card: the org's automation identity, not a person.
   Reuses the profile-card shell with an admin-specific banner hue and a
   shield glyph (var(--primary)) on a neutral disc in place of a photo. */
.bf-admin-card .collab-profile-banner {
    --profile-banner-h1: 222deg;
    --profile-banner-h2: 264deg;
}
/* The card badge IS the canonical BfIcon bubble (via Html.AdminAvatar), sized up
   to the profile-card avatar footprint with the same white ring + drop shadow, so
   a custom icon / uploaded photo / default shield all paint through one renderer
   and match the feed. Size + radius drive the glyph; the bubble paints itself. */
.bf-admin-card-badge {
    --bf-icon-size: 104px;
    --bf-icon-radius: 50%;
    margin: 0 auto 1rem;
    box-shadow:
        0 0 0 5px var(--surface),
        0 0 0 6px color-mix(in srgb, var(--primary) 18%, transparent),
        0 14px 28px rgba(15, 23, 42, .22);
}
.bf-admin-card-rule {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    max-width: 100%;
    padding: .4rem .7rem;
    border: 1px solid var(--border);
    border-radius: 10px;
    color: var(--text);
    font-weight: 600;
    text-decoration: none;
    transition: color .12s ease, border-color .12s ease;
}
.bf-admin-card-rule:hover {
    color: var(--text);
    border-color: color-mix(in srgb, var(--text) 25%, transparent));
}
.bf-admin-card-rule-glyph { flex: 0 0 auto; }
.bf-admin-card-rule-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bf-admin-card-rule-arrow { flex: 0 0 auto; opacity: .55; }
.collab-profile-online-dot {
    position: absolute;
    right: 6px; bottom: 6px;
    width: 16px; height: 16px;
    border-radius: 50%;
    background: #22c55e;
    box-shadow:
        0 0 0 3px var(--surface),
        0 0 0 4px color-mix(in srgb, #22c55e 32%, transparent);
}

/* ── Cookie counter pill + send button (profile card) ─────────────
   The cluster sits at the bottom of the card body, BELOW bio / joke,
   as the "appreciation" footer: one row with the count chip and the
   send button. The .profile-cookie-msg sits underneath as a second
   line. The cluster is centered to match the card's overall vertical
   rhythm, and wraps on narrow widths so neither child clips. */
.profile-cookie-count {
    display: inline-flex; align-items: center; gap: .35rem;
    padding: .25rem .7rem;
    margin: 0;
    background: linear-gradient(135deg, color-mix(in srgb, #f59e0b 22%, var(--surface-2)), color-mix(in srgb, #ec4899 18%, var(--surface-2)));
    border: 1px solid color-mix(in srgb, #f59e0b 28%, var(--border));
    border-radius: 999px;
    font-size: 11px;
    font-weight: 700;
    color: #92400e;
    line-height: 1.1;
}
.profile-cookie-emoji { font-size: 13px; line-height: 1; }
.profile-cookie-num { font-weight: 800; }
.profile-cookie-label { font-weight: 600; opacity: .85; }
html[data-theme="dark"] .profile-cookie-count {
    background: linear-gradient(135deg, color-mix(in srgb, #f59e0b 22%, var(--surface)), color-mix(in srgb, #ec4899 18%, var(--surface)));
    color: var(--warning);
}

.profile-cookie-actions {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: .65rem;
    /* Generous top margin + a hairline separator so the appreciation
       footer feels like its own beat, not glued to the bio/joke above
       it or the link chips below. The hairline is a thin centered
       divider that doesn't span the full width - softer than a
       hard <hr>. */
    margin: 1.25rem 0 1.25rem;
    padding-top: 1.1rem;
    position: relative;
}
.profile-cookie-actions::before {
    content: "";
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 60%;
    height: 1px;
    background: var(--border);
    opacity: .7;
}
/* The inline status / error message takes its own row below the
   chips so it never squeezes the count or button. */
.profile-cookie-actions .profile-cookie-msg { flex: 1 0 100%; margin-top: .25rem; }
.profile-cookie-btn {
    display: inline-flex; align-items: center; gap: .5rem;
    padding: .55rem 1.1rem;
    background: linear-gradient(135deg, #f59e0b, #ec4899);
    color: #fff;
    border: 0;
    border-radius: 999px;
    cursor: pointer;
    font-family: inherit;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: -.005em;
    box-shadow:
        0 1px 2px rgba(15, 23, 42, .08),
        0 4px 12px -4px color-mix(in srgb, #f59e0b 55%, transparent);
    transition: transform .12s ease, box-shadow .15s ease, filter .15s ease, opacity .15s ease;
}
.profile-cookie-btn:hover {
    transform: translateY(-1px);
    box-shadow:
        0 1px 2px rgba(15, 23, 42, .1),
        0 8px 18px -4px color-mix(in srgb, #f59e0b 65%, transparent);
    filter: brightness(1.04);
}
.profile-cookie-btn:active { transform: translateY(0); filter: brightness(.96); }
.profile-cookie-btn:disabled { cursor: default; opacity: .55; box-shadow: none; transform: none; filter: none; }
.profile-cookie-btn.is-sent {
    background: linear-gradient(135deg, #84cc16, #10b981);
    box-shadow: 0 1px 2px rgba(15, 23, 42, .08), 0 4px 12px -4px color-mix(in srgb, #10b981 55%, transparent);
    opacity: 1;
}
.profile-cookie-btn-emoji { font-size: 15px; line-height: 1; }
.profile-cookie-msg {
    font-size: 11px;
    color: var(--text-muted);
    text-align: center;
    max-width: 280px;
}
.profile-cookie-msg.is-error { color: #b91c1c; }

/* Cookie burst overlay - tiny emoji shower from the avatar on a
   successful send. Each particle gets a per-instance angle + delay
   so the burst looks organic. Removed from the DOM after 1.2s. */
.profile-cookie-burst {
    position: absolute;
    top: 50%; left: 50%;
    font-size: 1.3rem;
    line-height: 1;
    pointer-events: none;
    transform: translate(-50%, -50%) scale(.6);
    opacity: 0;
    animation: cookie-burst .9s ease-out forwards;
    animation-delay: calc(var(--burst-i, 0) * .05s);
}
@keyframes cookie-burst {
    0%   { transform: translate(-50%, -50%) scale(.6); opacity: 0; }
    20%  { opacity: 1; }
    100% {
        transform:
            translate(calc(-50% + cos(calc(var(--burst-i, 0) * 60deg)) * 90px),
                      calc(-50% + sin(calc(var(--burst-i, 0) * 60deg)) * 90px - 30px))
            scale(1) rotate(calc(var(--burst-i, 0) * 60deg));
        opacity: 0;
    }
}

.collab-profile-name {
    margin: 0 0 .25rem;
    font-size: 1.4rem; font-weight: 700; color: var(--text);
    letter-spacing: -.02em;
    overflow-wrap: anywhere;
}
/* One-line "ROLE · LOCATION" sub-meta under the name. The role gets
   a soft pill chip (it's job-title context, valuable enough to
   isolate); location stays as a faded suffix. The "·" only appears
   when both halves are present. */
.collab-profile-submeta {
    font-size: 12px; color: var(--text);
    margin: 0 0 .4rem;
    display: inline-flex; align-items: center; gap: .5rem;
    flex-wrap: wrap; justify-content: center;
}
.collab-profile-role-chip {
    display: inline-flex; align-items: center;
    padding: .15rem .65rem;
    background: var(--primary-bg);
    color: var(--primary);
    border-radius: 999px;
    font-size: 10px; font-weight: 700;
    letter-spacing: .015em;
    text-transform: uppercase;
    line-height: 1.4;
}
.collab-profile-location { color: var(--text-muted); }
.collab-profile-submeta-sep { color: var(--text-subtle); }
.collab-profile-tz-sep { color: var(--text-subtle); }
.collab-profile-tz {
    display: inline-flex; align-items: center; gap: 0.3rem;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}
.collab-profile-tz-icon { opacity: 0.7; flex: 0 0 auto; }
.collab-profile-tz-label {
    color: color-mix(in srgb, var(--text-muted) 80%, transparent);
}
.collab-profile-email {
    font-size: 12px; color: var(--text-muted);
    word-break: break-all;
    margin-bottom: 1rem;
}

/* ── Quick actions row (Mention · Email) ──────────────────────────
   In-app interactions, distinct from the external link chips below.
   Centered cluster of small text + icon buttons; same shape as the
   .eml/Trash buttons in the message header so the visual language
   is consistent across the app. */
.collab-profile-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: .5rem;
    margin: 0 0 1.25rem;
}
.collab-profile-action {
    display: inline-flex; align-items: center; gap: .4rem;
    padding: .35rem .8rem;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 999px;
    color: var(--text-muted);
    font-family: inherit;
    font-size: 11px; font-weight: 600;
    text-decoration: none;
    cursor: pointer;
    transition: background .12s ease, color .12s ease, border-color .12s ease, transform .08s ease;
}
.collab-profile-action:hover {
    background: color-mix(in srgb, var(--text) 5%, transparent);
    border-color: color-mix(in srgb, var(--text) 25%, transparent));
    color: var(--text);
    transform: translateY(-1px);
}
.collab-profile-action svg { flex: 0 0 auto; }

/* ── Section blocks (About / For fun / Connect / Appreciation) ────
   Each section is a wrapper that pairs an uppercase eyebrow label
   with a body block. Hidden when the body is empty; the label
   disappears with it. Aligned left so the eyebrow tracks above the
   block it labels. */
.collab-profile-section {
    text-align: left;
    margin: 0 0 1rem;
}
.collab-profile-section-label {
    display: block;
    font-size: 9px;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--text-subtle);
    margin: 0 0 .4rem;
}
/* Sections inside the card body keep their inner blocks margin-free
   so the wrapper's bottom margin governs spacing between sections. */
.collab-profile-section .collab-profile-bio,
.collab-profile-section .collab-profile-joke {
    margin-bottom: 0;
}
/* Appreciation section is centered (count chip + button). */
.collab-profile-section-cookies { text-align: center; }
.collab-profile-section-cookies .collab-profile-section-label { text-align: center; }
.collab-profile-section-cookies .profile-cookie-actions {
    margin: 0;
    padding-top: 0;
}
.collab-profile-section-cookies .profile-cookie-actions::before { display: none; }

/* ── Footer anchor ("Member since X · N workspaces shared") ─────── */
.collab-profile-footer {
    display: inline-flex; align-items: center; gap: .35rem;
    flex-wrap: wrap; justify-content: center;
    margin: .5rem 0 0;
    font-size: 10px;
    color: var(--text-subtle);
}
.collab-profile-footer-sep { opacity: .7; }
/* Bio rendered as a soft quote-card. The left-edge accent line
   reads as a quotation without needing literal " " glyphs. */
.collab-profile-bio {
    text-align: left;
    background: var(--surface-2);
    border-left: 3px solid color-mix(in srgb, var(--primary) 55%, var(--border));
    border-radius: 8px;
    padding: .65rem .85rem;
    margin: 0 0 1.1rem;
    font-size: 12px; line-height: 1.45;
    color: var(--text);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}
/* Favorite joke card - distinct from bio. Warmer accent (amber/yellow)
   so the eye reads it as "personality" rather than "professional bio".
   The little tag at the top earns its emoji by being one of the only
   visual cues on the card. */
.collab-profile-joke {
    text-align: left;
    background: linear-gradient(135deg,
        color-mix(in srgb, #f59e0b 8%, var(--surface-2)),
        var(--surface-2));
    border-left: 3px solid color-mix(in srgb, #f59e0b 70%, var(--border));
    border-radius: 8px;
    padding: .65rem .85rem;
    margin: 0 0 1.1rem;
    font-size: 12px; line-height: 1.45;
    color: var(--text);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    display: flex; flex-direction: column; gap: .25rem;
}
.collab-profile-joke-tag {
    font-size: 10px; font-weight: 700; letter-spacing: .04em;
    text-transform: uppercase;
    color: color-mix(in srgb, #b45309 80%, var(--text));
}
.collab-profile-joke-text { font-style: italic; color: var(--text); }
/* Link chips: LinkedIn / Website. Styled like rounded outline
   buttons so they read as actionable, not just static labels.
   The LinkedIn variant carries brand colors (#0A66C2 fill) so the
   user reads it at a glance; the Website variant stays neutral so
   it doesn't compete. */
.collab-profile-links {
    display: flex; gap: .55rem; flex-wrap: wrap; justify-content: center;
    margin: 0 0 1.1rem;
}
.collab-profile-link {
    display: inline-flex; align-items: center; gap: .45rem;
    padding: .4rem .85rem;
    border-radius: 999px;
    background: var(--surface);
    border: 1px solid var(--border);
    color: var(--text);
    font-size: 12px; font-weight: 600;
    text-decoration: none;
    transition: border-color .12s ease, background .12s ease, color .12s ease, transform .08s ease, box-shadow .12s ease;
    max-width: 100%;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.collab-profile-link:hover {
    transform: translateY(-1px);
    box-shadow: 0 4px 10px -3px rgba(15, 23, 42, .12);
}
.collab-profile-link-icon { flex: 0 0 auto; }

/* LinkedIn variant: branded blue glyph at rest; on hover the chip
   fills with the brand color so the icon flips white for contrast. */
.collab-profile-link.is-linkedin .collab-profile-link-icon { color: #0A66C2; }
.collab-profile-link.is-linkedin:hover {
    background: #0A66C2;
    border-color: #0A66C2;
    color: #fff;
}
.collab-profile-link.is-linkedin:hover .collab-profile-link-icon { color: #fff; }
html[data-theme="dark"] .collab-profile-link.is-linkedin .collab-profile-link-icon { color: #4096e6; }
html[data-theme="dark"] .collab-profile-link.is-linkedin:hover {
    background: #0A66C2;
    border-color: #0A66C2;
}

/* Website variant: neutral globe, lifts to the app primary on hover
   to differentiate it from the LinkedIn-branded chip. */
.collab-profile-link.is-website .collab-profile-link-icon { color: var(--text-muted); }
.collab-profile-link.is-website:hover {
    background: color-mix(in srgb, var(--text) 5%, transparent);
    border-color: color-mix(in srgb, var(--text) 25%, transparent));
    color: var(--text);
}
.collab-profile-link.is-website:hover .collab-profile-link-icon { color: var(--text); }
.collab-profile-meta {
    display: grid; grid-template-columns: 1fr 1fr; gap: .55rem;
    text-align: left;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: .85rem 1rem;
}
.collab-profile-stat {
    display: flex; flex-direction: column; gap: 2px;
    min-width: 0;
}
.collab-profile-stat-label {
    font-size: 9px; font-weight: 700; letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--text-subtle);
}
.collab-profile-stat-value {
    font-size: 13px; color: var(--text); font-weight: 600;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.collab-profile-stat-value.is-owner { color: var(--primary); }

/* Custom-emoji row: the input + "Use this" submit sit on the same line
   so a paste-then-go flow is one tap. */
.icon-custom-row { display: flex; gap: .5rem; align-items: flex-end; }
.icon-custom-row > div { flex: 1 1 auto; }
.icon-custom-row .btn { flex: 0 0 auto; }

/* Upload-your-own row inside the IconPicker. Now lives inside the
   extras row (right column) with no separator of its own - the
   extras row provides the border-top above. The crop canvas + zoom
   slider only render when the user has actually picked a file; in
   the idle state we just show the "Choose image" button + hint. */
.icon-upload-row {
    display: flex; flex-direction: column; gap: .5rem;
    margin: 0;
}
.icon-upload-pick { display: flex; align-items: center; gap: .65rem; flex-wrap: wrap; }
.icon-upload-pick-btn { cursor: pointer; }
.icon-upload-crop {
    position: relative;
    width: 256px; height: 256px;
    margin: .25rem auto;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 12px;
    overflow: hidden;
    cursor: grab;
    touch-action: none;
}
.icon-upload-crop:active { cursor: grabbing; }
.icon-upload-crop canvas { display: block; width: 100%; height: 100%; user-select: none; }
.icon-upload-mask {
    position: absolute; inset: 0;
    pointer-events: none;
    box-shadow: 0 0 0 9999px rgba(15, 23, 42, .25) inset;
    border-radius: 50%;
    border: 2px dashed rgba(255,255,255,.85);
}
.icon-upload-zoom {
    display: flex; align-items: center; gap: .55rem;
    max-width: 320px; margin: 0 auto;
    width: 100%;
}
.icon-upload-zoom-label { font-size: 11px; color: var(--text-muted); flex: 0 0 auto; }
.icon-upload-zoom input[type="range"] { flex: 1 1 auto; }

/* ================== Teams page ==================
   Premium card-list layout. Each team is a self-contained card with
   a tinted header (icon + name + count + actions), an inline list of
   members (avatar + name/email + role + remove on hover), and a
   styled "add a teammate" input at the bottom that uses the shared
   chip-suggest popover visuals.
   ================================================ */
/* Top padding matches .app-page-inner's default 2rem so the page
   title isn't glued to the nav bar. The previous override set
   padding-top to 0 which made the heading collide with the topbar
   on every entry to /Teams. */


/* Page header (used on Teams; reusable for any "list page" that wants
   a stronger affordance than the bare h1 + subtitle pair). The icon
   tile gives the title a visual anchor; the count pill earns its
   place by communicating density at a glance ("how many teams?"). */
.page-head {
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    gap: 1rem;
    padding: .25rem 0 1.25rem;
    margin-bottom: 1.25rem;
    border-bottom: 1px solid var(--border);
}
.page-head-icon {
    flex: 0 0 auto;
    width: 48px; height: 48px;
    display: inline-flex; align-items: center; justify-content: center;
    border-radius: 12px;
    background: linear-gradient(135deg,
        color-mix(in srgb, var(--primary) 18%, var(--surface)),
        color-mix(in srgb, var(--primary) 6%, var(--surface-2)));
    border: 1px solid color-mix(in srgb, var(--primary) 22%, var(--border));
    color: var(--primary);
    box-shadow: 0 6px 16px color-mix(in srgb, var(--primary) 15%, transparent);
}
.page-head-text { min-width: 0; }
.page-head-title {
    margin: 0 0 .15rem;
    font-size: 1.85rem; font-weight: 700;
    color: var(--text);
    letter-spacing: -.025em;
    line-height: 1.15;
    display: inline-flex; align-items: center; gap: .55rem;
}
.page-head-count {
    font-size: 11px; font-weight: 700;
    color: var(--primary);
    background: var(--primary-bg);
    padding: 2px 10px;
    border-radius: 999px;
    border: 1px solid color-mix(in srgb, var(--primary) 25%, var(--border));
    font-variant-numeric: tabular-nums;
    letter-spacing: 0;
    line-height: 1.4;
    vertical-align: middle;
}
.page-head-sub {
    margin: 0;
    font-size: 13px;
    color: var(--text-muted);
    line-height: 1.5;
    max-width: 60ch;
}
.page-head-actions { display: flex; gap: .5rem; align-items: center; }
@media (max-width: 640px) {
    .page-head { grid-template-columns: auto 1fr; }
    .page-head-actions { grid-column: 1 / -1; justify-content: flex-end; }
}
.teams-list { display: flex; flex-direction: column; gap: 1.25rem; }

.team-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    box-shadow: var(--shadow-sm);
    /* `overflow: visible` so the per-team chip-invite-picker's suggest
       popup can extend past the card's bottom edge without being
       clipped. The previous `overflow: hidden` (used to crop the
       header gradient to the rounded corners) cut off the dropdown
       just below the input - the user's "visibility problem in
       Teams". The card's children already use plain surface
       backgrounds, so nothing visually relies on the overflow clip. */
    overflow: visible;
    transition: border-color .18s ease, box-shadow .18s ease, transform .18s ease;
}
.team-card:hover {
    border-color: color-mix(in srgb, var(--text) 25%, transparent));
    box-shadow: 0 12px 28px rgba(15, 23, 42, .07);
    transform: translateY(-1px);
}
/* While the per-card chip-suggest is open the card lifts above its
   siblings so the dropdown isn't covered by the next team-card below.
   focus-within fires while any input/menu inside the card has focus. */
.team-card { position: relative; z-index: 0; }
.team-card:focus-within { z-index: 10; }

/* Team card header reuses the workspace-card pattern: same gradient
   stripe, same icon affordance, same identity-text typography. The
   .team-card-head wrapper just specializes padding/background; every
   inner class (.account-icon-btn, .bf-icon, .account-identity,
   .account-email, .account-sub) is the SAME rule that powers the
   workspace card. One source of truth for "entity card title bar". */
/* Team card header matches the Workspaces card pattern: flat surface
   background, same padding rhythm, border-bottom-only (border lives
   on the outer .team-card). One source of truth across the two
   surfaces - any visual change to the header lands consistently on
   both. */
.team-card-head {
    display: flex; align-items: center; justify-content: space-between;
    gap: 1rem;
    padding: 1rem 1.35rem .85rem;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
}
/* Team name styling identical to workspace card title (.account-email
   is shared - on workspaces it holds the address, on teams it holds
   the team name). Both render as the strong heading. */
.team-card-head .account-email {
    font-size: 14px;
    font-weight: 700;
    letter-spacing: -.01em;
    color: var(--text);
    /* Plain text in a div defaults to the I-beam (text-edit) cursor.
       The team name isn't directly editable here - renaming goes
       through the dedicated Rename button - so the I-beam is
       misleading. Use the arrow cursor so the row reads as a static
       label, not "click to type". */
    cursor: default;
}
/* When the team head's identity-text wraps two lines (name + sub), the
   icon visually dominates because flex-center stacks both lines around
   one icon. Pull the line-height tighter so the text "block" matches
   the icon height more closely - reads as balanced. */
.team-card-head .account-identity-text { line-height: 1.25; gap: 1px; }
/* Member-count pill sitting next to the team name. Uses a real
   `border` (not box-shadow) for its outline because the parent
   .account-email has overflow:hidden for title ellipsis - a
   box-shadow outline gets clipped at the title boundary, leaving
   only half the pill rendered (the user reported this). The parent
   is inline-flex with its own gap so we don't add a margin-left
   here. flex:0 0 auto keeps the pill at its natural width when the
   team name is long. */
.team-card-count {
    font-size: 10px; font-weight: 700; color: var(--primary);
    background: var(--surface);
    padding: 1px 7px; border-radius: 999px;
    border: 1px solid color-mix(in srgb, var(--primary) 25%, var(--border));
    flex: 0 0 auto;
    font-variant-numeric: tabular-nums;
    line-height: 1.4;
}
/* Sub-line under the team name. Same .account-sub baseline so the
   workspace's "Up to date . 2m ago" and the team's "2 members" share
   typography. */
.team-card-sub {
    text-transform: none !important;
    letter-spacing: 0 !important;
    font-weight: 500 !important;
    font-size: 11px !important;
    color: var(--text-muted) !important;
}
.team-card-actions { display: inline-flex; gap: .25rem; flex: 0 0 auto; }

/* The whole card is a click target now (it opens the team drawer). The roster,
   invite picker and management controls moved into that slide-over - see the
   `.team-drawer*` rules in people.css. */
.team-card-head { cursor: pointer; }
.team-card-head-actions {
    display: inline-flex;
    align-items: center;
    gap: .5rem;
    flex: 0 0 auto;
}
/* Workspace collaborator pip avatar (the inline pile rendered on each
   workspace card next to "Open / Manage"). The matching .team-member-avatar
   was dropped when the Teams page migrated to the shared _CollaboratorList
   partial. */
.ws-collab-pip-img {
    border-radius: 50%;
    background: var(--avatar-bg);
    color: var(--avatar-color);
    display: inline-flex; align-items: center; justify-content: center;
    font-weight: 600;
    flex: 0 0 auto;
    box-shadow: 0 0 0 2px var(--surface);
    overflow: hidden;
    width: 34px; height: 34px;
    font-size: 12px;
}
img.ws-collab-pip-img { object-fit: cover; }

/* Add-member input - styled to LOOK like an inviting action even before
   the user clicks. The plain-form-control look read as static text; this
   version has a + icon inside a dashed circle, a subtle dashed wrapper,
   and a saturated focus state so the picker feels like a real call-to-
   action. The popover above is the existing .chip-suggest. */
.team-add-form { margin: 0; }
.team-add-input.chip-input {
    display: flex; align-items: center; gap: .55rem;
    padding: .45rem .7rem;
    background: var(--surface-2);
    border: 1px dashed var(--border-strong);
    border-radius: 8px;
    transition: border-color .15s ease, background .15s ease, box-shadow .15s ease;
    min-height: 48px;
    position: relative;
}
.team-add-input.chip-input:focus-within,
.team-add-input.chip-input.is-focused {
    background: var(--surface);
    border-style: solid;
    border-color: var(--primary);
    box-shadow: 0 0 0 3px var(--primary-bg);
}
.team-add-icon {
    display: inline-flex; align-items: center; justify-content: center;
    width: 30px; height: 30px; border-radius: 50%;
    background: var(--surface);
    border: 1px dashed var(--border-strong);
    color: var(--text-subtle);
    flex: 0 0 auto;
    transition: background .12s ease, color .12s ease, border-color .12s ease, border-style .12s ease;
}
.team-add-input.chip-input:focus-within .team-add-icon,
.team-add-input.chip-input.is-focused .team-add-icon {
    background: var(--text); color: var(--surface, #fff);
    border-color: var(--text); border-style: solid;
}
.team-add-needle.chip-input-entry {
    flex: 1 1 auto;
    background: transparent; border: 0; outline: 0; padding: 0;
    font-size: 13px; color: var(--text);
}
.team-add-needle.chip-input-entry::placeholder { color: var(--text-subtle); }
.team-add-suggest {
    position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 30;
    background: var(--surface); border: 1px solid var(--border);
    border-radius: 10px; box-shadow: 0 12px 32px rgba(15,23,42,.16);
    max-height: 280px; overflow-y: auto;
    display: flex; flex-direction: column; padding: 4px;
}
/* Portaled variant: appended to <body> so .team-card's overflow:hidden
   stops clipping the popup. Coordinates set from JS at open time. */
.team-add-suggest.is-portal {
    position: absolute;
    top: 0; left: 0; right: auto;
    z-index: 1080;
    width: auto;
}
/* Empty-state row inside the picker - distinct from "popup not open"
   and from a populated list. Reads as helpful copy, not a row. */
.chip-suggest-empty {
    padding: .9rem 1rem;
    font-size: 12px;
    color: var(--text-muted);
    text-align: center;
    line-height: 1.4;
}

/* ================== Collaborator strip on workspace card ==================
   Single-line pile + names + Manage. The previous fat chip grid took
   ~120px of vertical space for what is essentially a "who's in here"
   readout - replaced with an overlapping avatar pile (Notion-style) +
   inline names + per-avatar role tooltips. Stays one row even on a
   workspace with eight collaborators (sixth+ collapse into "+N"). */
.ws-collab-strip {
    margin-top: .75rem;
    padding: .7rem .9rem;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 10px;
    display: flex; align-items: center; justify-content: space-between;
    gap: 1rem;
    min-height: 56px;
}
.ws-collab-left {
    display: flex; align-items: center; gap: 1rem;
    min-width: 0; flex: 1 1 auto;
}
.ws-collab-pile { display: inline-flex; flex: 0 0 auto; }

.ws-collab-pip {
    /* Pip is a <button> so users can click to open the profile modal -
       reset the user-agent button chrome so it looks like a circle, not
       a button. Padding/border/font-family unset; only the visual we
       define below is kept. */
    width: 34px; height: 34px; border-radius: 50%;
    padding: 0; border: 0; font-family: inherit;
    display: inline-flex; align-items: center; justify-content: center;
    box-shadow: 0 0 0 2px var(--surface-2);
    background: var(--avatar-bg);
    color: var(--avatar-color);
    font-size: 12px; font-weight: 600;
    overflow: hidden;
    cursor: pointer;
    transition: transform .12s ease, z-index 0s .12s;
    position: relative;
}
.ws-collab-pip:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--surface-2), 0 0 0 4px var(--primary);
}
.ws-collab-pip + .ws-collab-pip { margin-left: -10px; }
.ws-collab-pip:hover { transform: translateY(-2px) scale(1.06); z-index: 2; transition: transform .12s ease, z-index 0s; }
.ws-collab-pip.is-owner { box-shadow: 0 0 0 2px var(--primary); }
/* Admin gets a softer ring than Owner so the role hierarchy reads visually
   left-to-right: solid primary (Owner) > muted indigo (Admin) > unringed
   (Viewer). Both viewers and owners need to be able to identify each
   role at a glance, regardless of which one they hold themselves. */
.ws-collab-pip.is-admin { box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 55%, var(--surface-2)); }
/* If a user is somehow tagged both (shouldn't happen at the data layer
   but defensively): Owner wins, since the Owner ring is more prominent. */
.ws-collab-pip.is-owner.is-admin { box-shadow: 0 0 0 2px var(--primary); }
/* .ws-collab-pip-img is defined in the team-member-avatar shared block
   higher up so the two surfaces never drift in styling. Don't redefine
   the visuals here. */
.ws-collab-pip-more {
    background: var(--surface);
    color: var(--text-subtle);
    border: 1px dashed var(--border-strong);
    font-size: 10px;
    box-shadow: 0 0 0 2px var(--surface-2);
}

/* ----- Workspace card "members" panel -----
   Three role rows stacked vertically: OWNER (bigger avatar, named)
   then ADMINS (named, comma-separated) then USERS (avatar pile +
   count, the full named list lives on /Collaborators). Manage
   button anchors top-right via position:absolute so it never
   shoves the OWNER label off-axis. */
.ws-members {
    position: relative;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: .75rem .85rem;
    /* Two-column grid: fixed-width LABEL column + flexible CONTENT
       column. Every row inherits this grid so avatars across all
       rows land at the EXACT same X position (vertically aligned),
       regardless of which group they belong to. */
    display: grid;
    grid-template-columns: 80px 1fr;
    row-gap: .55rem;
    column-gap: .65rem;
    align-items: center;
}
.ws-members-line {
    /* Each line is a sub-grid contributing to the parent's two
       columns. Using display:contents passes the line's children
       directly through to the parent grid, which is what
       vertically aligns avatars across heterogeneous rows. */
    display: contents;
}
.ws-members-rolelabel {
    /* Lives in column 1 of the parent grid - fixed width, every
       row's label sits in the same vertical strip. */
    grid-column: 1;
    align-self: center;
    font-weight: 700;
    font-size: 10px;
    letter-spacing: .06em;
    color: var(--text-muted);
}
.ws-members-rolelabel.is-owner { color: var(--primary); }
.ws-members-rolelabel.is-admin { color: color-mix(in srgb, var(--primary) 70%, var(--text-muted)); }
.ws-members-people {
    /* Lives in column 2. Avatars start at this column's edge so
       OWNER, ADMIN, and USER rows all share the same X start
       position - the panel reads as a clean two-column grid. */
    grid-column: 2;
    display: inline-flex; align-items: center; flex-wrap: wrap;
    gap: .25rem;
    /* Reserve right padding so the absolute-positioned Manage
       button doesn't overlap the trailing content. */
    padding-right: 90px;
}
.ws-members-pile {
    display: inline-flex; align-items: center;
}
.ws-members-pip {
    /* Pure circle button. No background pill, no name label - the
       previous design dragged a sideways grey rectangle on hover
       which read as a stretched rendering glitch. Identity comes
       from the avatar itself, role from the column-1 label.

       UNIFIED pile grammar (owner, 2026-09-01): every pip wears a
       die-cut 2px surface ring so overlaps read as cut paper, and
       hover is ELEVATION - a gentle lift with a soft shadow. Never
       a hard color ring: hard-edged box-shadow rings alias into
       jagged circles at pip sizes (the old --primary ring turned
       ink with the accent retirement and read as a black aliased
       circle on every pile in the app). */
    background: transparent;
    border: 0;
    padding: 0;
    border-radius: 50%;
    cursor: pointer;
    position: relative;
    z-index: 1;
    box-shadow: 0 0 0 2px var(--surface);
    /* Asymmetric motion so sweeping the pile reads as a WAVE: the
       rest rule times the SETTLE (slow, gentle), the hover rule
       times the rise (quick, responsive). A single fast symmetric
       transition made pip-to-pip handovers read hard and twitchy. */
    transition: transform .32s cubic-bezier(.33, .68, .3, 1), box-shadow .32s cubic-bezier(.33, .68, .3, 1);
    display: inline-flex; align-items: center; justify-content: center;
}
.ws-members-pip + .ws-members-pip { margin-left: -8px; }
.ws-members-pip:hover,
.ws-members-pip:focus-visible {
    transform: translateY(-2px);
    z-index: 2;
    box-shadow: 0 0 0 2px var(--surface), 0 6px 12px -4px rgba(15, 23, 42, .38);
    border-radius: 50%;
    transition-duration: .18s;
}
html[data-theme="dark"] .ws-members-pip:hover,
html[data-theme="dark"] .ws-members-pip:focus-visible {
    box-shadow: 0 0 0 2px var(--surface), 0 6px 12px -4px rgba(0, 0, 0, .65);
}
.ws-members-avatar {
    /* LAYOUT + size only. Fill / hue / weight / color come from
       the canonical `.bf-avatar-letter` rule. Single uniform
       size across every role row so the panel reads as a clean
       grid; visual hierarchy comes from the role-label color,
       not from differently-sized avatars. */
    width: 32px; height: 32px;
    border-radius: 50%;
    object-fit: cover;
    flex: 0 0 auto;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 12px;
}
.ws-members-pip-more {
    /* Matches .ws-members-avatar size so the pile reads as a
       single grid of identically-sized circles, the +N bubble
       sitting flush with the others - a quiet solid-hairline disc
       on the selection wash, wearing the pile's same die-cut ring
       (the dashed border + gray halo read as clutter). */
    width: 32px; height: 32px;
    border-radius: 50%;
    background: var(--surface-2);
    color: var(--text-muted);
    border: 1px solid var(--border);
    font-size: 10px; font-weight: 600;
    box-shadow: 0 0 0 2px var(--surface);
    margin-left: -8px;
    display: inline-flex; align-items: center; justify-content: center;
    /* Full member of the pile family: tucked under the pips at rest
       (z 0 vs their 1, the established overlap), but hover/focus is
       the SAME elevation and takes the top of the stack - a lifted
       circle half-buried under its neighbour is a rejected artifact. */
    position: relative;
    z-index: 0;
    cursor: pointer;
    transition: transform .32s cubic-bezier(.33, .68, .3, 1), box-shadow .32s cubic-bezier(.33, .68, .3, 1);
}
.ws-members-pip-more:hover,
.ws-members-pip-more:focus-visible {
    transform: translateY(-2px);
    z-index: 2;
    box-shadow: 0 0 0 2px var(--surface), 0 6px 12px -4px rgba(15, 23, 42, .38);
    outline: none;
    transition-duration: .18s;
}
html[data-theme="dark"] .ws-members-pip-more:hover,
html[data-theme="dark"] .ws-members-pip-more:focus-visible {
    box-shadow: 0 0 0 2px var(--surface), 0 6px 12px -4px rgba(0, 0, 0, .65);
}
.ws-members-count {
    font-size: 11px;
    color: var(--text-muted);
    margin-left: .25rem;
}
.ws-members-manage {
    position: absolute;
    top: .55rem; right: .65rem;
}
.ws-collab-meta {
    display: flex; flex-direction: column; min-width: 0;
    line-height: 1.35;
    gap: 1px;
}
/* Count is the LEAD now (avatars convey identity). Bigger / bolder so
   the user reads "3 collaborators" first; role breakdown sits below as
   supporting metadata. Listing names was redundant with the pile and
   noisy past 2-3 people. */
.ws-collab-count {
    font-size: 14px;
    font-weight: 600;
    color: var(--text);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    letter-spacing: -.005em;
}
.ws-collab-summary {
    font-size: 11px;
    color: var(--text-muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    display: inline-flex; align-items: center; gap: .35rem; flex-wrap: wrap;
}
.ws-collab-summary > text { /* @text Razor wrapper is a fragment - inline content */ }
.ws-collab-hidden { color: var(--warning); font-weight: 600; }
.ws-collab-manage {
    flex: 0 0 auto;
    display: inline-flex; align-items: center; gap: .3rem;
    color: var(--primary); font-weight: 600;
    padding: .35rem .65rem;
}
.ws-collab-manage:hover { background: color-mix(in srgb, var(--text) 5%, transparent); }

/* Legacy fat-card classes (kept temporarily so older renderings don't
   break before the page is reloaded). They render off-screen size 0
   if any stray markup is still around - effectively dead. */
.account-collab-section {
    margin-top: .75rem;
    padding: .85rem 1rem .9rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
}
.account-collab-head {
    display: flex; align-items: center; justify-content: space-between;
    gap: .75rem;
    margin-bottom: .65rem;
}
.account-collab-title {
    display: flex; align-items: baseline; gap: .55rem;
    flex: 1 1 auto; min-width: 0;
}
.account-collab-icon { color: var(--text-muted); flex: 0 0 auto; transform: translateY(2px); }
.account-collab-heading {
    font-size: 10px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
    color: var(--text-subtle);
    flex: 0 0 auto;
}
.account-collab-summary {
    font-size: 12px; color: var(--text);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    min-width: 0;
}
.account-collab-summary-sub { color: var(--text-muted); font-size: 11px; margin-left: .15rem; }
.account-collab-manage {
    display: inline-flex; align-items: center; gap: .25rem;
    flex: 0 0 auto;
    color: var(--primary); font-weight: 600;
}
.account-collab-manage:hover { color: var(--text); background: color-mix(in srgb, var(--text) 5%, transparent); }

/* Grid of people chips. Auto-fills so a 1-person workspace renders
   compact and a 6-person workspace fills the row evenly. min(220px)
   keeps the chip wide enough for "Loïc Carrère · Owner" without the
   role tag wrapping awkwardly. */
.account-collab-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: .4rem;
}
.collab-chip {
    display: flex; align-items: center; gap: .6rem;
    padding: .45rem .55rem;
    background: var(--surface-2);
    border: 1px solid transparent;
    border-radius: 8px;
    transition: border-color .12s ease, background .12s ease, transform .08s ease;
    min-width: 0;
}
.collab-chip:hover { border-color: var(--border-strong); background: var(--surface); transform: translateY(-1px); }
.collab-chip.is-owner {
    background: linear-gradient(135deg, var(--primary-bg), color-mix(in srgb, var(--primary-bg) 60%, var(--surface)));
    border-color: color-mix(in srgb, var(--primary) 25%, var(--border));
}
.collab-chip-avatar {
    /* LAYOUT + size + chip-specific ring. Fill / hue / weight /
       color come from the canonical `.bf-avatar-letter` rule. */
    width: 32px; height: 32px; border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 11px;
    flex: 0 0 auto;
    box-shadow: 0 0 0 2px var(--surface);
}
.collab-chip.is-owner .collab-chip-avatar { box-shadow: 0 0 0 2px var(--primary); }
img.collab-chip-avatar { object-fit: cover; }
.collab-chip-meta {
    display: flex; flex-direction: column; min-width: 0;
    line-height: 1.2;
}
.collab-chip-name {
    font-size: 12px; font-weight: 600; color: var(--text);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.collab-chip-role {
    font-size: 9px; font-weight: 700; letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--text-subtle);
}
.collab-chip.is-owner .collab-chip-role { color: var(--primary); }

/* Overflow chip: visually consistent with the others but neutral.
   The "+N" sits in the avatar slot; the meta column reads "N more
   collaborators" so a glance picks up the count and the meaning. */
.collab-chip.is-overflow {
    background: var(--surface);
    border: 1px dashed var(--border-strong);
}
.collab-chip-avatar-more {
    background: var(--surface); color: var(--text-subtle);
    border: 1px dashed var(--border-strong);
    box-shadow: none;
    font-variant-numeric: tabular-nums;
}

/* Hidden-from-viewer footer. Subtle, italic-feeling row that explains
   why the count above doesn't match the workspace's actual member
   count. Uses an eye-with-strike icon so the meaning is unambiguous. */
.account-collab-foot {
    margin-top: .6rem;
    padding-top: .55rem;
    border-top: 1px dashed var(--border);
    display: flex; align-items: center; gap: .4rem;
    font-size: 10px; color: var(--text-subtle);
}
.account-collab-foot svg { flex: 0 0 auto; }
/* Ghost "+" tile for the connect-another-workspace row. Earlier
   iteration used a 1px dashed border that read as a mid-tier
   placeholder pattern; replaced with a flat soft-surface tile
   that looks like a deliberate ghost affordance, not an empty
   slot waiting to be filled. The plus glyph sits in subtle text
   colour at rest and lifts to the primary tone on hover. */
.quick-open-icon-ghost {
    background: color-mix(in srgb, var(--surface-2) 70%, transparent);
    color: var(--text-muted);
    border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
    font-weight: 400;
}
.quick-open-add .quick-open-email {
    color: var(--text-muted);
    font-weight: 500;
}
.quick-open-add:hover {
    background: color-mix(in srgb, var(--text) 5%, transparent);
}
.quick-open-add:hover .quick-open-icon-ghost {
    background: color-mix(in srgb, var(--text) 5%, transparent);
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
    color: var(--text);
}
.quick-open-add:hover .quick-open-email { color: var(--text); }
.quick-open-body { flex: 1 1 auto; min-width: 0; }
.quick-open-email { font-weight: 500; color: var(--text); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.quick-open-meta {
    font-size: 11px;
    color: var(--text-subtle);
    margin-top: .1rem;
    display: flex;
    align-items: center;
    gap: .35rem;
    flex-wrap: wrap;
    /* Tabular numerals so workspace count meta lines ("1 234
       messages", "21 pages", "3 open issues") stay column-aligned
       across rows in the Pinned / Recent stack. */
    font-variant-numeric: tabular-nums;
    /* Reserve one line of vertical space even when the count
       hasn't been computed yet (cold start, new workspace, stats
       broadcast not run). Without this, an empty meta div
       collapses to 0 height and the row becomes shorter than its
       neighbours - the visual the user reported on workspaces
       like "Infrastructure Réseau" where MessagesStored is still
       null. The type glyph alone would reserve ~16px but we
       hard-floor to 1.1em (~16-17px at .75rem font-size) so the
       layout is deterministic. */
    min-height: 1.1em;
}
.quick-open-meta .qo-dot { color: var(--border-strong); }

/* ── Per-workspace indicators (right-aligned slot) ───────────
   Anchors to the right edge of the row as a flex 0 0 auto
   sibling of .quick-open-body. Coloured glyphs (heat-coded to
   the urgency signal) + bold tabular counts. NO new line - the
   indicator line stays HORIZONTAL on the right so the row's
   vertical footprint never grows when an indicator appears.
   That's the contract: every pinned / recent row in the stack
   has the same height regardless of whether it has 0, 1, or 2
   indicators surfaced.

   The colour-coded approach borrows from the Waiting widget's
   bucket-dot vocabulary - amber for waiting, primary purple for
   mentions - so the visual signal is consistent across surfaces.

   Each indicator only renders when count > 0; the strip itself
   is omitted when no indicator applies (see partial). */
.quick-open-indicators {
    flex: 0 0 auto;
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: 12px;
    margin-left: 10px;
    /* The chevron sits to the RIGHT of this slot at the row's
       extreme right edge; the gap here keeps the indicator
       count + label from kissing the chevron. */
    margin-right: 6px;
    font-size: 12px;
    line-height: 1.3;
    font-variant-numeric: tabular-nums;
}
.quick-open-indicator {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    color: var(--text-subtle);
}
.quick-open-indicator-glyph {
    flex: 0 0 auto;
    /* Glyph carries the heat colour per indicator kind. */
}
.quick-open-indicator-count {
    font-weight: 700;
    letter-spacing: -.01em;
    /* Count colour matches the glyph - the eye reads the pair
       as one signal at a glance. */
}
.quick-open-indicator-label {
    /* Visually hidden but kept in the DOM for assistive tech.
       The coloured glyph (amber clock / purple @-mention) + the
       tabular count carries the visual signal; the parent's
       title= + aria-label= carries the hover + screen-reader
       explanation. Painting the word "waiting" / "mentions"
       next to every count ate ~50-60px per indicator and forced
       the workspace email (.quick-open-email) to ellipsis-
       truncate in the narrow Pinned / Recent columns - the
       label was the cause of "acmeinfo@ao..." showing as
       truncated when there was no real shortage of room. */
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}
/* Waiting (clock): amber - "this needs your attention" without
   crossing into alarm-red territory. Matches the Waiting
   widget's Aging-bucket hue so the cross-widget signal stays
   coherent ("amber = you have work to do"). */
.quick-open-indicator-waiting .quick-open-indicator-glyph,
.quick-open-indicator-waiting .quick-open-indicator-count {
    color: var(--warning);
}
html[data-theme="dark"] .quick-open-indicator-waiting .quick-open-indicator-glyph,
html[data-theme="dark"] .quick-open-indicator-waiting .quick-open-indicator-count {
    color: var(--warning);
}
/* Clickable indicators (waiting AND mentions): open the workspace
   pre-filtered to exactly the counted items. One affordance rule for
   both - cursor + hover underline + focus ring only, no filled
   background, zero geometry (a badge must not grow when it becomes
   navigable). */
.quick-open-indicator.is-clickable {
    cursor: pointer;
}
.quick-open-indicator.is-clickable:hover .quick-open-indicator-count {
    text-decoration: underline;
    text-underline-offset: 2px;
}
.quick-open-indicator.is-clickable:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
    border-radius: 4px;
}
/* Mentions (@-glyph): primary purple - "someone called your
   name" is a strong personal signal, distinct from the diffuse
   waiting bucket. Uses the same primary hue the mentions chip
   uses across the platform. */
.quick-open-indicator-mentions .quick-open-indicator-glyph,
.quick-open-indicator-mentions .quick-open-indicator-count {
    color: var(--primary);
}
html[data-theme="dark"] .quick-open-indicator-mentions .quick-open-indicator-glyph,
html[data-theme="dark"] .quick-open-indicator-mentions .quick-open-indicator-count {
    color: var(--primary);
}
/* Workspace-type glyph that prefixes the count line. Sized at
   16px with stroke-width 2 in the source SVG (4px ≈ visual
   parity with the 12px count text it sits beside). Premium-
   subtle contrast: pulled one shade darker than the surrounding
   muted-slate text via var(--text-muted), so the glyph reads as
   the structural anchor of the line while the noun ("messages"
   / "pages" / "open issues") sits quietly behind it - the Apple
   stats-line idiom. Never adopts the workspace's accent hue:
   the card already carries the accent stripe + icon bubble, so
   a third coloured element would break the calm. flex-shrink: 0
   keeps the glyph from collapsing when the row wraps; align-
   self centre pins it to the text's optical baseline in the
   flex row. */
.quick-open-meta-glyph {
    flex: 0 0 auto;
    align-self: center;
    width: 16px;
    height: 16px;
    color: var(--text-muted);
}
/* "Counting messages..." italic placeholder rendered next to
   the type glyph when the count snapshot hasn't been computed
   yet. Italic + reduced opacity reads as a transitional state
   ("real value coming") so the user doesn't mistake the missing
   number for "the workspace is empty". Pairs with the `else`
   branch of `Pages/Site/_WorkspaceCountMeta.cshtml`. */
.quick-open-meta-computing {
    font-style: italic;
    opacity: .65;
}
.quick-open-meta .qo-viewer-tag {
    color: var(--primary);
    background: var(--primary-bg);
    border-radius: 4px;
    padding: 0 .35rem;
    font-weight: 600;
    font-size: 10px;
    letter-spacing: .02em;
    text-transform: uppercase;
}

/* The 24h activity pulse pill (.quick-open-pulse + siblings) was
   retired alongside the matching markup in
   _WorkspaceCountMeta.cshtml. The primary "- N today" delta on
   the meta line already carries the activity signal cards need;
   the separate pulse chip broke the single-line layout (only the
   workspace with non-null primary count + non-zero pulse showed
   it, leaving every other card visually shorter) without
   proportional value. */
.quick-open-arrow {
    /* Demoted to a barely-there indicator. The "→" used to read as
       a heavy call-to-action; in an Apple-style row, the chevron
       just hints at "this row is clickable" and only sharpens on
       hover. Smaller box (12px) is set inline on the <svg>; here
       we cap colour at rgba black ~25% and add an opacity stop so
       the row-hover handler upstairs can lift it. */
    color: rgba(15, 23, 42, .25);
    opacity: .5;
    flex: 0 0 auto;
    transition: transform .12s ease, color .12s ease, opacity .12s ease;
}
html[data-theme="dark"] .quick-open-arrow { color: rgba(255, 255, 255, .3); }

/* `.home-card-all` - the "All N →" link in the launcher header
   (and any future section header that wants the same quiet
   view-all affordance). The rest of the legacy home-card /
   home-grid vocabulary retired with the widget dashboard. */
.home-card-all {
    font-size: var(--bf-dash-row-secondary-size, 12px);
    color: var(--bf-dash-row-secondary-color, var(--text-muted));
    text-decoration: none;
    transition: var(--bf-dash-row-transition);
}
.home-card-all:hover { color: var(--text); }
/* No per-widget padding overrides on .quick-open-card any more;
   the row padding tokens (--bf-dash-row-pad-y / -x) apply
   uniformly across every widget that renders workspace rows. */

/* Activity feed inside the home dashboard. Reuses the timestamp / avatar
   conventions from the dedicated /Activity page but rendered tighter. */
.home-activity { list-style: none; padding: 0; margin: 0; }
/* No hairline divider between rows. The previous 1px line read as
   "stitching" once 8-12 same-actor rows stacked - the eye saw the
   pattern of dividers rather than the content. Whitespace + the
   row's hover background (var(--bf-dash-row-bg-hover)) carry the
   row boundary cue; each row's 10px top + 10px bottom padding
   already yields ~20px of vertical breathing between adjacent
   content blocks. Premium = quiet (CONVENTIONS.md). */
.home-activity-row {
    /* Horizontal padding from token --bf-dash-row-pad-x so the
       avatar + text align under the widget title. Hover bg +
       transition both come from tokens so every dashboard row
       across every widget shares one motion + one color.
       `align-items: center` is critical: without it children
       fall back to flex default `stretch`, the fixed-size avatar
       pins to the top of the row, and the right-side timestamp
       text-aligns to the top - the left/right columns visibly
       drift apart at row heights > 56px. */
    display: flex;
    align-items: center;
    gap: var(--bf-dash-row-gap, 12px);
    padding: var(--bf-dash-row-pad-y, 10px) var(--bf-dash-row-pad-x, 16px);
    margin: 0;
    text-decoration: none;
    color: inherit;
    border-radius: var(--bf-dash-radius-row, 0);
    transition: var(--bf-dash-row-transition);
}
.home-activity-row:hover { background: var(--bf-dash-row-bg-hover); }
.home-activity-row:hover .home-activity-line strong { color: var(--text); }
.home-activity-row:focus-visible {
    outline: none;
    box-shadow: var(--bf-dash-focus-ring);
    background: var(--bf-dash-row-bg-active);
}
.home-activity-avatar {
    /* LAYOUT + size only. Fill / hue / weight / color come from
       the canonical `.bf-avatar-letter` rule (user letter bubbles) or,
       for the Admin badge, the `.bf-icon` engine. `--bf-icon-size` lets
       that engine scale its glyph to this 28px slot; it is inert for the
       letter / photo variants, which don't read it. */
    width: 28px; height: 28px; border-radius: 50%;
    --bf-icon-size: 28px;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 11px; flex: 0 0 auto;
}
img.home-activity-avatar { object-fit: cover; }
.home-activity-body { flex: 1 1 auto; min-width: 0; }
.home-activity-line {
    font-size: 12px; color: var(--text); line-height: 1.35;
    /* Single-line truncation. Without white-space:nowrap the
       actor name + verb + workspace wraps onto 2-3 visual lines
       at narrow widths, which exceeds the fixed 56px row height
       and visually stacks on top of the next row. nowrap +
       overflow:hidden + text-overflow:ellipsis is the canonical
       single-line truncation triplet. */
    white-space: nowrap;
    overflow: hidden; text-overflow: ellipsis;
}
.home-activity-line strong {
    font-weight: 600;
    /* Use the canonical dashboard motion timing so every hover
       transition across home widgets feels identical. */
    transition: color var(--bf-dash-duration, 160ms) var(--bf-dash-ease, cubic-bezier(.22, .61, .36, 1));
}
/* Resting-state contrast: keep the verb at full --text so the row reads
   as a live link, not as faded metadata. The "where" stays one notch
   muted to mark it as secondary context (the workspace label), but we
   use --text-muted (not --text-subtle) so it doesn't look disabled. */
.home-activity-verb { color: var(--text); margin: 0 .25rem; }
.home-activity-where { color: var(--text-muted); }
.home-activity-quote {
    /* Quoted referenced content, sized one notch smaller than the
       verb sentence above so the eye reads it as *the thing acted
       on*, not paragraph body. No fill and no box: the quote is a
       hairline left rule + muted text only, so the day-bucket header
       stays the single hard rhythm break in the feed and rows don't
       read as stacked cards. A small top margin opens a touch of air
       between the verb line and the quote. The `strong` subject line
       sits in full text colour so the title stays scannable. */
    font-size: 11px;
    color: var(--text-muted);
    border-left: 2px solid color-mix(in srgb, var(--primary) 18%, var(--border-strong));
    padding: 0 0 0 .55rem;
    margin: .3rem 0 .1rem;
    overflow: hidden;
    display: -webkit-box;
    /* SINGLE line. The activity list is a fixed-height row grid
       (56px cells, see .home-activity-item). A two-line quote pushed a
       quoted row past its cell, so overflow:hidden clipped the second
       line and it read as the next row overlapping this one - most
       visible when an actor filter surfaces many long issue titles
       back to back. One clamped line keeps every row inside its cell;
       the title stays scannable and the deep-link / preview shows the
       full text. */
    -webkit-line-clamp: 1;
    line-clamp: 1;
    -webkit-box-orient: vertical;
    /* word-break lets a long unbroken token wrap so -webkit-line-clamp
       can engage on it instead of overflowing horizontally. */
    word-break: break-word;
    line-height: 1.4;
}
.home-activity-quote strong {
    color: var(--text);
    font-weight: 600;
}
.home-activity-time {
    font-size: 10px;
    color: var(--text-muted);
    /* Tabular numerals so "3h" / "10d" / "1m" stay column-aligned
       across rows; otherwise proportional digits left-shift the
       label every other row and the column reads as jagged. */
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    /* Right-aligned fixed-width column so every row's timestamp
       sits at exactly the same X coordinate. Mirrors the
       `.home-waiting-time` posture so Waiting + Activity widgets
       share the same row rhythm. The vertical centering is
       handled by `align-items: center` on the parent row - no
       margin-top kludge needed. */
    text-align: right;
    min-width: 44px;
    flex: 0 0 auto;
}
.home-activity-empty { padding: 1rem .25rem; }

/* ── Home dashboard "For you" notification card ─────────────────────
   Surfaces the unread set the bell badge counts (assignments to me,
   cookies received) so the user always knows what triggered the dot
   AND can act on each item without hunting. "Mark all as read" clears
   the bell counter; clicking an assignment row deep-links to the
   thread which auto-clears it via SeenByAssigneeUtc on the message
   page. */
.home-notif-card {
    margin: 0 0 1.25rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    overflow: hidden;
    box-shadow: 0 1px 2px rgba(15, 23, 42, .04);
    transition: opacity .2s ease, transform .2s ease;
}
.home-notif-card.is-clearing { opacity: 0; transform: translateY(-4px); }
.home-notif-head {
    display: flex; align-items: center; justify-content: space-between;
    padding: .65rem .9rem;
    background: linear-gradient(135deg, color-mix(in srgb, var(--primary) 8%, var(--surface-2)), var(--surface-2));
    border-bottom: 1px solid var(--border);
}
.home-notif-title {
    display: inline-flex; align-items: center; gap: .45rem;
    font-size: 12px;
    font-weight: 700;
    color: var(--text);
    letter-spacing: -.005em;
}
.home-notif-title svg { color: var(--primary); }
.home-notif-count {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 20px; height: 20px;
    padding: 0 .45rem;
    background: transparent;
    color: var(--primary);
    border-radius: 999px;
    font-size: 10px;
    font-weight: 700;
    margin-left: .15rem;
}
.home-notif-clear {
    background: transparent;
    border: 0;
    color: var(--text-muted);
    font-size: 11px;
    font-weight: 600;
    cursor: pointer;
    padding: .25rem .55rem;
    border-radius: 6px;
    transition: background .12s ease, color .12s ease;
}
.home-notif-clear:hover { background: var(--surface); color: var(--text); }
.home-notif-clear:disabled { opacity: .55; cursor: default; }
.home-notif-body {
    display: flex;
    flex-direction: column;
}
.home-notif-item {
    display: flex; align-items: center; gap: .65rem;
    padding: .65rem .9rem;
    border-top: 1px solid var(--surface-2);
    text-decoration: none;
    color: inherit;
    transition: background .1s ease;
}
.home-notif-item:first-child { border-top: 0; }
.home-notif-item:hover { background: var(--surface-2); }
.home-notif-icon {
    width: 28px; height: 28px;
    display: inline-flex; align-items: center; justify-content: center;
    border-radius: 8px;
    flex: 0 0 auto;
    font-size: 14px; line-height: 1;
}
.home-notif-icon-assign {
    background: color-mix(in srgb, var(--primary) 12%, var(--surface-2));
    color: var(--primary);
}
.home-notif-icon-cookie {
    background: linear-gradient(135deg, color-mix(in srgb, #f59e0b 18%, var(--surface)), color-mix(in srgb, #ec4899 14%, var(--surface)));
}
.home-notif-avatar {
    /* LAYOUT + size only. Fill / hue / weight / color come from
       the canonical `.bf-avatar-letter` rule. */
    width: 26px; height: 26px;
    border-radius: 50%;
    flex: 0 0 auto;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 10px;
}
.home-notif-text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.home-notif-line {
    font-size: 12px;
    color: var(--text);
    line-height: 1.35;
    overflow: hidden; text-overflow: ellipsis;
}
.home-notif-line strong { font-weight: 700; }
.home-notif-line em { font-style: normal; font-weight: 600; color: var(--primary); }
.home-notif-sub {
    font-size: 10px;
    color: var(--text-muted);
}

/* Unread-mention call-out banner. Slim row with icon, copy, stacked
   avatars, and an arrow. Sits between hero and dashboard - the
   strongest visual signal on the page when someone has pinged you. */
.home-mentions-banner {
    display: flex; align-items: center; gap: .9rem;
    padding: .8rem 1rem;
    margin: 0 0 1.25rem;
    background: linear-gradient(135deg, var(--primary-bg), color-mix(in srgb, var(--primary-bg) 70%, var(--surface)));
    border: 1px solid color-mix(in srgb, var(--primary) 25%, var(--border));
    border-radius: 12px;
    text-decoration: none; color: inherit;
    transition: transform .12s ease, border-color .12s ease, box-shadow .12s ease, opacity .3s ease;
}
.home-mentions-banner.is-leaving { opacity: 0; pointer-events: none; }
.home-mentions-banner.is-hidden { display: none !important; }
.home-mentions-banner:hover {
    transform: translateY(-1px);
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
    box-shadow: 0 6px 18px rgba(79, 70, 229, .15);
}
.home-mentions-icon {
    flex: 0 0 auto;
    width: 38px; height: 38px; border-radius: 10px;
    background: var(--text); color: var(--surface, #fff);
    display: inline-flex; align-items: center; justify-content: center;
}
.home-mentions-body { flex: 1 1 auto; min-width: 0; }
.home-mentions-headline { font-weight: 600; color: var(--text); font-size: 13px; line-height: 1.25; }
.home-mentions-sub { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
.home-mentions-avatars {
    display: inline-flex; flex: 0 0 auto;
    /* Right-edge stacking with negative margins for the classic "avatar
       pile" look. The first avatar sits at full opacity; the rest layer
       behind. Cap is 3 avatars (matched in Razor's Take(3)). */
}
.home-mentions-avatar {
    /* LAYOUT + size + stack-ring only. Fill / hue / weight /
       color come from the canonical `.bf-avatar-letter` rule. */
    width: 28px; height: 28px; border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 10px;
    box-shadow: 0 0 0 2px var(--surface);
}
.home-mentions-avatar + .home-mentions-avatar { margin-left: -10px; }
img.home-mentions-avatar { object-fit: cover; }
.home-mentions-arrow { color: var(--primary); flex: 0 0 auto; transition: transform .12s ease; }
.home-mentions-banner:hover .home-mentions-arrow { transform: translateX(2px); }

/* Footer hint */
.home-footer-hint {
    text-align: center; margin-top: 2.5rem;
    color: var(--text-subtle); font-size: 11px;
}
.home-footer-hint a { color: var(--text-muted); }
.home-footer-hint a:hover { color: var(--text); }
.home-footer-hint kbd {
    display: inline-block; padding: 1px 5px; margin: 0 1px;
    background: var(--surface); border: 1px solid var(--border);
    border-radius: 4px; font-size: 10px; color: var(--text-muted);
    font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
}

.section-title { font-size: 14px; font-weight: 700; color: var(--text); margin: 2rem 0 .85rem; }

/* Activity feed (the page itself is the cockpit frame - work.css) */
.page-title { font-size: 1.6rem; font-weight: 700; margin: 0; color: var(--text); letter-spacing: -.02em; }
.page-subtitle { font-size: 12px; color: var(--text-muted); margin-top: .2rem; }
/* Activity filter chips + pagination. Chip row sits directly under the
   page title and wraps on narrow viewports. "is-active" inverts to the
   primary color so the active filter is visually obvious even in
   muted light themes. */
.activity-filters {
    display: flex; flex-wrap: wrap; gap: .4rem;
    margin-bottom: 1.1rem;
    padding: .25rem 0;
}
.activity-filter-chip {
    display: inline-flex; align-items: center; gap: .35rem;
    padding: .35rem .75rem;
    border-radius: 999px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text-muted);
    font-size: 12px; font-weight: 500;
    text-decoration: none;
    transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
}
.activity-filter-chip:hover {
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
    color: var(--text);
}
.activity-filter-chip.is-active {
    background: color-mix(in srgb, var(--text) 7%, transparent);
    border-color: transparent;
    color: var(--text);
}

.activity-pagination {
    display: flex; align-items: center; justify-content: space-between;
    gap: 1rem;
    margin-top: 1.25rem;
    padding: .5rem 0 .25rem;
    border-top: 1px solid var(--border);
    flex-wrap: wrap;
}
.activity-pagination-summary { font-size: 12px; color: var(--text-muted); }
.activity-pagination-controls { display: flex; gap: .4rem; }
.activity-pagination-controls .is-disabled {
    pointer-events: none;
    opacity: .4;
}

.activity-feed { display: flex; flex-direction: column; gap: .4rem; }
.activity-entry {
    display: flex; gap: .75rem; align-items: flex-start;
    padding: .75rem .85rem;
    background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
    text-decoration: none; color: inherit;
    transition: border-color .12s ease, background .12s ease;
}
.activity-entry:hover { border-color: var(--border-strong); background: var(--surface-2); }

/* Orphaned entries - the linked account was removed, so the entry can't
   navigate anywhere and shouldn't invite a click. Muted, no hover lift. */
.activity-entry.is-orphaned {
    cursor: not-allowed;
    opacity: .72;
    border-style: dashed;
    background: var(--surface);
}
.activity-entry.is-orphaned:hover {
    border-color: var(--border);
    background: var(--surface);
}
.activity-entry.is-orphaned .activity-avatar {
    background: var(--border-strong);
    color: var(--text-muted);
}
.activity-orphan-tag {
    display: inline-block;
    padding: .05rem .45rem;
    font-size: 10px;
    font-weight: 500;
    color: var(--text-muted);
    background: var(--surface-2);
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    margin-left: .15rem;
    vertical-align: baseline;
}
html[data-theme="dark"] .activity-entry.is-orphaned .activity-avatar {
    background: #334155;
    color: var(--text-muted);
}
.activity-avatar {
    /* LAYOUT + size only. Fill / hue / weight / color come from
       the canonical `.bf-avatar-letter` rule. */
    width: 30px; height: 30px; border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 12px; flex: 0 0 auto;
}
.activity-body { flex: 1 1 auto; min-width: 0; }
.activity-line { font-size: 12px; color: var(--text-muted); }
.activity-line strong { color: var(--text); font-weight: 600; }
.activity-line em { color: var(--text); font-style: normal; font-weight: 500; }
.activity-reaction-chip {
    display: inline-flex; align-items: center;
    margin-left: .35rem;
    padding: 2px 8px;
    border-radius: 999px;
    background: var(--surface-2);
    border: 1px solid var(--border);
    font-size: 14px; line-height: 1.1;
}
/* Mail-sent rows render the subject + "to" recipient on a
   single line in lieu of a body quote. Slightly tighter than
   the regular quote so the row reads like a tiny mail receipt. */
.activity-quote.activity-mailsent {
    display: flex; flex-wrap: wrap; align-items: baseline;
    gap: .5rem;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: .35rem .65rem;
    font-style: normal;
    color: var(--text);
    quotes: none;
}
.activity-quote.activity-mailsent::before,
.activity-quote.activity-mailsent::after { content: ""; }
.activity-quote.activity-mailsent strong {
    color: var(--text);
    font-weight: 600;
    font-size: 13px;
}
.activity-mailsent-to {
    color: var(--text-muted);
    font-size: 12px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    min-width: 0;
}
.activity-mailsent-to::before {
    content: "to ";
    color: var(--text-subtle);
    margin-right: .15rem;
}

/* Reply / Reply-all / Forward compose panel. Sits below the
   message body, above the thread-next list, and slides in when
   one of the topbar triggers is clicked. To / Cc / Subject
   stack as labelled rows; body is a freely-resizing textarea. */
.msg-compose {
    margin-top: 1.5rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    box-shadow: 0 8px 24px rgba(15, 23, 42, .06);
    overflow: hidden;
}
.msg-compose-head {
    display: flex; align-items: center; justify-content: space-between;
    padding: .75rem 1.1rem;
    border-bottom: 1px solid var(--border);
    /* Neutral head - the composer is a work surface, not a callout;
       accent washes are reserved (design language: no accent-tinted
       chrome). */
    background: var(--surface-2);
}
.msg-compose-title {
    display: flex; align-items: baseline; gap: .65rem;
    min-width: 0;
}
.msg-compose-mode {
    font-weight: 700;
    color: var(--text);
    font-size: 13px;
}
.msg-compose-from {
    font-size: 11px;
    color: var(--text-muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.msg-compose-from strong {
    color: var(--text);
    font-weight: 600;
}
/* Reply-composer "as [teammate]" chip - same shape as
   .mail-compose-card-as on the new-mail composer, scoped under the
   reply-pane prefix. Sits to the right of the From line so the
   workspace identity remains the primary anchor and the teammate
   identity reads as a delegate-of relationship. Pinned by
   MailLocalSenderAttributionTests. */
.msg-compose-as {
    display: inline-flex; align-items: center; gap: .3rem;
    margin-left: .55rem;
    color: var(--text);
    font-weight: 500;
    font-size: 11px;
    overflow: visible;
    white-space: nowrap;
}
.msg-compose-as-sep {
    color: var(--text-muted);
    font-weight: 400;
}
.msg-compose-as-avatar {
    width: 20px !important; height: 20px !important;
    font-size: 10px !important; line-height: 20px !important;
    border-radius: 50% !important;
    flex: 0 0 auto;
}
.msg-compose-as-name {
    color: var(--text);
    font-weight: 500;
    max-width: 10rem;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Reader-side teammate attribution: inline avatar + name where
   the sender label normally sits. Used for outbound messages whose
   Burofolk-side row carries a resolved LocalSentByUserId; the
   workspace identity is already covered by the workspace-icon
   avatar on the left of the row + the <email> next to the name,
   so we don't repeat a separate "Sent by" chip. The avatar is the
   canonical letter-bubble paint via Html.UserAvatar - same hue
   the user gets on every other surface in the app. */
.msg-sender-attrib {
    display: inline-flex; align-items: center; gap: .35rem;
    vertical-align: middle;
    line-height: 1;
}
.msg-sender-attrib-avatar {
    width: 20px !important; height: 20px !important;
    font-size: 10px !important; line-height: 20px !important;
    border-radius: 50% !important;
    flex: 0 0 auto;
    /* The classic .msg-avatar in the same row is 36px+; this
       inline avatar deliberately stays smaller so the eye reads
       "this is the human attribution next to the name" rather
       than competing with the primary avatar on the left. */
}
.msg-sender-attrib .sender-name {
    /* Tighten the gap between avatar + name - default sender-name
       has its own margin tuned for the SVG-less layout. */
    margin-left: 0;
}
.msg-compose-close {
    background: transparent;
    border: 0;
    border-radius: 8px;
    width: 28px; height: 28px;
    display: inline-flex; align-items: center; justify-content: center;
    color: var(--text-muted);
    cursor: pointer;
}
.msg-compose-close:hover { background: var(--surface-2); color: var(--text); }
.msg-compose form { padding: .85rem 1.1rem 1rem; }
/* Inline send-error banner inside the reply compose. Same shape as
   the compose-new card's error banner; matches the danger-bg palette
   so the user reads it as "this attempt failed, fix and retry". */
.msg-compose-error {
    background: var(--danger-bg);
    border: 1px solid color-mix(in srgb, var(--danger) 35%, var(--border));
    color: var(--danger);
    padding: .55rem .75rem;
    border-radius: 8px;
    font-size: 12px;
    line-height: 1.45;
    word-break: break-word;
    margin-bottom: .5rem;
}
.msg-compose-error[hidden] { display: none; }

/* Issue #182 - live collision banner inside the reply compose. Warm
   sibling of the error banner above: the send didn't fail, the
   CONVERSATION MOVED, so it wears the warning palette and keeps a
   real open-in-new-tab door beside the fact. */
.msg-compose-collision {
    display: flex;
    align-items: center;
    gap: .5rem;
    background: var(--warning-bg);
    border: 1px solid color-mix(in srgb, var(--warning) 35%, var(--border));
    color: var(--warning);
    padding: .55rem .75rem;
    border-radius: 8px;
    font-size: 12px;
    line-height: 1.45;
    margin-bottom: .5rem;
}
.msg-compose-collision[hidden] { display: none; }
.msg-compose-collision svg { flex-shrink: 0; }
.msg-compose-collision [data-compose-collision-text] { flex: 1 1 auto; min-width: 0; word-break: break-word; }
.msg-compose-collision-open {
    flex-shrink: 0;
    color: var(--warning);
    font-weight: 600;
    text-decoration: underline;
    text-underline-offset: 2px;
}
.msg-compose-collision-open:hover { color: var(--warning); opacity: .8; }

.msg-compose-row {
    display: grid;
    grid-template-columns: 70px 1fr;
    align-items: center;
    gap: .65rem;
    padding: .35rem 0;
    border-bottom: 1px solid var(--border);
}
.msg-compose-row label {
    color: var(--text-muted);
    font-size: 12px;
    font-weight: 500;
    margin: 0;
}
.msg-compose-row input {
    width: 100%;
    border: 0;
    background: transparent;
    padding: .35rem 0;
    font-size: 13px;
    color: var(--text);
    outline: none;
}
.msg-compose textarea {
    width: 100%;
    min-height: 180px;
    margin-top: .85rem;
    padding: .85rem;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--surface);
    font-size: 13px;
    line-height: 1.55;
    color: var(--text);
    font-family: inherit;
    resize: vertical;
    outline: none;
    transition: border-color .12s ease, box-shadow .12s ease;
}
.msg-compose textarea:focus {
    border-color: var(--primary);
    box-shadow: 0 0 0 3px var(--primary-bg);
}

/* ── Rich-text compose editor (toolbar + contenteditable canvas) ─── */
.compose-editor {
    margin-top: .85rem;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--surface);
    overflow: hidden;
    transition: border-color .12s ease, box-shadow .12s ease;
}
.compose-editor:focus-within {
    border-color: var(--primary);
    box-shadow: 0 0 0 3px var(--primary-bg);
}
.compose-toolbar {
    display: flex;
    /* flex-start keeps the size picker and the pinned AI button level
       with the first row of formatting buttons when the cluster wraps
       to a second line on a narrow composer. */
    align-items: flex-start;
    /* The toolbar itself never wraps - the formatting buttons live in
       .compose-tool-tools, which wraps internally. The size picker
       stays pinned left, the AI pill pinned right; only the buttons in
       between wrap to a new line. Wrapping the buttons (rather than
       scrolling or shrinking them) keeps every icon full-size and
       visible and stops the AI pill being orphaned onto its own line. */
    flex-wrap: nowrap;
    min-width: 0;
    gap: 2px;
    padding: .35rem .45rem;
    background: var(--surface-2);
    border-bottom: 1px solid var(--surface-2);
}
/* Formatting cluster. Fills the middle of the toolbar (pushing the AI
   pill to the right edge) and WRAPS its buttons to a second line when
   the composer is too narrow to lay them all in a row - rather than
   scrolling them out of sight or shrinking them until the icons
   collide. The size picker and AI button are pinned siblings OUTSIDE
   this cluster, so they never wrap or overlap. */
.compose-tool-tools {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 2px;
    row-gap: 4px;
    flex: 1 1 auto;
    min-width: 0;
}
.compose-tool {
    flex-shrink: 0;
    width: 28px; height: 28px;
    display: inline-flex; align-items: center; justify-content: center;
    background: transparent;
    border: 0;
    border-radius: 6px;
    color: var(--text-muted);
    cursor: pointer;
    transition: background .1s ease, color .1s ease;
}
.compose-tool:hover { background: var(--surface); color: var(--text); }
.compose-tool.is-active { background: color-mix(in srgb, var(--text) 7%, transparent); color: var(--text); }
.compose-tool:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 1px;
}
.compose-tool-sep {
    width: 1px;
    height: 18px;
    margin: 0 .25rem;
    background: var(--border);
    flex: 0 0 auto;
}

/* Quoted-thread preview that sits BELOW the reply editable.
   Structurally read-only: the user types into the editable
   above, the quote is shown so they remember what they're
   replying to. The server re-stitches the canonical quote at
   send time. */
.compose-quote {
    /* Generous interior margins so the toggle never touches
       the editor card's border. Top 14px separates from the
       reply editable, bottom 6px gives the card air below. */
    margin: 14px 14px 6px;
}
.compose-quote-toggle {
    /* Clean inline text affordance - NO pill, no badge, no
       border-radius-999. Reads as a "show more" link with a
       leading three-dot icon, not as a chip. */
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 2px 6px 2px 2px;
    background: transparent;
    border: 0;
    color: var(--text-muted);
    font-size: 12px;
    font-weight: 500;
    line-height: 1.2;
    cursor: pointer;
    border-radius: 4px;
    transition: color .15s ease;
}
.compose-quote-toggle:hover {
    color: var(--text);
}
.compose-quote-toggle[aria-expanded="true"] {
    color: var(--text);
}
.compose-quote-toggle:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}
.compose-quote-toggle svg {
    opacity: .65;
    transition: opacity .15s ease;
}
.compose-quote-toggle:hover svg,
.compose-quote-toggle[aria-expanded="true"] svg {
    opacity: 1;
}
.compose-quote-body {
    margin-top: 10px;
    padding: 12px 16px;
    background: var(--surface-2, #f8fafc);
    border-left: 2px solid var(--border, #e2e8f0);
    border-radius: 0 6px 6px 0;
    color: var(--text-muted);
    font-size: 13px;
    line-height: 1.55;
    white-space: pre-wrap;
    user-select: text;
    cursor: default;
    max-height: 260px;
    overflow-y: auto;
}
.compose-tool-with-caret {
    width: auto;
    padding: 0 .35rem;
    gap: 2px;
}
.compose-tool-with-caret .compose-tool-caret { opacity: .6; margin-left: -1px; }
.compose-tool-with-caret[aria-expanded="true"] {
    background: color-mix(in srgb, var(--text) 7%, transparent);
    color: var(--text);
}

/* Generic compose-toolbar dropdown - host for the font-size menu (and
   any future picker the toolbar grows). The button stays in the
   toolbar's flex flow; the menu pops below via absolute positioning
   and is gated by the [hidden] attribute so JS just toggles a single
   property. */
/* Size picker stays pinned at the left of the toolbar, OUTSIDE the
   scrollable .compose-tool-tools cluster - its dropdown menu must
   escape downward, so it can't live inside an overflow scroller. */
.compose-tool-dropdown { position: relative; display: inline-flex; flex: 0 0 auto; }
.compose-tool-menu {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    z-index: 30;
    min-width: 150px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: .25rem;
    box-shadow: 0 12px 28px -8px rgba(15,23,42,.18), 0 2px 6px -1px rgba(15,23,42,.06);
    animation: bulk-fade-in .1s ease;
}
.compose-tool-menu[hidden] { display: none; }
.compose-tool-menu-item {
    display: flex;
    align-items: center;
    gap: .65rem;
    width: 100%;
    padding: .35rem .55rem;
    border: 0;
    background: transparent;
    color: var(--text);
    border-radius: 6px;
    cursor: pointer;
    text-align: left;
    transition: background .08s ease;
    font: inherit;
}
.compose-tool-menu-item:hover { background: color-mix(in srgb, var(--text) 5%, transparent); }
.compose-tool-menu-item.is-active { background: color-mix(in srgb, var(--text) 7%, transparent); color: var(--text); }
.compose-tool-menu-item > span:first-child {
    display: inline-flex; align-items: center; justify-content: center;
    width: 32px; flex: 0 0 auto;
    font-family: inherit;
    color: var(--text);
}
.compose-tool-menu-label {
    flex: 1 1 auto;
    font-size: 12px;
    color: var(--text);
}
.compose-link-panel {
    display: flex;
    align-items: center;
    gap: .35rem;
    padding: .45rem .55rem;
    background: color-mix(in srgb, var(--primary) 6%, var(--surface));
    border-bottom: 1px solid var(--surface-2);
    animation: bulk-fade-in .12s ease;
}
.compose-link-panel[hidden] { display: none; }
.compose-link-panel input[type="url"] {
    flex: 1 1 auto;
    min-width: 0;
    padding: .32rem .55rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--surface);
    color: var(--text);
    font-size: 12px;
    outline: 0;
}
.compose-link-panel input[type="url"]:focus {
    border-color: var(--primary);
    box-shadow: 0 0 0 2px var(--primary-bg);
}
.compose-body {
    min-height: 180px;
    max-height: 480px;
    overflow-y: auto;
    padding: .85rem 1rem;
    font-size: 13px;
    line-height: 1.55;
    color: var(--text);
    outline: 0;
    /* Allow vertical resize via the wrapper's resize hook below; the
       contenteditable itself doesn't take a native resize handle. */
}
.compose-body:empty::before {
    content: attr(data-placeholder);
    color: var(--text-subtle);
    pointer-events: none;
}
.compose-body p:first-child { margin-top: 0; }
.compose-body p:last-child { margin-bottom: 0; }
.compose-body ul, .compose-body ol { padding-left: 1.4rem; margin: .4rem 0; }
.compose-body blockquote {
    margin: .4rem 0;
    padding: .25rem 0 .25rem .85rem;
    border-left: 3px solid var(--border-strong);
    color: var(--text-muted);
}
.compose-body a { color: var(--primary); }
.compose-body a:hover { text-decoration: underline; }

.msg-compose-foot {
    display: flex; align-items: center; justify-content: space-between;
    gap: 1rem;
    margin-top: .85rem;
}
.msg-compose-hint { flex: 1 1 auto; min-width: 0; }
.msg-compose-foot div { flex: 0 0 auto; display: inline-flex; gap: .35rem; }

.msg-compose-validation {
    margin-top: .5rem;
    padding: .45rem .65rem;
    border-radius: 8px;
    background: rgba(220, 38, 38, .1);
    color: #991B1B;
    font-size: 12px;
}
html[data-theme="dark"] .msg-compose-validation {
    background: rgba(248, 113, 113, .15);
    color: #FCA5A5;
}

/* Attachments: a file-pick button + a hint line + a chip list of
   the picked files. The native input is hidden; the styled label
   triggers it. */
.msg-compose-attachments {
    margin-top: .65rem;
    display: flex; flex-wrap: wrap;
    align-items: center; gap: .45rem .85rem;
}
.msg-compose-attach-pick {
    display: inline-flex; align-items: center; gap: .35rem;
    padding: .3rem .65rem;
    border-radius: 999px;
    background: var(--surface-2);
    color: var(--text-muted);
    font-size: 12px; font-weight: 500;
    cursor: pointer;
    border: 1px solid var(--border);
    transition: background .12s ease, color .12s ease;
    margin: 0;
}
.msg-compose-attach-pick:hover { background: var(--surface); color: var(--text); }
.msg-compose-attach-pick input[type="file"] { display: none; }
.msg-compose-attach-hint {
    font-size: 11px;
    color: var(--text-subtle);
}
.msg-compose-attach-list {
    flex-basis: 100%;
    display: flex; flex-wrap: wrap;
    gap: .35rem;
}
.msg-compose-attach-chip {
    display: inline-flex; align-items: center; gap: .35rem;
    padding: .25rem .65rem;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 999px;
    font-size: 11px;
    max-width: 16rem;
}
.msg-compose-attach-name {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    color: var(--text);
}
.msg-compose-attach-size {
    color: var(--text-subtle);
    font-variant-numeric: tabular-nums;
    flex: 0 0 auto;
}
.msg-compose-attach-chip { padding-right: .35rem; }
.msg-compose-attach-remove {
    flex: 0 0 auto;
    display: inline-flex; align-items: center; justify-content: center;
    width: 18px; height: 18px;
    padding: 0;
    margin-left: .15rem;
    background: transparent;
    border: 0;
    border-radius: 50%;
    color: var(--text-subtle);
    cursor: pointer !important;
    transition: background .12s ease, color .12s ease;
}
.msg-compose-attach-remove:hover {
    background: rgba(220, 38, 38, .12);
    color: #B91C1C;
}
.msg-compose-attach-remove svg { display: block; }

/* Forward-only "Include original attachments" block. Lists the
   parent message's files (read-only chips) with a checkbox - on by
   default - that decides whether they ride along with the forward.
   Hidden by the compose JS for reply / reply-all. */
.msg-compose-forward-attach {
    margin-top: .65rem;
    padding-top: .65rem;
    border-top: 1px dashed var(--border);
    display: flex; flex-direction: column; gap: .5rem;
}
.msg-compose-forward-attach-toggle {
    display: inline-flex; align-items: center; gap: .45rem;
    align-self: flex-start;
    margin: 0;
    font-size: 12px; font-weight: 500;
    color: var(--text);
    cursor: pointer;
}
.msg-compose-forward-attach-toggle input[type="checkbox"] {
    width: 15px; height: 15px;
    margin: 0;
    accent-color: var(--primary);
    cursor: pointer;
}
.msg-compose-forward-attach-label { display: inline-flex; align-items: center; gap: .4rem; }
.msg-compose-forward-attach-count {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 1.1rem; height: 1.1rem; padding: 0 .3rem;
    border-radius: 999px;
    background: var(--surface-2);
    border: 1px solid var(--border);
    font-size: 10px; font-weight: 600;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}
.msg-compose-forward-attach-list {
    display: flex; flex-wrap: wrap; gap: .35rem;
    transition: opacity .12s ease;
}
.msg-compose-forward-attach-chip { cursor: default; }
.msg-compose-forward-attach-icon {
    flex: 0 0 auto;
    display: inline-flex; align-items: center;
    font-size: 13px;
}
/* Unchecked: the originals won't be forwarded - dim + strike the
   list so the preview matches the outcome. */
.msg-compose-forward-attach.is-excluded .msg-compose-forward-attach-list {
    opacity: .4;
}
.msg-compose-forward-attach.is-excluded .msg-compose-attach-name {
    text-decoration: line-through;
    text-decoration-color: var(--text-subtle);
}

/* Drop-zone visual when a file is being dragged over the compose
   panel. Pseudo-element overlay rather than a background swap so
   the original form chrome stays visible underneath - the user
   sees "I'm about to drop on THIS panel" without losing track of
   their work-in-progress draft. */
.msg-compose { position: relative; }
.msg-compose::after {
    content: "Drop files to attach";
    position: absolute; inset: 6px;
    display: flex; align-items: center; justify-content: center;
    border: 2px dashed var(--primary);
    border-radius: 12px;
    background: color-mix(in srgb, var(--primary) 8%, var(--surface));
    color: var(--primary);
    font-weight: 700;
    font-size: 14px;
    pointer-events: none;
    opacity: 0;
    transition: opacity .15s ease;
    z-index: 5;
}
.msg-compose.is-drag-over::after { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
    .msg-compose::after { transition: none; }
}
.activity-quote {
    font-size: 12px; color: var(--text); margin-top: .3rem;
    border-left: 2px solid var(--primary); padding-left: .65rem;
    overflow: hidden; text-overflow: ellipsis; display: -webkit-box;
    -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.activity-time { font-size: 10px; color: var(--text-subtle); margin-top: .25rem; }
/* Account card - stacked layout: header row (identity + actions always on
   the same baseline) and body row (status + progress + metrics). Premium
   finish: subtle divider between header and body, tighter rhythm, brighter
   accents, crisper typography. */
.account-row {
    display: flex;
    flex-direction: column;
    gap: 0;
    padding: 0;
    overflow: hidden;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    box-shadow: var(--shadow-sm);
}
/* No hover effect: the card itself isn't clickable - the actions in the
   header (Open / Pause / Remove) are what the user interacts with. A hover
   tint made the whole card look actionable, which was misleading. */
.account-row.flash {
    /* Long, double-pulse animation so the user can't miss
       which workspace just landed on the page after Connect-
       mailbox. The previous 1.8s single-pulse was easy to
       skip past if the page was scrolled or the user blinked
       at the wrong moment. */
    animation: row-flash 3s ease-out 1;
    /* Outline ring stays for the full duration as a
       belt-and-braces signal the row is the focused one,
       even after the inner pulse fades. */
    outline: 2px solid var(--primary);
    outline-offset: 2px;
    border-radius: 12px;
    transition: outline-color .3s ease 2.6s, outline-offset .3s ease 2.6s;
}
@keyframes row-flash {
    0%   { background: color-mix(in srgb, var(--primary) 22%, transparent); box-shadow: inset 4px 0 0 var(--primary); }
    25%  { background: color-mix(in srgb, var(--primary) 14%, transparent); box-shadow: inset 4px 0 0 var(--primary); }
    50%  { background: color-mix(in srgb, var(--primary) 22%, transparent); box-shadow: inset 4px 0 0 var(--primary); }
    75%  { background: color-mix(in srgb, var(--primary) 12%, transparent); box-shadow: inset 4px 0 0 var(--primary); }
    100% { background: transparent; box-shadow: inset 4px 0 0 transparent; }
}
/* Legacy account-email/sub/status/metrics rules removed - superseded by the
   premium layout further down (.account-row-header / .account-row-body
   typography and the stat-card metrics grid). */

/* Whole row is display-only UI - no I-beam cursor, no accidental text selection. */
.account-row,
.account-row * {
    cursor: default;
    user-select: none;
}
/* Keep interactive controls pointer-y. */
/* Interactive elements AND all their descendants show the hand cursor.
   Without the `... *` half of each pair, hovering the SVG icon or the label
   span inside a button fell through to the `.account-row *` default-cursor
   rule and produced a flicker between pointer and arrow as the mouse moved
   across the button. */
.account-row a,        .account-row a *,
.account-row button,   .account-row button *,
.account-row [role="button"], .account-row [role="button"] *,
.account-row .btn,     .account-row .btn * {
    cursor: pointer !important;
}
/* Even while an action button is mid-submit (we disable it briefly to show
   the spinner) the hand cursor should stay. Bootstrap's default `disabled`
   style flips to `not-allowed` which flickers on every live-progress tick
   that re-renders the button. */
.account-row .btn:disabled,     .account-row .btn:disabled *,
.account-row button:disabled,   .account-row button:disabled * {
    cursor: pointer !important;
    pointer-events: auto;
}
.account-row input,
.account-row textarea {
    cursor: text;
    user-select: text;
}

/* Gap / failure warning block on the account row */
.account-gap {
    margin-top: .55rem;
    padding: .55rem .7rem;
    background: var(--warning-bg);
    border: 1px solid rgba(245, 158, 11, .25);
    border-radius: 8px;
    font-size: 11px;
}
.account-gap-head { display: flex; align-items: center; gap: .35rem; margin-bottom: .15rem; }
.account-gap-icon { color: #f59e0b; font-weight: 700; }
.account-gap-head strong { color: var(--text); font-weight: 600; }
.account-gap-body { color: var(--text-muted); font-size: 10px; line-height: 1.4; }
.account-gap-actions { display: flex; gap: .35rem; margin-top: .5rem; flex-wrap: wrap; }
.account-gap-actions .btn { font-size: 11px; padding: .25rem .6rem; }
html[data-theme="dark"] .account-gap { background: rgba(251, 191, 36, .08); border-color: rgba(251, 191, 36, .35); }
/* Header - identity on the left, actions on the right. Flat background
   (no gradient) - the gradient was distracting and didn't add any info. */
.account-row-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 1rem 1.35rem .85rem;
    border-bottom: 1px solid var(--border);
    background: var(--surface);
}
.account-identity {
    min-width: 0;
    flex: 1 1 auto;
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: .8rem;
}
/* Inner column wraps the title + email + state stack so the icon stays
   inline-left and the text reads naturally beside it. */
.account-identity-text {
    min-width: 0;
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    gap: .15rem;
}

/* Body - premium dashboard layout: progress-row (status+bar+%) then a
   stat-card grid of metrics, then optional live-subject and gap warning. */
.account-row-body {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: .85rem;
    padding: 1rem 1.35rem 1.1rem;
}

/* Progress row - bar spans the full body width, percentage to the right.
   Status message now lives on its OWN line above (see .account-status),
   so the bar doesn't get squished into a narrow 140-260px slot any more. */
.account-progress-row {
    display: flex;
    align-items: center;
    gap: .75rem;
}
.account-progress-row .progress.account-progress {
    flex: 1 1 auto;
    height: 8px;
    background: var(--surface-2);
    border-radius: 999px;
    overflow: hidden;
}
.account-progress-row .account-progress .progress-bar {
    height: 100%;
    background: var(--primary);
    border-radius: 999px;
    transition: width .35s ease;
}
.account-pct {
    font-size: 11px;
    font-weight: 700;
    color: var(--text-muted);
    letter-spacing: -.01em;
    flex: 0 0 auto;
    min-width: 2.4rem;
    text-align: right;
    font-variant-numeric: tabular-nums;
}

/* Metrics grid - each metric is a stat card with label above value.
   Fixed equal columns (instead of auto-fit) so the grid does NOT
   recompute column count when individual metrics toggle visibility
   during a live sync. With auto-fit, hiding one metric shifted the
   width of every other metric every second - the dominant source
   of cumulative layout shift on the workspaces page during import. */
.account-metrics {
    display: grid;
    grid-template-columns: repeat(6, minmax(0, 1fr));
    gap: .3rem .8rem;
    margin: 0;
    padding: .55rem .75rem;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 10px;
}
@media (max-width: 1100px) {
    /* Narrower viewports collapse to fewer columns. Discrete
       breakpoints (not auto-fit) so column count is deterministic. */
    .account-metrics { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 640px) {
    .account-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* Reserved-space hide. Polling toggles transient metrics (ETA, Rate,
   This Batch, Elapsed, Live subject) on/off depending on whether
   sync is active. Using `display: none` removed them from the layout
   flow, which (with the auto-fit grid above) shifted every other
   metric. With this class the cell stays reserved but its contents
   are invisible - zero layout shift on toggle. */
.is-poll-hidden {
    visibility: hidden;
}

/* Live-subject row reserves vertical space whether or not its
   text is rendered, so toggling visibility during sync doesn't
   bump the cards below it up and down. */
.account-live-subject {
    min-height: 1.4em;
}
.account-metric {
    display: flex;
    flex-direction: column;
    gap: .1rem;
    min-width: 0;
    overflow: hidden;
}
.account-metric .metric-label {
    font-size: 9px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .08em;
    color: var(--text-muted);
    white-space: nowrap;
}
.account-metric .metric-value {
    font-size: 12px;
    color: var(--text);
    font-weight: 500;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.account-metric .metric-value strong,
.account-metric .metric-unit {
    /* tabular-nums propagates to nested numeric children so a
       digit-width drift (1,234 → 12,345) on the polling tick
       can't reflow the metric and cascade through the row's
       grid - the per-second number updates were the dominant
       source of cumulative layout shift on the workspaces page. */
    font-variant-numeric: tabular-nums;
}

/* The value strong is the ACTUAL hot-spot for CLS - poll updates
   write to its textContent every second when sync is active. Reset
   to its original styling block. */
.account-metric .metric-value strong {
    color: var(--text);
    font-weight: 700;
}
.account-metric .metric-sep {
    color: var(--text-subtle);
    margin: 0 .1rem;
}
.account-metric .metric-unit {
    color: var(--text-muted);
    font-weight: 500;
    margin-left: .2rem;
    font-size: 11px;
}

/* Mobile: tighter metric grid. */
@media (max-width: 720px) {
    .account-metrics {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

/* Refined typography in the identity block - bigger email, subtle state. */
.account-email {
    font-weight: 600;
    color: var(--text);
    font-size: 14px;
    letter-spacing: -.005em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    display: inline-flex;
    align-items: center;
    gap: .35rem;
}
.account-email-sub {
    font-size: 11px;
    font-weight: 400;
    color: var(--text-muted);
    text-transform: none;
    letter-spacing: 0;
    margin-top: .1rem;
    /* Don't shout email addresses at the user. The legacy all-caps
       treatment came from .account-sub before this override - left
       in place to belt-and-suspender the cascade. */
    font-variant-caps: normal;
}
/* State sub-line: instead of an all-caps yelled label like "IDLE",
   render as a subtle pill. Reads as data, not as a heading.
   tabular-nums + min-height keeps the pill stable across the
   per-second polling tick: status text changes ("Scanning... " →
   "Downloading 1,234 of 5,678...") would otherwise reflow the
   surrounding card and cause cumulative layout shift. */
.account-state-sub {
    text-transform: none !important;
    letter-spacing: 0 !important;
    font-weight: 500 !important;
    font-size: 10px !important;
    color: var(--text-subtle) !important;
    display: inline-flex; align-items: center; gap: .35rem;
    margin-top: .15rem;
    min-height: 1em;
    font-variant-numeric: tabular-nums;
}
.account-state-sub::before {
    content: "";
    width: 6px; height: 6px; border-radius: 50%;
    background: var(--text-subtle);
    flex: 0 0 auto;
}
.account-sub {
    font-size: 10px;
    font-weight: 600;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: .05em;
}

/* Status line - icon + message, middle-aligned. Used inside the
   account-progress-row to the left of the progress bar. */
.account-status {
    font-size: 13px;
    color: var(--text);
    display: flex;
    align-items: center;
    gap: .55rem;
    margin: 0;
    font-weight: 500;
    min-width: 0;
}
.account-status .account-msg {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

/* Live-subject - quote-style block below the metrics grid. */
.account-live-subject {
    font-size: 11px !important;
    color: var(--text-muted) !important;
    margin: 0 !important;
    padding: .35rem .65rem !important;
    background: var(--surface-2);
    border-left: 2px solid var(--primary);
    border-radius: 4px;
}
.btn.with-icon { display: inline-flex; align-items: center; gap: .35rem; line-height: 1; }
.btn.with-icon svg { width: 14px; height: 14px; flex: 0 0 auto; stroke-width: 2; }
.btn.icon-only { width: 32px; height: 32px; padding: 0; display: inline-flex; align-items: center; justify-content: center; border-radius: 8px; }
.btn.icon-only svg { width: 15px; height: 15px; stroke-width: 1.8; }
.btn-ghost-danger { color: var(--text-subtle); background: transparent; border: 1px solid transparent; transition: all .12s ease; }
.btn-ghost-danger:hover { color: var(--danger); background: var(--danger-bg); border-color: #fecaca; }
.btn-ghost { color: var(--text-muted); background: transparent; border: 1px solid var(--border); transition: all .12s ease; }
.btn-ghost:hover { color: var(--text); background: var(--surface-2); border-color: var(--border-strong); }
.btn-ghost.is-busy { opacity: .6; pointer-events: none; }
.btn-ghost.is-busy svg { animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* Spinner */
.spinner-border-sm { width: 1rem; height: 1rem; border-width: 2px; }

/* Archive app */
.archive-layout { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }

/* ============================================================
   Archive header - compact left-clustered toolbar.
   Identity sits tight on the left; search pill and chips follow
   immediately with a small gap. No stretch, no wasted distance.
   ============================================================ */
.archive-header {
    flex: 0 0 auto;
    padding: .55rem 1rem;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    display: flex;
    align-items: center;
    gap: .85rem;
    min-height: 52px;
}
.archive-identity {
    display: flex; align-items: baseline; gap: .55rem;
    flex: 0 1 auto;
    min-width: 0;
    padding-right: .85rem;
    border-right: 1px solid var(--border);
}
/* The view's title holds ONE fixed width (owner, 2026-10-07: "make this not
   changing size or button alignment when switching folder"): "Inbox",
   "Closed" or "Loic Carrere (you) - Open" never move the sort button and
   the chips beside it; a longer name ends in an ellipsis, the whole name in
   the tab's title. */
@media (min-width: 821px) {
    .archive-identity { flex: 0 0 200px; width: 200px; }
    .archive-identity > .ws-manage-row { min-width: 0; max-width: 100%; }
}
.archive-identity h2 {
    font-size: 13px; font-weight: 600; color: var(--text); margin: 0;
    letter-spacing: -.01em;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    /* No fixed max-width: a hard cap clipped legitimately-short
       workspace names on tablets where the header wrapped to a
       narrower row but the cap still applied. Let flex shrink
       handle the truncation - the parent .archive-identity has
       min-width: 0 + flex: 0 1 auto which already lets the H2
       collapse gracefully. */
    min-width: 0;
    line-height: 1.3;
}
/* Disposal section: Spam + Trash close the sidebar behind one
   hairline. No heading - the rows say what they are; a title over
   two self-explanatory rows would be its own noise. */
.mail-sidebar .sb-utility {
    margin-top: .9rem;
    padding-top: .55rem;
    border-top: 1px solid var(--border);
}

/* The h2 is the VIEW title (Inbox, a teammate's queue, a saved
   view), not the workspace name - the scope suffix ("· Open")
   rides along muted so a queue's "open work only" contract stays
   visible without shouting. */
.mail-view-title-scope {
    font-weight: 500;
    font-size: 12px;
    color: var(--text-muted);
    margin-left: .1rem;
}
/* Saved-view definition: a quiet chevron beside the title opens a
   fixed, viewport-clamped popover holding the stored conditions as
   read-only pills (same renderer as the band). */
.view-def-toggle {
    display: inline-flex; align-items: center; justify-content: center;
    width: 20px; height: 20px;
    background: transparent; border: 0; border-radius: 6px;
    color: var(--text-muted); cursor: pointer;
    flex: 0 0 auto;
    transition: background .12s ease, color .12s ease;
}
.view-def-toggle:hover,
.view-def-toggle[aria-expanded="true"] { background: var(--surface-2); color: var(--text); }
/* Inside a saved view the whole title opens the definition - the
   pointer and the caret's hover state announce it together. */
.ws-manage-row:has(#viewDefToggle) .mail-view-title { cursor: pointer; }
.ws-manage-row:has(#viewDefToggle):hover .view-def-toggle { background: var(--surface-2); color: var(--text); }
.view-def-pop {
    position: fixed; z-index: 60;
    min-width: min(260px, calc(100vw - 24px));
    max-width: min(420px, calc(100vw - 24px));
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    box-shadow: var(--shadow);
    padding: .65rem .75rem .75rem;
}
.view-def-pop-title {
    font-size: 10px; font-weight: 600; letter-spacing: .04em;
    text-transform: uppercase; color: var(--text-muted);
    margin-bottom: .5rem;
}
.view-def-pills { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; }
/* "Run a rule" (subject manual trigger, wf-run-rules.js): the shared popover
   listing the item's runnable rules. Fixed + viewport-clamped like every
   platform popover; the BUTTON itself wears each surface's own control class. */
.wf-runrules-pop {
    position: fixed; z-index: 60;
    min-width: min(220px, calc(100vw - 24px));
    max-width: min(360px, calc(100vw - 24px));
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    box-shadow: var(--shadow);
    padding: .3rem;
    display: flex; flex-direction: column; gap: 2px;
}
.wf-runrules-item {
    display: block; width: 100%; text-align: left;
    background: transparent; border: 0; border-radius: 8px;
    padding: .42rem .6rem;
    font-size: 13px; font-weight: 520; color: var(--text);
    cursor: pointer;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.wf-runrules-item:hover, .wf-runrules-item:focus-visible { background: var(--surface-2); }
.archive-meta {
    font-size: 11px; color: var(--text-muted);
    display: inline-flex; align-items: center; gap: .4rem;
    white-space: nowrap;
    line-height: 1.2;
}
/* View-mode toggle: Conversation (threaded) <-> Message (flat).
   No track, no rounded chrome - just two icons side-by-side.
   The active glyph is brand-coloured with a thin square-corner
   underline (the indicator); the inactive glyph is muted and
   lifts on hover. Both icons stay visible so the operator sees
   the choice space at a glance.
   Lives at the top of the mail sidebar (.mail-mode-toggle-row)
   in a tight left-aligned strip - mode is a list-shape control,
   the folder list directly below IS that list. */
.mail-mode-toggle-row {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    /* Sidebar has padding:0 so the toggle row owns its own
       horizontal + vertical padding. Sits at the very top of the
       sidebar with a hairline border separating it from the
       scrollable body below. */
    /* On the pane's well (owner, 2026-09-06: "rounded appearance like
       Documents"): the row is part of the well, never a white cap on it. */
    padding: 8px 10px 6px;
    border-bottom: 1px solid var(--bf-hairline, rgba(15, 23, 42, .08));
    background: transparent;
}
/* Workspace-total tally on the right of the row. Both mode
   variants are server-rendered into the same grid cell so the
   tally's outer width = the WIDER of the two texts. Toggling
   visibility shows one + hides the other without changing the
   container's width: zero CLS on mode flip. No JS, no recompute.
   No vertical separator before the count - the gap from the
   toggle row's flex layout is enough breathing room and the
   hairline read as visual noise. */
.mail-mode-toggle-count {
    display: inline-grid;
    align-items: baseline;
    padding-left: 10px;
    font-size: 12px;
    color: var(--text-muted);
    line-height: 1.5;
    white-space: nowrap;
    min-width: 0;
    /* Single grid cell; both mode spans live inside it, sized
       by the wider one. justify-self:start keeps the visible
       text left-aligned on the start edge. */
    grid-template-columns: max-content;
    grid-template-rows: auto;
}
.mail-mode-toggle-count-text {
    grid-column: 1;
    grid-row: 1;
    display: inline-flex;
    align-items: baseline;
    gap: 4px;
    overflow: hidden;
    min-width: 0;
    justify-self: start;
}
.mail-mode-toggle-count-unit {
    color: var(--text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
}
/* Inactive mode = visibility:hidden (NOT display:none) so the
   span still occupies its grid cell and contributes to the
   tally's width calculation. The cell sizes to the wider of
   the two; flipping mode is a pure paint, no reflow. */
.mail-mode-toggle-count[data-view-mode="grouped"] .mail-mode-toggle-count-text[data-mode="flat"],
.mail-mode-toggle-count[data-view-mode="flat"]    .mail-mode-toggle-count-text[data-mode="grouped"] {
    visibility: hidden;
}
.mail-mode-toggle-count-unit {
    color: var(--text-muted);
}
.mail-mode-toggle {
    display: inline-flex;
    align-items: center;
    gap: 4px;
}
.mail-mode-toggle-half {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 26px;
    color: var(--text-muted);
    background: transparent;
    border: 0;
    text-decoration: none;
    transition: color .12s ease, background .12s ease;
}
.mail-mode-toggle-half svg {
    width: 15px;
    height: 15px;
    display: block;
}
/* Square-corner underline indicator. Always rendered, transparent
   on inactive halves, brand-coloured on the active. The icon stays
   on the same baseline regardless of state - no jump on flip. */
.mail-mode-toggle-half::after {
    content: "";
    position: absolute;
    left: 4px;
    right: 4px;
    bottom: 0;
    height: 2px;
    background: transparent;
    transition: background .12s ease;
}
.mail-mode-toggle-half:not(.is-active):hover {
    color: var(--text);
}
.mail-mode-toggle-half:not(.is-active):hover::after {
    background: color-mix(in srgb, var(--text-muted) 35%, transparent);
}
.mail-mode-toggle-half:focus-visible {
    outline: none;
    color: var(--text);
}
.mail-mode-toggle-half:focus-visible::after {
    background: var(--primary);
}
.mail-mode-toggle-half.is-active {
    color: var(--text);
    cursor: default;
}
.mail-mode-toggle-half.is-active::after {
    background: var(--primary);
}
/* Visually hidden text label for screen readers. */
.mail-mode-toggle-sr {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

/* Mail-app preferences gear. Sits at the top-right of the mail
   header (Pages/Mail/Index.cshtml), beside the conversation /
   messages mode toggle. Same visual weight as the .nav-icon-btn the
   global toolbar uses, so the operator's eye treats both as
   peripheral chrome. Hover lifts the icon's opacity to mirror the
   mode-seg hover affordance. */
.mail-prefs-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Same 34px as the search box and cmd-btns beside it - the gear at
       38px was silently setting the whole commandbar row 4px taller. */
    width: 34px; height: 34px;
    margin-left: auto;
    border: 1px solid transparent;
    background: transparent;
    border-radius: 10px;
    color: var(--text-muted);
    cursor: pointer;
    transition: background .12s ease, color .12s ease, border-color .12s ease;
}
.mail-prefs-btn svg { width: 19px; height: 19px; }
.mail-prefs-btn:hover {
    color: var(--text);
    background: var(--surface-2);
    border-color: var(--border);
}
.mail-prefs-btn:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}
.mail-prefs-btn svg { display: block; }

/* Commandbar is a flex row - search + filter + sort, packed left. */
.archive-commandbar {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    gap: .4rem;
    min-width: 0;
}

/* (The command bar's search field retired on 2026-09-06: it rides the strip
   - Services/Shell/StripSearch, app-shell.css ".app-search". The operators
   card below keeps its paint.) */

/* Action chips: Filter + Sort - sit immediately next to search, no gap */
.cmd-btn {
    display: inline-flex; align-items: center; gap: .4rem;
    padding: 0 .7rem; height: 34px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    font-size: 12px; font-weight: 500;
    color: var(--text);
    cursor: pointer;
    flex: 0 0 auto;
    transition: background .1s ease, border-color .1s ease, color .1s ease;
}
.cmd-btn:hover { background: var(--surface-2); border-color: var(--border-strong); }
.cmd-btn.active { background: color-mix(in srgb, var(--text) 7%, transparent); border-color: color-mix(in srgb, var(--text) 25%, transparent); color: var(--text); }
.cmd-btn svg { flex: 0 0 auto; color: var(--text-muted); }
.cmd-btn.active svg { color: var(--text); }
.cmd-btn .cmd-count {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 18px; height: 18px; padding: 0 5px;
    background: transparent; color: var(--primary);
    border-radius: 10px; font-size: 10px; font-weight: 700;
    margin-left: .1rem;
}

.sort-menu { position: relative; display: flex; }

/* Search help (?) button inside the pill + popover */
/* The search BUILDER (owner, 2026-09-06: the operators table was "too raw"):
   a card in the shell's grammar, one tile per operator - glyph, word, the
   syntax as a chip - that inserts itself into the field. */
.search-help-pop {
    position: absolute; top: calc(100% + 8px); left: 0; z-index: 60;
    background: var(--surface); border: 1px solid var(--border);
    border-radius: var(--app-pane-radius, 10px); box-shadow: 0 12px 28px rgba(15, 23, 42, .12), 0 2px 6px rgba(15, 23, 42, .08);
    width: 460px; max-width: calc(100vw - 24px); padding: 10px; font-size: 12px;
    animation: slide-down-sm .12s ease;
}
.search-help-pop[hidden] { display: none; }
.search-help-pop .shp-title { display: flex; align-items: center; min-height: 24px; padding: 0 6px; font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--text-subtle); }
.shp-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; margin-top: 6px; }
.shp-op {
    display: flex; align-items: center; gap: 10px;
    min-height: 44px; padding: 6px 10px;
    border: 1px solid var(--border); border-radius: 8px;
    background: var(--surface); color: var(--text);
    font: inherit; text-align: left; cursor: pointer;
    transition: border-color .14s ease, background-color .14s ease;
}
.shp-op:hover { border-color: var(--border-strong); background: color-mix(in srgb, var(--text) 4%, var(--surface)); }
.shp-op:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
.shp-op-glyph { flex: 0 0 auto; color: var(--text-muted); }
.shp-op-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.shp-op-word { font-size: 12px; font-weight: 500; line-height: 1.3; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.shp-op-code { font-family: ui-monospace, "Cascadia Mono", Menlo, Consolas, monospace; font-size: 11px; line-height: 1.2; color: var(--text-muted); background: transparent; padding: 0; }
.search-help-pop .shp-footer { margin-top: 8px; padding: 6px 6px 0; font-size: 11px; color: var(--text-muted); line-height: 1.45; }
.search-help-pop .shp-grid[hidden],
.search-help-pop .shp-title[hidden] { display: none; }
/* The builder's live sections (mail-search-builder.js): what the query is
   understood as - one hairline chip per part, a bad part in the danger
   tone with its note - and the suggestions for the token at the caret,
   32px rows the arrows walk. */
.msb-section { margin-bottom: 4px; }
.msb-section[hidden] { display: none; }
.msb-eyebrow { display: flex; align-items: center; min-height: 24px; padding: 0 6px; font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--text-subtle); }
.msb-chips { display: flex; flex-wrap: wrap; gap: 6px; padding: 2px 6px 6px; }
.msb-chip {
    display: inline-flex; align-items: center; gap: 6px;
    height: 28px; padding: 0 9px 0 8px; max-width: 100%;
    border: 1px solid var(--border); border-radius: 6px;
    background: var(--surface); color: var(--text); font-size: 12px;
}
.msb-chip svg { width: 13px; height: 13px; flex: 0 0 auto; color: var(--text-muted); }
.msb-chip-word { color: var(--text-muted); font-weight: 500; white-space: nowrap; }
.msb-chip-value { font-weight: 600; min-width: 0; max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msb-chip-swatch { width: 10px; height: 10px; border-radius: 2px; flex: 0 0 auto; }
.msb-chip-note { color: var(--danger); font-weight: 500; white-space: nowrap; }
.msb-chip.is-bad { border-color: color-mix(in srgb, var(--danger) 45%, transparent); }
.msb-chip.is-bad svg,
.msb-chip.is-bad .msb-chip-value { color: var(--danger); }
.msb-list { display: flex; flex-direction: column; gap: 1px; padding: 0 2px 4px; max-height: 296px; overflow-y: auto; }
.msb-row {
    display: flex; align-items: center; gap: 10px; width: 100%;
    min-height: 32px; padding: 0 8px;
    border: 0; border-radius: 6px; background: transparent;
    color: var(--text); font: inherit; font-size: 13px; text-align: left; cursor: pointer;
}
.msb-row:hover,
.msb-row.is-active { background: color-mix(in srgb, var(--text) 6%, transparent); }
.msb-row:focus-visible { outline: 2px solid var(--text); outline-offset: -2px; }
.msb-row svg { width: 14px; height: 14px; flex: 0 0 auto; color: var(--text-muted); }
.msb-row-swatch { width: 12px; height: 12px; border-radius: 4px; flex: 0 0 auto; }
.msb-row-word { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msb-row-code { font-family: ui-monospace, "Cascadia Mono", Menlo, Consolas, monospace; font-size: 11px; color: var(--text-subtle); }
.msb-hint { padding: 4px 8px 6px; font-size: 12px; color: var(--text-muted); }
@media (max-width: 560px) { .shp-grid { grid-template-columns: minmax(0, 1fr); } }

/* Quick filter chips */
/* ── The ONE filter band ──
   Quick-filter chips + active-filter pills share a single wrapping row
   (they were two stacked bordered bars; see the Index.cshtml comment).
   The band owns the background and the single bottom border; the two
   region divs inside it are layout-transparent. min-height (never
   fixed) + row-gap per the MailToolbarResizeContractTests doctrine;
   the pills segment keeps its own pinned fixed height so filter
   add / remove / clear never moves the list. */
.mail-filter-row {
    display: flex; flex-wrap: nowrap; align-items: center;
    column-gap: .6rem; row-gap: .3rem;
    min-height: 40px;
    /* Item of the height-locked .archive-layout column: without
       flex-shrink 0 the shell compresses the band to its min-height
       and a wrapped second line (cross-scope hint) paints over the
       list below. */
    flex: 0 0 auto;
    padding: 0 1rem;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
}
/* The regions dissolve into the row: their children (chips strip,
   pills bar, cross-scope hint) are the flex items. The adv-filter
   form is the one static in-between element - contents keeps it from
   becoming a phantom flex item (its own panel is position: fixed). */
.mail-filter-row > [data-mail-pane-region="filter-pills"],
.mail-filter-row > .adv-panel-form { display: contents; }
/* ONE line: a filter coming or going never changes the band's height
   (owner, 2026-10-24: the body moved 21px when the pills wrapped). The
   fit ladder (data-bf-fit on the band, bf-fit.js, decided before the
   first paint and in the task of every swap) folds only what it must:
   is-compact - the quick chips' words wait in their tooltips;
   is-tight - the door's and Save view's words too, the value pill narrows;
   is-snug - the value pills narrow again (their value in their tooltip);
   is-wrap - a band no fold can hold wraps rather than bite a control.
   The phone keeps its own sliding strip (mail-phone.css). */
/* Its two segments never shrink into a second line of their own: a band
   that does not fit OVERFLOWS, so the ladder sees it and folds. */
.mail-filter-row:not(.is-wrap) .quick-filters,
.mail-filter-row:not(.is-wrap) .filter-pills { flex-wrap: nowrap; flex-shrink: 0; }
/* The band centres its pills: their segment adds no height of its own
   (its 40px box made a 40px band, border included, one pixel taller). */
.mail-filter-row .filter-pills { height: auto; padding-block: 0; }
@media (min-width: 721px) {
    .mail-filter-row.is-wrap { flex-wrap: wrap; }
    .mail-filter-row.is-compact .qf-chip:not(.qf-chip-more) { min-width: 28px; padding: 0 7px; gap: .3rem; justify-content: center; }
    .mail-filter-row.is-compact .qf-chip-word { display: none; }
    .mail-filter-row.is-tight .qf-door-word { display: none; }
    .mail-filter-row.is-tight .qf-chip-more,
    .mail-filter-row.is-tight .filter-pill.save-view-pill { padding: 0 7px; gap: .3rem; }
    .mail-filter-row.is-tight .filter-pill-value { max-width: 120px; }
    .mail-filter-row.is-snug .filter-pill-value { max-width: 72px; }
}

.quick-filters {
    /* 8px between chips: two chips never read as one (the strip rule). */
    display: flex; gap: 8px; align-items: center;
    flex-wrap: wrap;
}
/* Quick-filter chips. Slightly-rounded squares (6px), uniform
   sizing, outline icons. Hover lifts color + bg subtly; active
   state uses a brand-tinted fill with brand-coloured icon + text
   so the row reads as one control strip with the Filters / Newest
   buttons in the header (same family of "list-shape" controls). */
.qf-chip {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    padding: 0 .65rem;
    height: 28px;
    background: transparent;
    border: 1px solid var(--border);
    border-radius: 6px;
    font-size: 11px;
    font-weight: 500;
    font-family: inherit;
    color: var(--text-muted);
    text-decoration: none;
    cursor: pointer;
    white-space: nowrap;
    transition: background .12s ease, color .12s ease, border-color .12s ease, box-shadow .12s ease;
}
/* Active chips grow the same × the pills carry - the toggle's one
   representation reads as removable in place. Muted until the chip
   is hovered so the strip stays calm. */
.qf-chip .qf-chip-x {
    display: inline-flex; align-items: center;
    margin-left: .05rem;
    opacity: .55;
    transition: opacity .12s ease;
}
.qf-chip:hover .qf-chip-x { opacity: 1; }
/* The advanced-filters entry point is a DOOR where its siblings are
   switches. A hairline separates it from the toggle family; the
   chip itself sits a step darker (ink text, stronger border); the
   glyph is a real set of sliders whose knobs glide to new positions
   on hover - the icon demonstrates what lives behind the door - and
   the caret flips while the panel is open. Motion is decorative and
   switches off under prefers-reduced-motion. */
.qf-strip-sep {
    width: 1px; height: 16px;
    background: var(--border);
    margin: 0 .15rem;
    flex: 0 0 auto;
}
.qf-chip-more {
    border-color: var(--border-strong);
    color: var(--text);
    font-weight: 600;
}
.qf-chip-more .qf-sliders { overflow: visible; }
.qf-chip-more .qf-knob { fill: var(--surface); }
/* The knob POSITIONS are state - they move on hover and hold while
   the panel is open, reduced motion or not. Only the glide between
   positions is decoration, so only the transition is motion-gated.
   Driven through the SVG2 cx presentation property rather than a
   CSS transform: transforms on SVG children are ignored by some
   engines, cx animates everywhere Chromium-derived. */
.qf-chip-more:hover .qf-knob-1,
.qf-chip-more.active .qf-knob-1 { cx: 10px; }
.qf-chip-more:hover .qf-knob-2,
.qf-chip-more.active .qf-knob-2 { cx: 14px; }
.qf-chip-more:hover .qf-knob-3,
.qf-chip-more.active .qf-knob-3 { cx: 13px; }
@media (prefers-reduced-motion: no-preference) {
    .qf-chip-more .qf-knob {
        transition: cx .24s cubic-bezier(.3, 1.4, .5, 1);
    }
}
.qf-chip-more .qf-more-caret {
    margin-left: .05rem;
    opacity: .55;
    transition: transform .18s ease, opacity .12s ease;
}
.qf-chip-more:hover .qf-more-caret { opacity: .9; }
.qf-chip-more.active .qf-more-caret { transform: rotate(180deg); opacity: 1; }
.qf-chip:hover {
    background: var(--surface-2);
    color: var(--text);
    border-color: color-mix(in srgb, var(--border) 60%, var(--text-muted) 40%);
}
.qf-chip.active {
    background: color-mix(in srgb, var(--text) 7%, transparent);
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
    color: var(--text);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--primary) 20%, transparent);
}
.qf-chip.active:hover {
    background: color-mix(in srgb, var(--text) 5%, var(--surface));
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
}

/* Premium empty-state surfaces for the message list. Three
   shapes share the same envelope: large outline glyph, title,
   muted subtitle, optional CTA. Centered in the available
   vertical space; max-width keeps long copy from running edge
   to edge on wide viewports. */
.mail-empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 3.5rem 1.25rem;
    margin: auto;
    max-width: 420px;
    color: var(--text-muted);
    gap: .35rem;
}
.mail-empty-art {
    margin-bottom: 1.25rem;
    line-height: 0;
    /* Brand-bar colours - the Burofolk logo's cyan / violet /
       magenta. Per-state opacity + filters tune the saturation
       (vivid for celebrate, muted for filtered/quiet). */
    --bf-cyan: #06B6D4;
    --bf-violet: var(--primary);
    --bf-magenta: #EC4899;
}
.mail-empty-art .bf-bar { transition: fill .2s ease, opacity .2s ease; }
/* A search with no match: the search glyph stands bare in the subtle ink. */
.mail-empty-art-search { color: var(--text-subtle); }
.mail-empty-art .bf-bar-1 { fill: var(--bf-cyan); }
.mail-empty-art .bf-bar-2 { fill: var(--bf-violet); }
.mail-empty-art .bf-bar-3 { fill: var(--bf-magenta); }

/* Celebrate state - inbox zero. Bars at full saturation with a
   soft drop shadow + animated pulse. The sparkle group rotates
   slowly around the centre to feel alive without being noisy. */
.mail-empty-art-celebrate svg {
    filter: drop-shadow(0 6px 16px color-mix(in srgb, var(--primary) 25%, transparent));
    animation: mail-empty-pulse 2.6s ease-in-out infinite;
}
.mail-empty-art-celebrate .bf-spark circle {
    fill: var(--bf-violet);
    opacity: .6;
    animation: mail-empty-spark 3s ease-in-out infinite;
}
.mail-empty-art-celebrate .bf-spark circle:nth-child(2) { fill: var(--bf-cyan);    animation-delay: .4s; }
.mail-empty-art-celebrate .bf-spark circle:nth-child(3) { fill: var(--bf-magenta); animation-delay: .8s; }
.mail-empty-art-celebrate .bf-spark circle:nth-child(4) { fill: var(--bf-violet);  animation-delay: 1.2s; }
@keyframes mail-empty-pulse {
    0%, 100% { transform: scale(1);    opacity: 1; }
    50%      { transform: scale(1.03); opacity: .92; }
}
@keyframes mail-empty-spark {
    0%, 100% { opacity: .25; transform: scale(.8); }
    50%      { opacity: .9;  transform: scale(1.2); }
}

/* Filtered no-results state - bars desaturated to grey, with a
   subtle diagonal slash sweeping across them. Reads as "we
   tried, nothing came through the filter". */
.mail-empty-art-filtered .bf-bar {
    fill: color-mix(in srgb, var(--text-muted) 60%, transparent);
}
.mail-empty-art-filtered .bf-slash {
    stroke: color-mix(in srgb, var(--text-muted) 90%, transparent);
}

/* Generic empty folder - same bars but lower opacity. Quiet,
   not a problem state, just "nothing here yet". */
.mail-empty-art-quiet .bf-bar {
    opacity: .42;
}
.mail-empty-title {
    font-size: 15px;
    font-weight: 600;
    color: var(--text, #0f172a);
    margin: 0;
    letter-spacing: -0.005em;
}
.mail-empty-sub {
    font-size: 12px;
    color: var(--text-muted);
    margin: 0;
    line-height: 1.5;
}
.mail-empty-cta {
    margin-top: 1rem;
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    padding: .55rem 1.1rem;
    border: 1px solid var(--border, #e2e8f0);
    border-radius: 8px;
    background: var(--surface, #fff);
    color: var(--text, #0f172a);
    font-size: 12px;
    font-weight: 500;
    text-decoration: none;
    transition: background .12s ease, border-color .12s ease, color .12s ease;
}
.mail-empty-cta:hover {
    background: color-mix(in srgb, var(--text) 5%, transparent);
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
    color: var(--text);
    text-decoration: none;
}

/* Day-grouping section header inside the message list. Sticky so
   the operator always knows which bucket they're scrolling
   through; muted uppercase eyebrow style keeps it quiet enough
   not to compete with the actual rows. */
.mail-day-group {
    position: sticky;
    top: 0;
    z-index: 2;
    display: flex;
    align-items: center;
    padding: 6px 1rem 4px;
    background: var(--surface, #ffffff);
    border-bottom: 1px solid color-mix(in srgb, var(--border, #e2e8f0) 60%, transparent);
    user-select: none;
}
.mail-day-group-label {
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--text-muted);
}
/* Per-bucket count chip - sits inline RIGHT AFTER the label so
   "TODAY · 12" reads as one unit. The interpunct (·) separator
   is a CSS pseudo-element so the markup stays clean. Variable
   width per row (TODAY · 12 vs EARLIER THIS WEEK · 47) feels
   more crafted than a templated right-edge column.
   tabular-nums + letter-spacing:0 keep digit alignment crisp
   regardless of the parent row's eyebrow tracking. */
.mail-day-group-count {
    display: inline-flex;
    align-items: center;
    margin-left: 8px;
    font-size: 11px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0;
    text-transform: none;
    color: var(--text-muted);
    transition: color 120ms;
}
/* The interpunct separator. Lives in CSS so the Razor markup
   stays semantic (label + count, no glyph in between). The
   leading space + trailing space keep the dot visually balanced
   between the two text runs. */
.mail-day-group-count::before {
    content: "·";
    margin-right: 8px;
    color: color-mix(in srgb, var(--text-muted) 50%, transparent);
    font-weight: 400;
}
/* On hover of the day-group row, nudge the count to primary
   tint so the active bucket feels alive. Tiny effect - the row
   itself doesn't change. */
.mail-day-group:hover .mail-day-group-count {
    color: var(--primary);
}
/* Toast that confirms a view action (save / delete / share / unshare).
   Floats in the top-right over the page - overlay only, does NOT take layout
   space, so appearing/dismissing it never shifts the content underneath. */
.view-flash {
    position: fixed;
    top: 1rem;
    right: 1rem;
    z-index: 10000;
    display: flex;
    align-items: center;
    gap: .55rem;
    padding: .6rem 1rem;
    max-width: min(420px, calc(100vw - 2rem));
    background: var(--surface);
    border: 1px solid var(--border);
    color: var(--text);
    border-radius: 12px;
    font-size: 12px;
    font-weight: 500;
    box-shadow: 0 8px 24px -8px rgba(15, 23, 42, .28),
                0 2px 6px rgba(15, 23, 42, .12);
    animation: view-flash-in .2s ease-out;
    pointer-events: auto;
}
.view-flash svg { flex: 0 0 auto; color: var(--primary); }
.view-flash > span { flex: 1 1 auto; color: var(--text); min-width: 0; }
.view-flash-close {
    flex: 0 0 auto;
    background: transparent;
    border: 0;
    font-size: 1.15rem;
    line-height: 1;
    color: var(--text-muted);
    cursor: pointer;
    padding: 0 .3rem;
    border-radius: 4px;
}
.view-flash-close:hover { color: var(--text); background: var(--surface-2); }
.view-flash.is-leaving { animation: view-flash-out .2s ease-in forwards; }
@keyframes view-flash-in { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }
@keyframes view-flash-out { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateY(-8px); } }
html[data-theme="dark"] .view-flash {
    background: var(--surface);
    border-color: var(--border-strong);
    box-shadow: 0 8px 24px -8px rgba(0, 0, 0, .5),
                0 2px 6px rgba(0, 0, 0, .4);
}

.qf-chip svg { color: currentColor; }
.qf-chip .qf-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; display: inline-block; }

/* Undo toast */
.undo-toast {
    position: fixed; bottom: 24px; left: 50%; transform: translate(-50%, 20px);
    background: #1e293b; color: #fff;
    padding: .65rem .75rem .65rem 1rem;
    border-radius: 10px;
    box-shadow: 0 20px 48px rgba(15,23,42,.45);
    display: flex; align-items: center; gap: .6rem;
    min-width: 280px; max-width: 90vw;
    z-index: 4000;
    opacity: 0; transition: opacity .18s ease, transform .18s ease;
    overflow: hidden;
}
.undo-toast.visible { opacity: 1; transform: translate(-50%, 0); }
/* Nothing ever touches: the quickbar's tab shares the bottom centre; while it
   shows, the undo toast stands above it instead of over it (issue #450 P0,
   the first undo shown on /Work). */
body:has(.bf-quickbar-tab.is-visible) .undo-toast { bottom: 56px; }
.undo-toast .undo-msg { font-size: 12px; color: #e2e8f0; flex: 1 1 auto; }
.undo-toast .undo-btn {
    padding: .25rem .7rem; border-radius: 6px;
    background: transparent; border: 1px solid rgba(255,255,255,.25); color: #fff;
    font-size: 11px; font-weight: 600; cursor: pointer;
    transition: background .1s ease;
}
.undo-toast .undo-btn:hover { background: rgba(255,255,255,.12); }
.undo-toast.is-undoing .undo-btn { opacity: .5; cursor: default; }
.undo-toast .undo-close {
    width: 24px; height: 24px; border-radius: 50%;
    background: transparent; border: 0; color: var(--text-subtle);
    font-size: 16px; cursor: pointer; line-height: 1;
    display: inline-flex; align-items: center; justify-content: center;
}
.undo-toast .undo-close:hover { background: rgba(255,255,255,.1); color: #fff; }
.undo-toast .undo-bar {
    position: absolute; left: 0; bottom: 0; height: 2px;
    background: var(--primary); width: 100%;
}
@keyframes undo-countdown { from { width: 100%; } to { width: 0%; } }

/* Icon-only commandbar chip (e.g. Refresh) */
.cmd-btn.cmd-icon-btn { padding: 0; width: 34px; justify-content: center; }
.cmd-btn.cmd-icon-btn svg { color: var(--text-muted); }
.cmd-btn.cmd-icon-btn:hover svg { color: var(--text); }
/* Trailing chip: pushed to the far right of the commandbar */
.cmd-btn.cmd-trailing { margin-left: auto; }

/* Wrap the mail header at the iPad-portrait threshold (~1024px)
   plus a bit of headroom so a tablet at 900-1100px doesn't try
   to cram identity + segmented toggle + search + filter chips +
   sort + gear onto a single row, which clipped the workspace
   name and the trailing buttons before this breakpoint widened. */
@media (max-width: 1100px) {
    .archive-header { flex-wrap: wrap; gap: .55rem; }
    .archive-identity { padding-right: 0; border-right: 0; flex: 1 1 100%; }
    .archive-commandbar { flex: 1 1 100%; }
    .archive-search { flex: 1 1 auto; max-width: none; }
}
@media (max-width: 560px) {
    .cmd-btn span.cmd-label { display: none; }
    .cmd-btn { padding: 0 .55rem; }
}

/* Advanced filter form - floating panel anchored under Filters button via JS */
.adv-panel-form { position: static; z-index: 40; }
.adv-panel-form .adv-panel {
    position: fixed;
    /* top + left set by JS to sit under the Filters button */
    width: min(680px, calc(100vw - 2rem));
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 1.1rem 1.25rem 1rem;
    box-shadow: 0 20px 50px rgba(15, 23, 42, .18), 0 2px 6px rgba(15, 23, 42, .06);
    margin: 0;
    z-index: 40;
    animation: slide-down-sm .15s ease;
}
.adv-panel-form .adv-panel .adv-row { gap: .85rem; margin-bottom: .65rem; }
.adv-panel-form .adv-panel label span { font-size: 10px; }
.adv-panel-form .adv-panel input[type="text"],
.adv-panel-form .adv-panel input[type="date"],
.adv-panel-form .adv-panel label input:not([type="checkbox"]) { padding: .4rem .6rem; font-size: 12px; }
.adv-panel-form .adv-panel .adv-actions { margin-top: .75rem; padding-top: .75rem; border-top: 1px solid var(--border); }

/* Label-filter row. Visually matches the other input rows in the
   panel: a small-caps caption followed by a full-width control. The
   "control" here is a stack - mode <select> on top, chip cloud +
   hint underneath when mode = "specific". The mode select's full
   width is intentional; pairing it with anything to the right made
   the row feel half-empty when chips were hidden. */
.adv-panel-form .adv-panel .adv-row-1 {
    grid-template-columns: 1fr;
}
.adv-panel-form .adv-panel .adv-row-label-filter {
    margin-top: .25rem;
    padding-top: .65rem;
    border-top: 1px solid var(--border);
}
.adv-panel-form .adv-panel .adv-label-control {
    display: flex;
    flex-direction: column;
    gap: .5rem;
}
.adv-panel-form .adv-panel .adv-label-mode {
    /* Compact select sized to its content. Width: max-content lets
       the browser size to the longest option text + native chevron;
       no full-width stretch, no oversized minimum. The widest option
       ("Has specific label(s)") sits around ~170px including the
       arrow, which is right-sized for a small dropdown. */
    width: max-content;
    max-width: 100%;
    padding: .35rem .55rem;
    font-size: 12px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--surface);
    color: var(--text);
    appearance: auto;
    cursor: pointer;
    align-self: flex-start;
}
.adv-panel-form .adv-panel .adv-label-mode:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 1px;
}

/* Compact label list: hidden until mode == specific. One line per
   label (checkbox + name), scrollable when the list grows past
   ~6 entries. Sits flush below the select with a soft outline so
   it reads as a "list of options to tick" without competing with
   the input rows above. */
.adv-panel-form .adv-panel .adv-label-list {
    display: none;
    flex-direction: column;
    gap: 0;
    max-height: 200px;
    overflow-y: auto;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
}
.adv-panel-form .adv-panel .adv-label-control[data-adv-label-active="true"] .adv-label-list {
    display: flex;
}
.adv-panel-form .adv-panel .adv-label-row {
    /* One row = one line. Force horizontal flow and LEFT alignment
       (justify-content: flex-start) so the checkbox sits next to its
       label like a regular OS-style multi-pick list. The select-
       picker style on .chk has previously bled in via specificity
       conflicts; the !important here is defensive against any
       future ancestor change that injects align-items: center on
       a stacked column. */
    display: flex !important;
    flex-direction: row !important;
    align-items: center !important;
    justify-content: flex-start !important;
    gap: .5rem;
    padding: .35rem .6rem;
    margin: 0;
    border: 0;
    background: transparent;
    cursor: pointer;
    font-size: 12px;
    color: var(--text);
    text-transform: none;
    letter-spacing: 0;
    text-align: left;
    transition: background .08s ease;
}
.adv-panel-form .adv-panel .adv-label-row + .adv-label-row {
    border-top: 1px solid var(--surface-2);
}
.adv-panel-form .adv-panel .adv-label-row:hover {
    background: var(--surface-2);
}
.adv-panel-form .adv-panel .adv-label-row input[type="checkbox"] {
    flex: 0 0 auto;
    width: 14px;
    height: 14px;
    margin: 0;
    accent-color: var(--primary);
    cursor: pointer;
}
.adv-panel-form .adv-panel .adv-label-row-name {
    flex: 1 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-weight: 500;
    /* Override the panel's small-caps eyebrow style on bare <span>. */
    text-transform: none;
    letter-spacing: 0;
    font-size: 12px;
    color: var(--text);
}
.adv-panel-form .adv-panel .adv-label-row:has(input:checked) {
    background: var(--primary-bg);
}
.adv-panel-form .adv-panel .adv-label-row:has(input:checked) .adv-label-row-name {
    color: var(--primary);
    font-weight: 600;
}
/* Assignees variant - same row shape as labels but with a 22px
   user avatar between the checkbox and the name. Avatar uses the
   same letter-bubble style as everywhere else in the app. */
.adv-panel-form .adv-panel .adv-assignee-avatar {
    width: 22px; height: 22px;
    min-width: 22px; max-width: 22px;
    min-height: 22px; max-height: 22px;
    border-radius: 50%;
    flex: 0 0 auto;
    overflow: hidden;
    background: var(--avatar-bg);
    color: var(--avatar-color);
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 10px; font-weight: 700;
    object-fit: cover;
    object-position: center;
    margin-right: .35rem;
}
.adv-panel-form .adv-panel img.adv-assignee-avatar { display: block; }
.adv-panel-form .adv-panel .adv-label-hint {
    display: none;
    font-size: 10px;
    color: var(--text-muted);
    line-height: 1.4;
    padding: 0 .15rem;
}
.adv-panel-form .adv-panel .adv-label-control[data-adv-label-active="true"] .adv-label-hint {
    display: block;
}
/* Backdrop click-out support (hidden by default; shown when panel is open via JS) */
.adv-panel-backdrop { position: fixed; inset: 0; z-index: 35; background: transparent; }
.adv-panel-backdrop[hidden] { display: none; }

/* Confirm modal - premium replacement for browser confirm() */
.confirm-backdrop { position: fixed; inset: 0; z-index: 9998; background: rgba(15,23,42,.55); backdrop-filter: blur(3px); display: flex; align-items: center; justify-content: center; padding: 1rem; animation: fade-in .12s ease; }
.confirm-backdrop[hidden] { display: none; }
.confirm-panel { width: min(440px, 92vw); background: var(--surface); border: 1px solid var(--border); border-radius: 16px; box-shadow: 0 20px 60px rgba(0,0,0,.3); padding: 1.75rem 1.75rem 1.25rem; text-align: center; animation: slide-down-sm .15s ease; }
.confirm-icon { width: 56px; height: 56px; margin: 0 auto 1rem; border-radius: 10px; background: var(--primary-bg); color: var(--primary); display: inline-flex; align-items: center; justify-content: center; }
.confirm-panel h3 { font-size: 1.15rem; font-weight: 700; margin: 0 0 .6rem; letter-spacing: -.01em; color: var(--text); }
.confirm-panel p { font-size: 13px; color: var(--text-muted); line-height: 1.55; margin: 0 0 1.5rem; }
.confirm-panel p strong { color: var(--text); }
.confirm-actions { display: flex; gap: .5rem; justify-content: center; }
/* ── The PROMPT variant of the panel ──────────────────────────────────
   A prompt asks for ONE value. The confirm dialog it borrows its shell from is
   built to stop somebody - centred, big title, two equal buttons - and a
   one-line question wearing that layout read as a warning with a pill in the
   middle of it (owner, 2026-08-17: "la modale d ajout de tag est immonde").
   Here: left-aligned, the question as the field label, the field in the app own
   grammar, and the actions at the end with one ink verb. */
.confirm-panel.is-prompt {
    width: min(420px, 92vw);
    padding: 18px 20px 16px;
    border-radius: 10px;
    text-align: left;
}
.confirm-panel.is-prompt h3 { font-size: 15px; font-weight: 600; margin: 0 0 12px; }
.confirm-panel.is-prompt .confirm-label {
    display: block;
    margin: 0 0 6px;
    font-size: 12px;
    font-weight: 500;
    color: var(--text-muted);
}
.confirm-panel.is-prompt .confirm-input {
    width: 100%;
    height: 36px;
    padding: 0 10px;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--surface);
    color: var(--text);
    font: inherit;
    font-size: 14px;
    outline: none;
}
.confirm-panel.is-prompt .confirm-input:focus {
    border-color: color-mix(in srgb, var(--brand-cyan, #0891b2) 60%, transparent);
}
.confirm-panel.is-prompt .confirm-actions {
    justify-content: flex-end;
    margin-top: 16px;
}
/* The report-a-problem dialog's field: the prompt input's chrome on a textarea
   that grows with its words (bf-autosize), never a fixed 36px line. */
.confirm-panel.is-prompt .confirm-input.report-problem-body {
    height: auto;
    min-height: 84px;
    padding: 8px 10px;
    line-height: 1.45;
    resize: none;
}
.confirm-panel.is-prompt .confirm-actions .btn { min-width: 0; padding: 6px 14px; }
.confirm-actions .btn { min-width: 120px; }
/* Spinner shown inside the OK button while an async-confirm callback
   is running. Same look as the bulk-toolbar spinner so the visual
   language is consistent across the app. */
.confirm-actions .btn .confirm-spin {
    display: inline-block;
    width: 14px; height: 14px;
    margin-right: .45rem;
    border: 2px solid currentColor;
    border-right-color: transparent;
    border-radius: 50%;
    vertical-align: -2px;
    animation: confirm-spin .65s linear infinite;
}
@keyframes confirm-spin { to { transform: rotate(360deg); } }
.confirm-actions .btn:disabled, .confirm-actions .btn[disabled] { opacity: .7; cursor: progress; }
/* Inline-error state for the message paragraph - used when an async
   onConfirm callback throws so the user can read what went wrong
   without leaving the modal. */
.confirm-panel p.confirm-error {
    color: #b91c1c;
    background: color-mix(in srgb, #b91c1c 6%, transparent);
    border: 1px solid color-mix(in srgb, #b91c1c 22%, transparent);
    border-radius: 8px;
    padding: .65rem .85rem;
    margin: 0 0 1.25rem;
    text-align: left;
}
/* Disclosure list under the message (Burofolk.confirm `details`): the
   data-model delete names, per workspace, the documents whose extracted
   data the delete erases. Quiet left-aligned block - hairline rules, no
   fill; a long list scrolls inside the dialog (the panel opens at its
   final size and never grows under the pointer). */
.confirm-details { list-style: none; margin: -.5rem 0 1.5rem; padding: .3rem 0; text-align: left; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); max-height: 11rem; overflow-y: auto; }
.confirm-details li { font-size: 12px; color: var(--text); line-height: 1.5; padding: .32rem .25rem; }
.confirm-details li + li { border-top: 1px solid color-mix(in srgb, var(--border) 55%, transparent); }
/* A details item can be a LINK (the data-model usage listing's workspace
   rows): type glyph + name + muted count on one line. Hover underlines the
   name only - the row stays quiet, no fill. */
.confirm-details-link { display: flex; align-items: center; gap: .5rem; min-width: 0; color: var(--text); text-decoration: none; }
.confirm-details-link:hover .confirm-details-name,
.confirm-details-link:focus-visible .confirm-details-name { text-decoration: underline; }
.confirm-details-ic { flex: 0 0 auto; display: inline-flex; }
/* The workspace's own identity bubble (bf-icon), chip-sized for the row. */
.confirm-details-ic .bf-icon { --bf-icon-size: 18px; --bf-icon-radius: 6px; width: 18px; height: 18px; }
/* A non-space door in a usage list (a circuit, a tray, a pack, an automation): its glyph in the same 18px box. */
.confirm-details-glyph { width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center; color: var(--text-muted); }
.confirm-details-glyph svg { width: 15px; height: 15px; }
.confirm-details-name { flex: 0 1 auto; min-width: 0; font-weight: 500; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.confirm-details-sub { flex: 0 0 auto; color: var(--text-muted); }
.confirm-details-sub::before { content: "· "; }
/* ── The MODES variant of the confirm (Burofolk.confirm `choices`) ─────────
   Two or three ways to do one thing, drawn side by side BEFORE anything
   commits (issue #428: restore a copy, or return and delete what followed).
   Each mode is a tile - glyph, ONE strong name, one muted line - and the
   picked one wears the selection wash with its hairline ring. A destructive
   mode says so in the danger tone; a mode a records rule refuses stays on
   screen, locked, its line naming the rule. The OK verb follows the pick. */
.confirm-panel.is-choice { width: min(540px, 92vw); }
.confirm-choices { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin: .25rem 0 1.5rem; text-align: left; }
.confirm-choice {
    display: flex; flex-direction: column; align-items: flex-start; gap: 6px;
    min-width: 0; min-height: 100%; padding: 14px 14px 13px;
    border: 1px solid var(--border); border-radius: 12px;
    background: transparent; color: var(--text); font: inherit; text-align: left; cursor: pointer;
    transition: background-color .12s ease, border-color .12s ease;
}
.confirm-choice:hover { border-color: color-mix(in srgb, var(--text) 22%, var(--border)); }
.confirm-choice:focus-visible { outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--text) 14%, transparent); }
.confirm-choice[aria-checked="true"] {
    background: color-mix(in srgb, var(--text) 7%, transparent);
    border-color: color-mix(in srgb, var(--text) 26%, var(--border));
}
.confirm-choice-ic { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; color: var(--text-muted); }
.confirm-choice-ic svg { width: 20px; height: 20px; }
.confirm-choice-title { font-size: 13px; font-weight: 600; line-height: 1.3; color: var(--text); }
.confirm-choice-sub { font-size: 12px; line-height: 1.4; color: var(--text-muted); }
.confirm-choice.is-danger .confirm-choice-ic,
.confirm-choice.is-danger .confirm-choice-title { color: var(--danger); }
.confirm-choice.is-danger:hover { border-color: color-mix(in srgb, var(--danger) 45%, var(--border)); }
.confirm-choice.is-danger[aria-checked="true"] {
    background: color-mix(in srgb, var(--danger) 7%, transparent);
    border-color: color-mix(in srgb, var(--danger) 50%, var(--border));
}
.confirm-choice.is-locked { cursor: not-allowed; background: transparent; }
.confirm-choice.is-locked:hover { border-color: var(--border); }
.confirm-choice.is-locked .confirm-choice-ic,
.confirm-choice.is-locked .confirm-choice-title { color: var(--text-muted); }
@media (max-width: 480px) { .confirm-choices { grid-template-columns: minmax(0, 1fr); } }
/* The REVERT confirm (issue #428) names every version it deletes - number,
   author, date - so its list never scrolls on its own: the panel is bounded
   by the viewport and is the one scroller when a history is very long. */
.confirm-panel.is-revert { width: min(480px, 92vw); max-height: calc(100dvh - 2rem); overflow-y: auto; }
.confirm-panel.is-revert .confirm-details { max-height: none; overflow: visible; }
/* A listing dialog (a type's usage and history, #541): the list is the content, never a scroller inside the dialog. */
.confirm-panel.is-list .confirm-details { max-height: none; overflow: visible; }
.confirm-panel.is-list { max-height: calc(100dvh - 32px); overflow-y: auto; }
.confirm-details-heading { display: block; padding-top: .35rem; font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--text-muted); }
.confirm-version { display: flex; align-items: center; gap: 8px; min-width: 0; }
.confirm-version .confirm-version-avatar { flex: 0 0 auto; width: 20px; height: 20px; border-radius: 50%; overflow: hidden; display: inline-flex; align-items: center; justify-content: center; font-size: 9px; object-fit: cover; }
.confirm-version-num { flex: 0 0 auto; min-width: 2.2em; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--danger); }
.confirm-version-who { flex: 0 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--text); }
.confirm-version-when { flex: 0 0 auto; margin-left: auto; padding-left: 8px; color: var(--text-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.archive-toolbar-left { flex: 0 0 auto; min-width: 0; }
.archive-toolbar-left h2 { font-size: 15px; font-weight: 600; margin: 0; color: var(--text); letter-spacing: -.01em; }
.archive-toolbar-left .sub { font-size: 11px; color: var(--text-muted); margin-top: .1rem; }

/* (legacy .archive-search block removed - see unified rules around line 736) */
.archive-body { flex: 1 1 auto; min-height: 0; display: flex; overflow: hidden; }
/* Sidebar drawer backdrop is mobile-only - hide on desktop so it can never
   accidentally cover the page and block clicks on the navbar. */
.archive-sidebar-backdrop { display: none; }
/* Sidebar width comes from a custom property so the pane resizer
   (Shell/_PaneResizer, pane-resize.js) can mutate it inline
   (--archive-sidebar-w) and the value is trivially persistable (one
   number to localStorage / read back on page load). 260px is the
   historical default. The sidebar itself does NOT scroll -
   .archive-sidebar-body inside it does. That keeps the view-mode toggle
   row pinned at the top so its scrollbar track starts BELOW the toggle,
   never overlapping the mode-switch area.
   The sidebar is the module's PANE: the same CARD as every module pane
   (the surface, one hairline edge, one radius, inset from the frame; owner,
   2026-09-06: "unify to what we have on documents", then "no washed
   backgrounds"). */
.archive-sidebar {
    flex: 0 0 var(--archive-sidebar-w, 260px);
    width: var(--archive-sidebar-w, 260px);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--app-pane-radius);
    margin: var(--app-pane-inset-y) 0 var(--app-pane-inset-y) var(--app-pane-inset-x);
    display: flex;
    flex-direction: column;
    min-height: 0;
    overflow: hidden;
    padding: 0;
    container-type: inline-size;
    container-name: archive-sidebar;
}
.archive-sidebar-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    padding: 0 .75rem 1rem;
}
/* ── The pane control folds the sidebar (owner, 2026-09-07) ─────────
   The page head's panel glyph (Shell/_PageHead, app-shell.js) folds the
   folders to the 60px glyph rail every module pane shares: .archive-layout
   wears is-pane-collapsed (rendered from the viewer's cookie -
   PaneCollapse); every row keeps its glyph, centred in a 36px seat, the
   words, counts and eyebrows wait for the expanded pane, Compose is its
   glyph, the mode toggle shows the ONE mode a click would switch to.
   Desktop only: below 821px the sidebar is a drawer the same glyph opens. */
@media (min-width: 821px) {
    .archive-layout.is-pane-collapsed .archive-sidebar { flex-basis: 60px; width: 60px; }
    .archive-layout.is-pane-collapsed .archive-body > .app-pane-resizer { visibility: hidden; pointer-events: none; }
    .archive-layout.is-pane-collapsed .archive-sidebar-body { padding: 0 6px 1rem; overflow-x: hidden; }
    .archive-layout.is-pane-collapsed .mail-sidebar h6,
    .archive-layout.is-pane-collapsed .sb-name,
    .archive-layout.is-pane-collapsed .sb-count,
    .archive-layout.is-pane-collapsed .sb-empty-toggle,
    .archive-layout.is-pane-collapsed .mail-mode-toggle-count,
    .archive-layout.is-pane-collapsed .mail-compose-btn-label,
    .archive-layout.is-pane-collapsed .view-name,
    .archive-layout.is-pane-collapsed .view-count,
    .archive-layout.is-pane-collapsed .view-actions,
    .archive-layout.is-pane-collapsed .mail-mode-toggle-half.is-active { display: none; }
    .archive-layout.is-pane-collapsed .mail-mode-toggle-row { justify-content: center; padding: 8px 4px 6px; }
    .archive-layout.is-pane-collapsed .mail-sidebar .list-group-item {
        justify-content: center;
        width: 36px;
        min-height: 36px;
        margin: 0 auto;
        padding: 0;
        gap: 0;
        border-radius: 8px;
    }
    .archive-layout.is-pane-collapsed .mail-sidebar .list-group-item .sb-icon,
    .archive-layout.is-pane-collapsed .mail-sidebar .list-group-item .sb-status-glyph { margin: 0; }
    .archive-layout.is-pane-collapsed .mail-compose-btn { width: 36px; height: 36px; margin: 10px auto 6px; padding: 0; gap: 0; }
    .archive-layout.is-pane-collapsed .view-item .view-link { justify-content: center; padding: .5rem 0; gap: 0; }
}
/* Below the drawer cut the sidebar leaves the frame: no inset, no radius,
   the surface. (The drawer block sits earlier in the file; this one wins
   over the well rule above by order.) */
@media (max-width: 820px) {
    .archive-sidebar { margin: 0; border-radius: 0; background: var(--surface); border: 0; border-right: 1px solid var(--border); }
}
.archive-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; overflow: hidden; background: var(--surface); }
/* The list is a pane card like its neighbours (owner, 2026-10-07: the
   toolbar and the pager "rectangular and stuck to the top / bottom
   producing unbalanced layout"): the folders and the reading pane stood
   inset, rounded and lifted while the list ran flush from the strip to the
   window's foot. Same inset, hairline, radius and lift as .archive-sidebar,
   in every reading mode; the toolbar is the card's head and the pager its
   foot, each wearing the card's corners (the column stops clipping beside a
   pane - mail-reading-pane.css - so the corners are the children's own).
   Below the drawer cut the list stays the full-bleed surface. */
@media (min-width: 821px) {
    .archive-main {
        --archive-card-r: calc(var(--app-pane-radius) - 1px);
        margin: var(--app-pane-inset-y) var(--app-pane-inset-x) var(--app-pane-inset-y) 0;
        border: 1px solid var(--border);
        border-radius: var(--app-pane-radius);
        box-shadow: var(--app-pane-shadow);
    }
    .archive-main > .bulk-bar { border-radius: var(--archive-card-r) var(--archive-card-r) 0 0; }
    .archive-main > .pagination-bar { border-radius: 0 0 var(--archive-card-r) var(--archive-card-r); box-shadow: none; }
}
/* The gap after the sidebar is THE pane resizer (app-shell.css
   .app-pane-resizer); below the drawer cut the sidebar leaves the flow
   and the handle goes with it. */
@media (max-width: 820px) {
    .archive-body > .app-pane-resizer { display: none; }
}
.archive-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.archive-list { flex: 1 1 auto; min-width: 0; overflow-y: auto; background: var(--surface); padding: 0; }

/* Lazy first-paint loading hero + skeleton. Rendered when the
   server emits Model.ListLazyLoad=true (i.e. on the synchronous
   /Mail/{id} render path). Hero is the focal point - centered
   spinner + clean copy - so the user sees a polished loading
   state, not raw text on a plain background. Skeleton rows
   continue underneath as ambient structure. */
.archive-scroll-loading { padding: 0; }

/* Premium loading hero: fills the message-list region with a
   centred Burofolk-branded mark + gradient title + informative
   copy. The mark is the three logo bars (cyan / violet / pink)
   doing a coordinated wave so the loader is unmistakably the
   product, not a generic spinner. Skeleton rows are hidden
   while the hero is on screen so the eye lands on one focal
   point - the brand. */
.mail-list-loading-hero {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1.5rem;
    min-height: min(60vh, 520px);
    padding: 3rem 2rem 4rem;
    text-align: center;
    background:
        radial-gradient(circle at 30% 30%,
            color-mix(in srgb, #06B6D4 6%, transparent) 0%,
            transparent 55%),
        radial-gradient(circle at 70% 60%,
            color-mix(in srgb, #EC4899 5%, transparent) 0%,
            transparent 60%),
        radial-gradient(circle at 50% 80%,
            color-mix(in srgb, #06B6D4 4%, transparent) 0%,
            transparent 60%);
}
/* Hide skeleton rows whenever the hero is on screen - the hero
   is the visual focus. Keeping the rows underneath drew the eye
   away from the brand mark and read as competing structure. */
.archive-scroll-loading .mail-row-skeleton {
    display: none;
}

/* Three-bar logo mark: 96 x 96 with the same proportions as the
   navbar brand mark. Each bar is a separate animated layer so
   they ripple in sequence. */
.mail-list-loading-mark {
    position: relative;
    width: 96px;
    height: 96px;
    flex: 0 0 96px;
    filter: drop-shadow(0 8px 24px color-mix(in srgb, var(--primary) 18%, transparent));
}
.mail-list-loading-bar {
    position: absolute;
    height: 18px;
    border-radius: 10px;
    transform-origin: left center;
    will-change: transform, opacity;
    animation: mail-list-loading-wave 1.4s cubic-bezier(.4, 0, .2, 1) infinite;
}
.mail-list-loading-bar-1 {
    top: 18px; left: 9px;
    width: 72px;
    background: color-mix(in srgb, var(--text) 18%, transparent);
    animation-delay: 0s;
}
.mail-list-loading-bar-2 {
    top: 39px; left: 3px;
    width: 72px;
    background: color-mix(in srgb, var(--text) 18%, transparent);
    animation-delay: .15s;
}
.mail-list-loading-bar-3 {
    top: 60px; left: 15px;
    width: 72px;
    background: color-mix(in srgb, var(--text) 18%, transparent);
    animation-delay: .3s;
}
@keyframes mail-list-loading-wave {
    0%, 100% {
        transform: scaleX(1);
        opacity: .9;
    }
    35% {
        transform: scaleX(1.08);
        opacity: 1;
    }
    65% {
        transform: scaleX(.55);
        opacity: .55;
    }
}

.mail-list-loading-copy {
    max-width: 32em;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .5rem;
}
.mail-list-loading-title {
    margin: 0;
    font-size: 1.25rem;
    font-weight: 600;
    letter-spacing: -0.01em;
    color: var(--text);
}
.mail-list-loading-detail {
    margin: 0;
    font-size: 13px;
    color: var(--text-muted);
    line-height: 1.55;
}
.mail-list-loading-detail [data-mail-list-retry] {
    margin-left: .65rem;
    vertical-align: middle;
}

/* Reduced-motion: keep the hero present + readable but freeze
   the bar wave so users with motion sensitivity see a static
   brand mark + copy. */
@media (prefers-reduced-motion: reduce) {
    .mail-list-loading-bar { animation: none; }
}

.mail-row.mail-row-skeleton {
    display: grid;
    grid-template-columns: 30px 22px 22px 30px minmax(120px, 200px) 1fr 80px;
    gap: 1rem;
    align-items: center;
    padding: .65rem 1.25rem;
    border-bottom: 1px solid var(--border);
    background: var(--surface);
    pointer-events: none;
    user-select: none;
}
.mail-row-skeleton .sk {
    display: block;
    background: linear-gradient(90deg,
        var(--surface-2) 0%,
        color-mix(in srgb, var(--surface-2) 50%, var(--surface)) 50%,
        var(--surface-2) 100%);
    background-size: 200% 100%;
    border-radius: 4px;
    animation: mail-skeleton-shimmer 1.6s ease-in-out infinite;
}
.mail-row-skeleton .sk-check  { width: 14px; height: 14px; border-radius: 4px; }
.mail-row-skeleton .sk-toggle { width: 14px; height: 14px; border-radius: 50%; }
.mail-row-skeleton .sk-star   { width: 14px; height: 14px; border-radius: 4px; }
.mail-row-skeleton .sk-avatar { width: 24px; height: 24px; border-radius: 50%; }
.mail-row-skeleton .sk-line   { height: 11px; border-radius: 4px; }
.mail-row-skeleton .sk-line-from    { width: 70%; }
.mail-row-skeleton .sk-line-subject { width: 85%; }
.mail-row-skeleton .sk-line-date    { width: 60px; justify-self: end; }
.mail-row-skeleton:nth-child(odd) .sk-line-from    { width: 55%; }
.mail-row-skeleton:nth-child(odd) .sk-line-subject { width: 75%; }
.mail-row-skeleton:nth-child(3n) .sk-line-from     { width: 80%; }
.mail-row-skeleton:nth-child(3n) .sk-line-subject  { width: 65%; }
@keyframes mail-skeleton-shimmer {
    0%   { background-position: 100% 0; }
    100% { background-position: -100% 0; }
}

/* The pane's row grammar, shared with every module pane (owner, 2026-09-06:
   "common appearance on different workspaces"): 32px, 13px, 0 8px, radius 6,
   an ink-wash hover (the sidebar IS the second surface). */
.mail-sidebar .list-group-item { border: 0; padding: 0 8px; background: transparent; color: var(--text); font-size: 13px; border-radius: 6px; text-decoration: none; display: flex; align-items: center; gap: 8px; min-height: 32px; }
.mail-sidebar .list-group-item:hover { background: color-mix(in srgb, var(--text) 7%, transparent); }

/* Sidebar rows (system folders + saved views) are navigational, not text.
   user-select: none prevents the text caret that otherwise appears when
   clicking the label - same issue the .account-row rule solved on the
   Accounts page. cursor: pointer is forced onto descendants (icons, count
   chips, name spans) so no child falls through to a text cursor. */
.mail-sidebar .list-group-item,
.mail-sidebar .list-group-item *,
.mail-sidebar .view-link,
.mail-sidebar .view-link * {
    -webkit-user-select: none;
    user-select: none;
    cursor: pointer !important;
}
.mail-sidebar .list-group-item.active { background: color-mix(in srgb, var(--text) 7%, transparent); color: var(--text); font-weight: 600; }
.mail-sidebar h6 { font-size: 11px; letter-spacing: .04em; color: var(--text-subtle); text-transform: uppercase; margin: 14px 0 2px; padding: 0 8px; min-height: 24px; display: flex; align-items: center; font-weight: 600; }
.mail-sidebar .list-group { display: flex; flex-direction: column; gap: 2px; }

/* Sidebar user-label row: link + hover-revealed delete trigger. The link
   keeps the same visual as before (hover, active, etc. - inherited from
   .list-group-item rules elsewhere). The delete button overlays the
   right side of the row, hidden until hover/focus so the sidebar stays
   calm at rest. */
.user-label-row {
    position: relative;
    display: flex; align-items: stretch;
}
.user-label-row .user-label-link { flex: 1 1 auto; min-width: 0; }
/* Active-state: when the sidebar's current Label matches this user
   label, paint the link with the same primary-bg + primary-color
   treatment system folders use (see .mail-sidebar .list-group-item
   .active). The active class lives on the OUTER .user-label-row so
   the row's per-label dot + delete affordance can react to it; the
   visible chrome lives on the inner .user-label-link to match the
   surrounding folder rows pixel-for-pixel. */
.user-label-row.active .user-label-link {
    background: color-mix(in srgb, var(--text) 7%, transparent);
    color: var(--text);
    font-weight: 600;
}
.user-label-row.active .user-label-link .sb-name { color: var(--text); }
.user-label-row.active .user-label-link .sb-count { color: var(--primary); }
.user-label-delete {
    position: absolute;
    top: 50%; right: 6px;
    transform: translateY(-50%);
    width: 22px; height: 22px;
    display: inline-flex; align-items: center; justify-content: center;
    border: 0;
    background: transparent;
    color: var(--text-muted);
    border-radius: 6px;
    cursor: pointer;
    opacity: 0;
    transition: opacity .12s ease, background .12s ease, color .12s ease;
}
/* Trash button visibility: shown only when the user is hovering
   the count badge of the SELECTED (.active) label. Two triggers
   keep the swap stable while the cursor is over the right-hand
   slot: hovering the count itself, OR hovering the trash once it
   has appeared (so the cursor sitting in the same spot keeps
   showing it instead of flickering). Inactive labels never reveal
   the trash on hover. Keyboard focus keeps the affordance
   accessible. */
.user-label-row.active:has(.sb-count:hover, .user-label-delete:hover) .user-label-delete,
.user-label-delete:focus-visible { opacity: 1; }
.user-label-delete:hover {
    background: color-mix(in srgb, #b91c1c 12%, transparent);
    color: #b91c1c;
}
.user-label-delete:focus-visible {
    outline: 2px solid #b91c1c;
    outline-offset: 1px;
}
/* Fade the count when the user hovers it (or the trash once
   it has appeared) on the SELECTED label - the trash button
   takes the count's slot during the hover so we hide the
   number to avoid double-stacking glyphs.

   Gated on the row ACTUALLY having a .user-label-delete child:
   read-only workspaces and non-owner views skip the delete button
   entirely, and fading the count there leaves an empty slot with
   nothing to swap in (the original "vanish on hover" bug). The
   chained :has() ensures the rule only fires when there's a real
   trash icon waiting to take over the slot. */
.user-label-row.active:has(.user-label-delete):has(.sb-count:hover, .user-label-delete:hover) .user-label-link .sb-count { opacity: 0; }
/* Pending state while the server processes the delete. */
.user-label-row.is-deleting { opacity: .5; pointer-events: none; }
.mail-sidebar .label-dot { width: 10px; height: 10px; border-radius: 4px; flex: 0 0 auto; margin: 0; }
.mail-sidebar .label-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1 1 auto; min-width: 0; }
.mail-sidebar .list-group-item > span:first-child { flex: 0 0 auto; }
.mail-sidebar .list-group-item .sb-icon {
    flex: 0 0 auto;
    width: 15px; height: 15px;
    color: var(--text-muted);
}
.mail-sidebar .list-group-item:hover .sb-icon,
.mail-sidebar .list-group-item.has-unread .sb-icon {
    color: var(--text);
}
.mail-sidebar .list-group-item.active .sb-icon { color: var(--text); }
/* Common vocabulary (owner, 2026-09-06: "always use red icon for trash"): the
   trash glyph is the danger red in every state, as in Documents. */
.mail-sidebar .list-group-item[data-folder="Trash"] .sb-icon,
.mail-sidebar .list-group-item[data-folder="Trash"]:hover .sb-icon,
.mail-sidebar .list-group-item.active[data-folder="Trash"] .sb-icon { color: var(--danger); }
/* And the favourite is the favourite's filled yellow everywhere ("always use
   the same colour for favourite, everywhere"): the starred folder's star. */
.mail-sidebar .list-group-item[data-folder="Starred"] .sb-icon,
.mail-sidebar .list-group-item[data-folder="Starred"]:hover .sb-icon,
.mail-sidebar .list-group-item.active[data-folder="Starred"] .sb-icon { color: var(--hue-star); fill: var(--hue-star); }
/* Every folder glyph wears its colour of the palette, in every state
   ("colour selection for all icons… like on iPad"; the ink-until-selected
   rule "is only a rule of the left icons of the left menu"): the mailbox
   blue, sent green, drafts gray, important orange, spam brown, the whole
   tray indigo. */
.mail-sidebar .list-group-item[data-folder="AllMail"] .sb-icon,
.mail-sidebar .list-group-item[data-folder="AllMail"]:hover .sb-icon,
.mail-sidebar .list-group-item.active[data-folder="AllMail"] .sb-icon { color: var(--sys-indigo); }
.mail-sidebar .list-group-item[data-folder="Inbox"] .sb-icon,
.mail-sidebar .list-group-item[data-folder="Inbox"]:hover .sb-icon,
.mail-sidebar .list-group-item.active[data-folder="Inbox"] .sb-icon { color: var(--sys-blue); }
.mail-sidebar .list-group-item[data-folder="Sent"] .sb-icon,
.mail-sidebar .list-group-item[data-folder="Sent"]:hover .sb-icon,
.mail-sidebar .list-group-item.active[data-folder="Sent"] .sb-icon { color: var(--sys-green); }
.mail-sidebar .list-group-item[data-folder="Drafts"] .sb-icon,
.mail-sidebar .list-group-item[data-folder="Drafts"]:hover .sb-icon,
.mail-sidebar .list-group-item.active[data-folder="Drafts"] .sb-icon { color: var(--sys-gray); }
.mail-sidebar .list-group-item[data-folder="Important"] .sb-icon,
.mail-sidebar .list-group-item[data-folder="Important"]:hover .sb-icon,
.mail-sidebar .list-group-item.active[data-folder="Important"] .sb-icon { color: var(--sys-orange); }
.mail-sidebar .list-group-item[data-folder="Spam"] .sb-icon,
.mail-sidebar .list-group-item[data-folder="Spam"]:hover .sb-icon,
.mail-sidebar .list-group-item.active[data-folder="Spam"] .sb-icon { color: var(--sys-brown); }
/* The team's views: mentions pink, mine purple, waiting on me teal, unassigned cyan. */
.mail-sidebar .list-group-item[data-folder="MentionsMe"] .sb-icon,
.mail-sidebar .list-group-item[data-folder="MentionsMe"]:hover .sb-icon,
.mail-sidebar .list-group-item.active[data-folder="MentionsMe"] .sb-icon { color: var(--sys-pink); }
.mail-sidebar .list-group-item[data-folder="MyAssigned"] .sb-icon,
.mail-sidebar .list-group-item[data-folder="MyAssigned"]:hover .sb-icon,
.mail-sidebar .list-group-item.active[data-folder="MyAssigned"] .sb-icon { color: var(--sys-purple); }
.mail-sidebar .list-group-item[data-folder="WaitingOnMe"] .sb-icon,
.mail-sidebar .list-group-item[data-folder="WaitingOnMe"]:hover .sb-icon,
.mail-sidebar .list-group-item.active[data-folder="WaitingOnMe"] .sb-icon { color: var(--sys-teal); }
.mail-sidebar .list-group-item[data-folder="TeamUnassigned"] .sb-icon,
.mail-sidebar .list-group-item[data-folder="TeamUnassigned"]:hover .sb-icon,
.mail-sidebar .list-group-item.active[data-folder="TeamUnassigned"] .sb-icon { color: var(--sys-cyan); }

/* A conversation's team status, drawn by ONE renderer (MailStatusGlyph,
   #596): the open circle, the pending clock, the closed check - the
   message page's own glyphs, in the status's colour. Never a coloured dot
   (design charter). Never animated: a status is a fact, not an alarm. */
.mail-status-glyph {
    display: inline-flex; align-items: center; justify-content: center;
    width: 14px; height: 14px;
    flex: 0 0 auto;
    color: var(--text-muted);
}
.mail-status-glyph svg { display: none; width: 14px; height: 14px; }
.mail-status-glyph[data-status="open"] svg[data-glyph="open"],
.mail-status-glyph[data-status="pending"] svg[data-glyph="pending"],
.mail-status-glyph[data-status="closed"] svg[data-glyph="closed"] { display: block; }
.mail-status-glyph[data-status="pending"] { color: var(--warning); }
.mail-status-glyph[data-status="closed"] { color: var(--success); }
/* The menus' "Status" trigger: the glyph in the label's own ink. */
.mail-status-glyph.is-plain { color: currentColor; }

/* Per-row team status: the glyph and its word, in its colour. Drawn only
   for a conversation somebody parked, and never inside its own status
   view, so the mark means "this one is different" rather than decorating
   every line. */
.mail-row-status {
    display: inline-flex; align-items: center; gap: 4px;
    flex: 0 0 auto;
    margin-left: 8px;
    font-size: 12px; font-weight: 500; line-height: 1;
    white-space: nowrap;
}
.mail-row-status[data-status="pending"] { color: var(--warning); }
.mail-row-status[data-status="closed"] { color: var(--success); }

/* The sidebar's triage scopes wear the same glyph as the rows they
   open, so the mark on a row and the entry that lists it are visibly
   the same fact. Sized to sit where a folder glyph would. */
.mail-sidebar .sb-status-glyph,
.mail-sidebar .sb-status-glyph svg { width: 15px; height: 15px; }

/* Folders holding strictly nothing.
   The server marks them; they collapse from frame one so the eye
   never has to read past "Starred 0" to find the rows that matter.
   !important is load-bearing: these rows carry Bootstrap's .d-flex
   (display:flex!important), which any ordinary rule loses to.
   Nothing is removed - .show-empty-folders on the sidebar brings the
   whole group back, and the pre-paint guard restores that choice
   before the first frame. */
.mail-sidebar .sb-folder-empty { display: none !important; }
.mail-sidebar.show-empty-folders .sb-folder-empty { display: flex !important; }

/* The reveal itself: a quiet row, deliberately lighter than a folder
   so it reads as a control rather than a destination. */
.mail-sidebar .sb-empty-toggle {
    display: flex; align-items: center; width: 100%;
    background: none; border: 0; text-align: left;
    color: var(--text-muted);
    font-size: 11px;
}
.mail-sidebar .sb-empty-toggle:hover { color: var(--text); }
.mail-sidebar .sb-empty-toggle-chevron { transition: transform .15s ease; }
.mail-sidebar.show-empty-folders .sb-empty-toggle-chevron { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) {
    .mail-sidebar .sb-empty-toggle-chevron { transition: none; }
}
/* One label per state, both in the markup: CSS picks the live one so
   the button reads correctly in frame one even when the pre-paint
   guard restored "shown" before this element existed. */
.mail-sidebar .sb-empty-toggle-hide { display: none; }
.mail-sidebar.show-empty-folders .sb-empty-toggle-show { display: none; }
.mail-sidebar.show-empty-folders .sb-empty-toggle-hide { display: block; }

/* Sidebar label + count layout
   Name takes the middle, count hugs the right edge.
   The count is a subtle tabular-number pill - low visual weight when
   the item is inactive, crisp primary-tinted when the item is active,
   and primary-filled on hover so it feels like part of the same chip. */
.mail-sidebar .sb-name {
    flex: 1 1 auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.mail-sidebar .sb-count {
    flex: 0 0 auto;
    /* The pane's figure grammar (every module pane): 12px, tabular, muted,
       no fill - a surface-2 pill vanished on the surface-2 well. The
       has-unread variant below still speaks in the primary tone. */
    font-size: 12px; font-weight: 500;
    color: var(--text-subtle);
    background: transparent;
    padding: 0;
    border-radius: 0;
    font-variant-numeric: tabular-nums;
    letter-spacing: .01em;
    transition: background .12s ease, color .12s ease;
    margin-left: auto;
}
/* Lazy folder counts fade in when they land rather than snapping
   into place, so the sidebar settles smoothly after the counts
   fetch. The count is right-aligned (margin-left:auto), so this only
   softens the number's appearance - it never shifts the row. The
   animation runs once, when the badge first gains a value. */
.mail-sidebar .sb-count:not(:empty) {
    animation: sbCountFadeIn .18s ease-out;
}
@keyframes sbCountFadeIn { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
    .mail-sidebar .sb-count:not(:empty) { animation: none; }
}
.mail-sidebar .list-group-item:hover .sb-count {
    color: var(--text);
}
.mail-sidebar .list-group-item.active .sb-count {
    background: transparent;
    color: var(--primary);
}

/* Unread accent. A folder with unread messages gets the count pill in
   the primary color so it stands out without an extra badge. The dot in
   front of the name is intentionally small - the goal is to read at a
   glance, not to scream. */
.mail-sidebar .list-group-item.has-unread .sb-name { font-weight: 600; color: var(--text); }
.mail-sidebar .list-group-item.has-unread .sb-count {
    background: var(--primary-bg);
    color: var(--primary);
}
.mail-sidebar .list-group-item.has-unread.active .sb-count { background: transparent; color: var(--primary); }

/* Optimistic-loading state: applied the moment the user clicks a
   sidebar row, removed when the soft-nav swap renders the new view.
   Visual contract:
     1. .active is applied in the same frame so the new selection is
        immediately legible.
     2. .is-loading hides the count chip and replaces it with an
        inline spinner anchored in the same right-side slot, so the
        row width does not shift.
     3. The spinner sits inside the row's existing right padding -
        absolute-positioned spinners that overflow would re-introduce
        the horizontal-scrollbar bug fixed earlier.
*/
.mail-sidebar .list-group-item.is-loading .sb-count,
.archive-sidebar .view-item.is-loading .view-count {
    visibility: hidden;
}
.mail-sidebar .list-group-item.is-loading,
.archive-sidebar .view-item.is-loading {
    position: relative;
}
.mail-sidebar .list-group-item.is-loading::after,
.archive-sidebar .view-item.is-loading::after {
    content: "";
    position: absolute;
    top: 50%;
    right: .9rem;
    width: 14px;
    height: 14px;
    margin-top: -7px;
    border: 2px solid var(--primary);
    border-right-color: transparent;
    border-radius: 50%;
    animation: bf-sidebar-spin .7s linear infinite;
    pointer-events: none;
}
@keyframes bf-sidebar-spin {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
}

/* Premium fragment-loading state: applied to the mail-list region
   the instant the user clicks a sidebar row, removed when the
   soft-nav swap renders the new fragment. The list visibly fades +
   slightly blurs so the user understands "this content is being
   replaced". Pointer events are disabled so a click on a stale row
   can't fire while the new view is loading. The spinner on the
   target sidebar row stays at full opacity above the haze - it is
   the visual focus.

   Performance: GPU-accelerated `filter` + `opacity` transitions, no
   layout reflow, and the blur radius is small (1.5px) so we don't
   trigger expensive backdrop-filter compositing on slow tablets. */
[data-mail-pane-region="main"] {
    transition: filter .18s ease, opacity .18s ease;
}
[data-mail-pane-region="main"].is-fragment-loading {
    filter: blur(1.5px) saturate(.85);
    opacity: .55;
    pointer-events: none;
    cursor: progress;
    user-select: none;
}
/* Just-rendered fade-in: a short, subtle settle animation that
   plays once when the new fragment lands. The previous frame had
   the region blurred + at .55 opacity; once we drop that class the
   new content is at full opacity instantly. To smooth the snap we
   replay a tiny opacity + Y-translate keyframe. Keep it short and
   eased - too long reads as "still loading" and defeats the point. */
[data-mail-pane-region="main"].is-just-rendered {
    animation: bf-fragment-settle .32s cubic-bezier(.2, .7, .2, 1) both;
    will-change: transform, opacity;
}
@keyframes bf-fragment-settle {
    0%   { opacity: .55; transform: translateY(2px); }
    100% { opacity: 1;   transform: translateY(0); }
}
/* Respect reduced-motion: skip the transition + drop the blur in
   favor of opacity only, which still communicates the disabled
   state without a visual disturbance. */
@media (prefers-reduced-motion: reduce) {
    [data-mail-pane-region="main"] { transition: opacity .18s ease; }
    [data-mail-pane-region="main"].is-fragment-loading {
        filter: none;
    }
    [data-mail-pane-region="main"].is-just-rendered {
        animation: none;
    }
}

/* Sidebar-row hover tooltip removed.
   The previous implementation positioned a :hover::after pseudo-
   element absolutely at left: calc(100% + 8px) - extending past
   the .archive-sidebar's 260px width. Combined with overflow-y:
   auto on the sidebar (which makes overflow-x compute to auto,
   not visible), this triggered a horizontal scrollbar on every
   row hover. Native title attributes carry the same per-row
   info ("Threads currently assigned to you", count breakdowns)
   without any CSS overflow side-effects. */
/* Preference: hide sidebar counts. Set via body.hide-label-counts
   from Burofolk.prefs.apply(). No layout shift - the row just loses
   its right-hand chip. */
body.hide-label-counts .mail-sidebar .sb-count { display: none; }
/* Sidebar count badges are now always rendered (even at 0) so live updates
   from star/read toggles can mutate the same node without inserting/removing
   DOM. Hide the empty case so a 0-count folder reads identically to the
   old conditional render. */
.mail-sidebar .sb-count:empty { display: none; }

.sidebar-nav .nav-item {
    display: flex; align-items: center; gap: .6rem;
    padding: .45rem .75rem;
    font-size: 12px;
    color: var(--text);
    border-radius: 6px;
    text-decoration: none;
    cursor: pointer;
}
.sidebar-nav .nav-item:hover { background: var(--surface-2); }
.sidebar-nav .nav-item.active { background: color-mix(in srgb, var(--text) 7%, transparent); color: var(--text); font-weight: 600; }
.sidebar-heading { font-size: 10px; letter-spacing: .08em; color: var(--text-subtle); text-transform: uppercase; margin: 1rem .75rem .4rem; font-weight: 600; }
.label-dot { display: inline-block; width: 10px; height: 10px; border-radius: 4px; flex: 0 0 auto; }

/* ── Canonical label token (.bf-label) ───────────────────────────────
   ONE way to draw a label everywhere: a solid per-label colour dot +
   the name on a transparent background. No pill, no fill. The hue is the
   single LabelPalette.ColorFor (C#) / Burofolk.labelColor (JS) value,
   written inline as --bf-label-color and shared by the dot and (for
   suggestions) the outline. States layer on as modifiers:
     .is-suggested → dotted, hue-tinted outline = an AI proposal that has
                     NOT been applied yet (the only bordered variant; a
                     plain applied label has no border, matching the
                     sidebar and the mail-row rail). */
.bf-label {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    min-width: 0;
    max-width: 100%;
    line-height: 1.3;
    /* The token is sometimes an <a> (filter-by-label); never render it as a
       default underlined link - the dot + name carry the meaning. */
    text-decoration: none;
    color: inherit;
}
/* When the whole token is a filter link (mail-row rail), signal it on hover
   + keyboard focus without shouting. */
a.bf-label { cursor: pointer; border-radius: 6px; }
a.bf-label:hover .bf-label-name { color: var(--text); }
a.bf-label:focus-visible,
a.bf-label-name:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
    border-radius: 4px;
}
.bf-label-dot {
    width: 9px; height: 9px;
    border-radius: 2px;
    background: var(--bf-label-color, var(--text-muted));
    flex: 0 0 auto;
    /* Optical centring: label names are usually all-caps and sit high in
       their line box (no descenders fill the lower half), so a box-centred
       dot reads as too low. Lift it a hair to sit level with the letters. */
    transform: translateY(-0.5px);
}
.bf-label-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 11px;
    font-weight: 500;
    color: var(--text);
    text-decoration: none;
    letter-spacing: .005em;
}
a.bf-label-name:hover { color: var(--text); }
/* A SUGGESTED label (an AI prediction not yet applied) must read as
   clearly different from a solid applied label. It gets a full-hue dotted
   outline AND hue-tinted name (coloured text on transparent, per the
   status-label rule) - never the plain neutral look of an applied label.
   Only the classifier suggest chip emits .is-suggested; applied labels
   (rail, message detail) never carry it, so they stay solid + neutral. */
.bf-label.is-suggested {
    padding: .14rem .5rem;
    border: 1.5px dotted var(--bf-label-color, var(--primary));
    border-radius: 8px;
}
.bf-label.is-suggested .bf-label-dot { display: none; }
.bf-label.is-suggested .bf-label-name {
    color: var(--bf-label-color, var(--primary));
    font-weight: 600;
}
/* Inline × on a removable applied label (message detail). Quiet by
   default; lifts on hover. Transparent - never a filled control. */
.bf-label-remove {
    display: inline-flex; align-items: center; justify-content: center;
    width: 16px; height: 16px;
    margin-left: .05rem;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--text-subtle);
    cursor: pointer;
    border-radius: 4px;
    opacity: .55;
    transition: opacity .12s ease, color .12s ease, background .12s ease;
}
.bf-label-remove:hover { opacity: 1; color: var(--text); background: var(--surface-2); }
.bf-label-remove:focus-visible { opacity: 1; outline: 2px solid var(--primary); outline-offset: 1px; }
.bf-label-remove svg { display: block; }
.bf-label.is-removing { opacity: .5; pointer-events: none; }

/* Mail row */
.mail-row {
    display: grid;
    /* Left cluster: checkbox | read-stripe | star - zero gap so each control
       owns its pixels. Content takes everything else as a two-line block:
       Line 1: sender ................................... date
       Line 2: subject · muted snippet preview ........................
       This is the Superhuman / Apple Mail pattern - scannable because each
       row has predictable visual anchors instead of one long text river.
       Left padding gives the checkbox breathing room from the row edge; the
       checkbox column itself widens a little so the label can fully fill it
       and own its pixels for clicks. */
    grid-template-columns: 36px 22px 32px minmax(0, 1fr);
    column-gap: 0;
    align-items: center;
    padding: .7rem 1.25rem .7rem .9rem;
    border-bottom: 1px solid var(--surface-2);
    text-decoration: none;
    color: inherit;
    cursor: default;
    transition: background .08s ease;
}
.mail-row > .mail-content {
    display: flex;
    flex-direction: column;
    gap: 3px;
    min-width: 0;
    /* Bigger left gap so the text doesn't crowd the star. The text area is
       also the only "open the message" hit zone on the row - cursor flips
       to pointer here, and stays default everywhere else (checkbox, read
       stripe, star) so users can target each control without surprises. */
    padding-left: .85rem;
    padding-right: .25rem;
    cursor: pointer;
}
.mail-line-1 {
    display: flex;
    align-items: baseline;
    gap: .75rem;
    min-width: 0;
}
.mail-line-1 > .mail-from {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: baseline;
    gap: .4rem;
    overflow: hidden;
}
.mail-line-1 > .mail-date {
    flex: 0 0 auto;
}
/* "Open in new tab" icon on each row. Invisible by default so the list
   stays clean; fades in on row hover so it's discoverable exactly when
   the user is considering that row. Clicks on it don't bubble up to the
   row's own click handler - so you don't also get a same-tab navigation
   happening under the new-tab action. */
.mail-line-1 > .mail-open-tab {
    flex: 0 0 auto;
    display: inline-flex; align-items: center; justify-content: center;
    width: 22px; height: 22px;
    border-radius: 6px;
    color: var(--text-muted);
    text-decoration: none;
    opacity: 0;
    transition: opacity .12s ease, background .12s ease, color .12s ease;
}
.mail-row:hover .mail-line-1 > .mail-open-tab { opacity: .7; }
.mail-line-1 > .mail-open-tab:hover {
    opacity: 1;
    background: color-mix(in srgb, var(--text) 5%, transparent);
    color: var(--text);
}
.mail-line-1 > .mail-open-tab:focus-visible {
    opacity: 1;
    outline: 2px solid var(--primary);
    outline-offset: 1px;
}
.mail-line-1 > .mail-open-tab svg { display: block; }
.mail-row.selected {
    background: var(--primary-bg) !important;
    box-shadow: inset 3px 0 0 var(--primary);
}

/* Custom checkbox in the row's first column. The label fills the entire
   grid cell (full width, full row height) so any click that lands in the
   checkbox column toggles selection - no narrow hit-target, no accidental
   row-open from clicking just beside the visual square. The 17px box still
   reads as a small precise checkbox; the surrounding label is invisible
   but absorbs the clicks. */
.mail-select {
    display: flex; align-items: center; justify-content: center;
    cursor: pointer; margin: 0;
    width: 100%;
    min-height: 36px;
    align-self: stretch;
    opacity: .4;
    transition: opacity .15s ease;
    -webkit-user-select: none;
    user-select: none;
}
.mail-row:hover .mail-select,
.mail-row.selected .mail-select,
body.has-selection .mail-select { opacity: 1; }
.mail-checkbox { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }
.mail-check-box {
    display: inline-block; width: 17px; height: 17px;
    border: 1.5px solid var(--border-strong); border-radius: 6px;
    background: var(--surface);
    position: relative;
    transition: background .12s ease, border-color .12s ease, box-shadow .12s ease, transform .1s ease;
}
.mail-select:hover .mail-check-box {
    border-color: var(--primary);
    box-shadow: 0 0 0 3px var(--primary-bg);
}
.mail-checkbox:focus-visible + .mail-check-box {
    border-color: var(--primary);
    box-shadow: 0 0 0 3px rgba(79, 70, 229, .25);
}
.mail-checkbox:checked + .mail-check-box {
    background: var(--primary);
    border-color: var(--primary);
    box-shadow: 0 2px 6px -1px rgba(79, 70, 229, .45);
}
.mail-select:active .mail-check-box { transform: scale(.92); }
.mail-checkbox:checked + .mail-check-box::after {
    content: ""; position: absolute;
    left: 4.5px; top: 1px;
    width: 5px; height: 10px;
    border-right: 2.2px solid #fff;
    border-bottom: 2.2px solid #fff;
    transform: rotate(42deg);
    border-radius: 1px;
}

/* Thread expand button - a clickable version of the count pill. Same
   visual weight as the static pill so its presence doesn't shout; the
   hover state signals interactivity. */
.thread-expand {
    display: inline-flex; align-items: center; gap: 3px;
    flex: 0 0 auto;
    background: var(--surface-2); border: 0; cursor: pointer;
    padding: 1px 6px 1px 7px; height: 18px;
    border-radius: 999px;
    color: var(--text-muted);
    font: inherit; font-size: 10px; font-weight: 700; line-height: 1;
    transition: background .12s ease, color .12s ease;
}
.thread-expand:hover { background: color-mix(in srgb, var(--text) 5%, transparent); color: var(--text); }
.thread-expand .thread-count {
    /* Inside the button - neutralize the standalone pill styling so the
       whole button reads as a single chip. */
    background: transparent; color: inherit;
    font-size: inherit; font-weight: inherit;
    padding: 0; margin: 0; line-height: 1;
}
.thread-expand:hover .thread-count { background: transparent; color: inherit; }
.thread-expand .thread-chev { transition: transform .15s ease; opacity: .8; }
.mail-row.expanded .thread-expand { background: var(--primary-bg); color: var(--primary); }
.mail-row.expanded .thread-expand .thread-chev { transform: rotate(180deg); opacity: 1; }
.mail-row.unread .thread-expand { background: var(--primary-bg); color: var(--primary); opacity: .85; }

/* Thread children - the conversation's messages under its list row, as ONE
   strand (owner, 2026-10-09: the old grid of fixed columns bit the subject to
   one letter in a narrow list and repeated the assignee on every row). A
   hairline guide under the parent's sender column; each row: the direction
   glyph (received / sent), the sender (the ONE strong element), what they
   said (muted, takes the room), the marks, the date. Unread reads by weight,
   never a dot. Under 520px of list the snippet takes its own line. */
.thread-children {
    container-type: inline-size;
    position: relative;
    padding: 4px 10px 8px 56px;
    border-bottom: 1px solid var(--border);
    background: var(--surface);
}
.thread-children::before {
    content: "";
    position: absolute;
    left: 46px;
    top: 6px;
    bottom: 12px;
    width: 1px;
    background: var(--border);
}
.thread-child {
    display: grid;
    grid-template-columns: 16px minmax(0, max-content) minmax(0, 1fr) auto auto;
    grid-template-areas: "dir from snip marks date";
    column-gap: 10px;
    align-items: center;
    min-height: 34px;
    padding: 4px 10px;
    border-radius: 8px;
    color: var(--text);
    text-decoration: none;
}
.thread-child:hover { background: var(--surface-2); color: var(--text); }
.thread-child.is-reading {
    background: color-mix(in srgb, var(--text) 7%, transparent);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 10%, transparent);
}
.thread-child.is-folded { display: none; }
.thread-child .tc-dir { grid-area: dir; display: inline-flex; color: var(--text-subtle); }
.thread-child .tc-dir svg { width: 13px; height: 13px; }
.thread-child.is-out .tc-dir { color: var(--info); }
.thread-child .tc-from {
    grid-area: from;
    max-width: 22ch;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-size: 12px; font-weight: 500; color: var(--text);
}
.thread-child .tc-snippet {
    grid-area: snip;
    min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-size: 12px; color: var(--text-muted);
}
.thread-child.unread .tc-from { font-weight: 700; }
.thread-child.unread .tc-snippet { color: var(--text); }
.thread-child .tc-marks { grid-area: marks; display: inline-flex; align-items: center; gap: 6px; }
.thread-child .tc-marks:empty { display: none; }
.thread-child .tc-attach { display: inline-flex; color: var(--text-muted); }
.thread-child .tc-attach svg { width: 12px; height: 12px; }
.thread-child .tc-draft { font-size: 11px; font-weight: 600; color: var(--text); }
.thread-child .tc-date { grid-area: date; font-size: 11px; color: var(--text-muted); white-space: nowrap; font-variant-numeric: tabular-nums; text-align: right; }
.thread-children-more {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    height: 30px;
    margin: 2px 0 2px 2px;
    padding: 0 10px 0 8px;
    border: 0;
    border-radius: 8px;
    background: transparent;
    font: inherit;
    font-size: 12px;
    font-weight: 600;
    color: var(--text-muted);
    cursor: pointer;
}
.thread-children-more:hover { background: var(--surface-2); color: var(--text); }
.thread-children-more svg { width: 14px; height: 14px; flex: 0 0 auto; }
@container (max-width: 520px) {
    .thread-child {
        grid-template-columns: 16px minmax(0, 1fr) auto auto;
        grid-template-areas: "dir from marks date" ". snip snip snip";
        row-gap: 2px;
        padding: 6px 8px;
    }
    .thread-child .tc-from { max-width: none; }
}
/* Zebra-stripe + fixed-height hover (no CLS) */
.mail-row:nth-child(even) { background: #f3f5fb; }
.mail-row:nth-child(odd) { background: var(--surface); }
/* Dark-mode list - standard mail-client pattern: uniform row background,
   thin separators, unread signaled by the left stripe + bold text that
   already live on .read-btn / .mail-row.unread. No zebra, no bg tint. */
html[data-theme="dark"] .mail-row,
html[data-theme="dark"] .mail-row:nth-child(odd),
html[data-theme="dark"] .mail-row:nth-child(even) {
    background: var(--surface);
}
html[data-theme="dark"] .mail-row { border-bottom: 1px solid rgba(148, 163, 184, .08); }
html[data-theme="dark"] .mail-row:hover {
    background: #182133 !important;
}
.mail-row:hover {
    background: color-mix(in srgb, var(--text) 5%, transparent);
}
.mail-row:hover .mail-snippet { color: var(--text); }
.mail-row:hover .mail-from { color: var(--text); }
.mail-row:hover .mail-date { color: var(--text); font-weight: 500; }

/* The sender names of a row are part of the row's door: they never answer
   hover (a card rising under a passing cursor stole the clicks meant for
   the rows below). The people's card has ONE door, the button after them. */
.mp-addr { display: inline; color: inherit; }

/* The row's people button: its slot always stands (zero shift), its glyph
   shows on the row's hover, its focus, or while its card is open. */
.mail-people-btn {
    flex: 0 0 auto;
    align-self: center;
    display: inline-flex; align-items: center; justify-content: center;
    width: 24px; height: 24px;
    margin: -3px 0 -3px -2px;
    padding: 0;
    border: 1px solid transparent;
    border-radius: 6px;
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
    opacity: 0;
    transition: opacity .12s ease, background .12s ease, border-color .12s ease, color .12s ease;
}
.mail-people-btn svg { display: block; }
.mail-row:hover .mail-people-btn,
.mail-row:focus-within .mail-people-btn { opacity: .75; }
.mail-people-btn:hover {
    opacity: 1;
    color: var(--text);
    border-color: color-mix(in srgb, var(--text) 18%, transparent);
    background: var(--surface);
}
.mail-people-btn.is-open {
    opacity: 1;
    color: var(--text);
    background: color-mix(in srgb, var(--text) 7%, transparent);
    border-color: transparent;
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 10%, transparent);
}
.mail-people-btn:focus-visible {
    opacity: 1;
    outline: 2px solid var(--primary);
    outline-offset: 1px;
}
@media (hover: none) {
    .mail-people-btn { opacity: .6; }
}

/* The people's card: one line per person (glyph plate, ONE strong name,
   the address muted), its verbs at the line's end. Appended to <body>
   so it escapes the list's overflow; inert until shown. */
.mp-addr-card {
    position: fixed;
    z-index: 1080;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    box-shadow: 0 12px 28px rgba(15, 23, 42, .14), 0 2px 6px rgba(15, 23, 42, .08);
    padding: 6px;
    min-width: 300px; max-width: min(400px, calc(100vw - 16px));
    display: flex; flex-direction: column; gap: 2px;
    opacity: 0; transform: translateY(-2px); pointer-events: none;
    transition: opacity .12s ease, transform .12s ease;
}
.mp-addr-card.visible {
    opacity: 1; transform: translateY(0); pointer-events: auto;
}
.mp-addr-card-person {
    display: flex; align-items: center; gap: 10px;
    padding: 8px 8px 8px 8px;
    border-radius: 8px;
    min-width: 0;
}
.mp-addr-card.is-many .mp-addr-card-person:hover {
    background: color-mix(in srgb, var(--text) 4%, transparent);
}
.mp-addr-card-avatar {
    flex: 0 0 auto;
    width: 32px; height: 32px;
    display: inline-flex; align-items: center; justify-content: center;
    border-radius: 50%;
    background: color-mix(in srgb, var(--primary) 18%, var(--surface-2));
    color: var(--primary);
    font-weight: 700; font-size: 13px;
    text-transform: uppercase;
}
.mp-addr-card-text { flex: 1 1 auto; min-width: 0; }
.mp-addr-card-name {
    font-size: 13px; font-weight: 600; color: var(--text);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.mp-addr-card-email {
    font-size: 12px; color: var(--text-muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    user-select: text;
}
.mp-addr-card-actions {
    flex: 0 0 auto;
    display: flex; gap: 6px; align-items: center;
}
.mp-addr-card-action {
    flex: 0 0 auto;
    display: inline-flex; align-items: center; justify-content: center;
    width: 30px; height: 30px; padding: 0;
    background: transparent;
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    cursor: pointer;
    transition: background .12s ease, color .12s ease, border-color .12s ease;
}
.mp-addr-card-action:hover {
    background: color-mix(in srgb, var(--text) 5%, transparent);
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
}
.mp-addr-card-action svg { flex: 0 0 auto; display: block; }
/* One person: writing to them is THE verb, in ink. */
.mp-addr-card:not(.is-many) .mp-addr-card-send {
    background: var(--text);
    border-color: var(--text);
    color: var(--surface, #fff);
}
.mp-addr-card:not(.is-many) .mp-addr-card-send:hover {
    background: color-mix(in srgb, var(--text) 84%, var(--surface));
}
.mp-addr-card-action.is-copied {
    background: color-mix(in srgb, #10b981 16%, var(--surface));
    border-color: #10b981;
    color: #047857;
}

.mail-flag { text-align: center; font-size: 14px; line-height: 1; }
.mail-flag .star { color: #eab308; }
.important-marker {
    display: inline-block;
    color: #eab308;
    font-weight: 700;
    font-size: 11px;
    margin-right: .3rem;
    transform: translateY(-1px);
    opacity: .9;
}
/* ── Two-line row typography ─────────────────────────────────────────
   Line 1 (sender + date): sender leads the eye down the list; date
     anchors the right edge so you can scan chronology at a glance.
   Line 2 (subject + snippet): subject is primary content, snippet is
     a recessed preview in muted color and slightly smaller.
   Unread state bumps weight + saturation on line 1's sender and line
   2's subject so unread rows feel heavier without changing layout. */
.mail-from {
    font-weight: 600;
    color: var(--text);
    font-size: 13px;
    letter-spacing: -.005em;
    min-width: 0;
}
.mail-from-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
    /* flex: 0 1 auto - name takes its natural width and shrinks under
       pressure (ellipsis). Crucially, it does NOT grow, so the thread-
       expand pill stays pinned right next to the name instead of being
       flung to the far right of the line. */
    flex: 0 1 auto;
}
/* Outbound row attribution: tiny canonical avatar before the
   sender label so a glance at the inbox tells the operator
   which teammate sent each thread. The wrapper is flex-shrinkable
   along with its sibling name so the row still ellipses cleanly
   on narrow viewports. Avatar paint comes from Html.UserAvatar
   (the platform-wide single source of truth - no one-off
   gradients here). */
.mail-from-attrib {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    min-width: 0;
    flex: 0 1 auto;
    overflow: hidden;
}
.mail-from-attrib-avatar {
    width: 18px !important; height: 18px !important;
    font-size: 9px !important; line-height: 18px !important;
    border-radius: 50% !important;
    flex: 0 0 auto;
}
.mail-from .thread-expand,
.mail-from .thread-count,
.mail-from .important-marker { flex: 0 0 auto; }
.mail-from-link { color: inherit; text-decoration: none; }
.mail-from-link:hover { color: var(--text); text-decoration: underline; }

.mail-body {
    display: flex;
    align-items: center;
    gap: .85rem;
    font-size: 12px;
    min-width: 0;
    line-height: 1.4;
}
/* Subject + snippet run on one clipped line. The label rail (when present)
   sits to its right and never shrinks, so labels read as a stable column
   beneath the date while the text ellipses to make room. */
.mail-body-text {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.mail-subject {
    color: var(--text);
    font-weight: 500;
}
.mail-snippet {
    color: var(--text-muted);
    margin-left: .5rem;
    font-weight: 400;
    opacity: .78;
}
/* Subtle dot separator between subject and snippet - lighter than an
   em-dash, just enough punctuation to say "this is where the preview
   starts". */
.mail-snippet::before {
    content: '·';
    display: inline-block;
    margin-right: .45rem;
    color: var(--border-strong, #cbd5e1);
    opacity: .7;
    transform: translateY(-1px);
}

/* Per-row label rail. Lives on line 1, in the open space after the sender
   names (line 2 is the subject + snippet). Hosts the canonical .bf-label
   tokens, compacted + muted for the dense list: the hue lives only on the
   dot, the name is calm muted text, no pill/fill/border. The cluster keeps
   its size (flex 0 0 auto) and the sender name ellipses first when the row
   gets tight, so labels stay legible. */
.mail-labels {
    flex: 0 0 auto;
    align-self: center;
    display: inline-flex;
    align-items: center;
    gap: .7rem;
    margin-left: .35rem;
    /* No overflow clip here - the parent .mail-from already clips, and
       clipping here would crop the label links' keyboard focus ring. */
}
/* Dense-list overrides on the shared token: tighter dot↔name, smaller
   muted name, 8px dot (vs the canonical 9px). */
.mail-labels .bf-label { gap: .34rem; }
.mail-labels .bf-label-dot { width: 8px; height: 8px; }
.mail-labels .bf-label-name {
    font-size: 10px;
    font-weight: 500;
    color: var(--text-muted);
    max-width: 140px;
}
.mail-label-more {
    flex: 0 0 auto;
    font-size: 10px;
    font-weight: 600;
    line-height: 1;
    color: var(--text-muted);
    opacity: .85;
    font-variant-numeric: tabular-nums;
}
/* Lift the names with the row on hover, in step with the snippet. */
.mail-row:hover .bf-label-name,
.mail-row:hover .mail-label-more { color: var(--text); }

/* "Your turn" indicator on a conversation row: a subtle inbound/reply glyph
   shown just before the label rail when the last message came IN (the last
   replier wasn't us). Bare monochrome glyph in a muted tone - a calm hint,
   not a chip, so a column of them never shouts. */
.mail-awaiting-reply {
    display: inline-flex;
    align-items: center;
    align-self: center;
    flex: 0 0 auto;
    color: var(--text-muted);
}
.mail-awaiting-reply svg { display: block; }

.mail-date {
    text-align: right;
    font-size: 10px;
    color: var(--text-muted);
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    letter-spacing: .01em;
}
.mail-attach {
    color: var(--text-muted);
    margin-left: .35rem;
    display: inline-flex;
    align-items: center;
    vertical-align: middle;
}
.mail-attach svg { display: block; }
.mail-pin {
    display: inline-flex; align-items: center; vertical-align: middle;
    margin-right: .35rem;
    color: #eab308; /* amber to match starred palette */
}
.mail-pin svg { display: block; }
/* Team-notes indicator on a row */
.mail-note {
    display: inline-flex; align-items: center; gap: 2px;
    margin-left: .35rem;
    padding: 0 5px; height: 16px;
    background: var(--primary-bg); color: var(--primary);
    border-radius: 999px;
    vertical-align: middle;
    border: 1px solid transparent;
}
.mail-note svg { color: currentColor; }
.mail-note-count { font-size: 9px; font-weight: 700; line-height: 1; }
html[data-theme="dark"] .mail-note { background: rgba(129,140,248,.15); border-color: rgba(129,140,248,.35); }

/* Unread emphasis. Three layers stack:
   1. Subject + sender bold + full text colour
   2. Date in primary text colour, semi-bold
   3. Soft brand wash on the row background so unread rows read
      as a group while scrolling - lighter than the active-row
      highlight, just enough to be clearly distinct from read.
   The slim brand stripe is rendered separately by .read-btn
   (clickable mark-read affordance, see further down). */
.mail-row.unread .mail-from,
.mail-row.unread .mail-subject { font-weight: 700; color: var(--text); }
.mail-row.unread .mail-date { color: var(--text); font-weight: 600; }
.mail-row.unread {
    background: color-mix(in srgb, var(--primary) 4%, var(--surface));
}
.mail-row.unread:hover {
    background: color-mix(in srgb, var(--text) 5%, var(--surface));
}
.mail-row { position: relative; }

/* Clickable read/unread indicator - a slim vertical stripe. The button fills
   its whole grid cell with no gaps around it, so the full 22px column is
   clickable and there's no dead-zone whitespace that would fall through to
   the row navigation handler. */
.read-btn {
    width: 100%; height: 34px; padding: 0;
    border: 0; background: transparent; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
}
.read-btn::before {
    content: ""; display: block;
    width: 3px; height: 22px; border-radius: 2px;
    transition: background .12s ease, width .12s ease, height .12s ease, box-shadow .12s ease;
}
/* Unread = bold primary stripe with a soft brand glow so it
   reads as a confident "needs attention" marker, not a hairline.
   Read = a ghosted, always-visible stripe so the control stays
   discoverable even when every row is read. */
.read-btn.is-unread::before {
    background: var(--primary);
    width: 4px;
    height: 26px;
    box-shadow: 0 0 0 1px color-mix(in srgb, var(--primary) 25%, transparent);
}
.read-btn.is-read::before { background: var(--border-strong); opacity: .35; }
.mail-row:hover .read-btn.is-read::before { opacity: .65; }
.read-btn:hover::before { background: var(--primary); opacity: 1; width: 5px; height: 28px; }
.read-btn:hover[title]::after {
    /* small inline tooltip so the affordance is obvious on hover */
    content: attr(title);
    position: absolute;
    left: 100%; top: 50%; transform: translateY(-50%);
    margin-left: 6px;
    padding: 2px 8px;
    background: var(--text); color: var(--surface);
    border-radius: 6px; font-size: 10px; white-space: nowrap;
    pointer-events: none; z-index: 10;
    box-shadow: 0 2px 6px rgba(0,0,0,.15);
}
.read-btn { position: relative; }
.read-btn:disabled { opacity: .4; pointer-events: none; }

/* Flash bar shown after an account redirect (e.g. the inbox the user was
   viewing was removed on another tab). Dismissible, auto-fades after 6s. */
.account-flash {
    display: inline-flex; align-items: center; gap: .5rem;
    padding: .55rem .9rem;
    margin-bottom: 1rem;
    background: var(--warning-bg, #fef3c7);
    border: 1px solid rgba(245, 158, 11, .35);
    color: var(--text);
    border-radius: 10px;
    font-size: 12px;
    animation: account-flash-in .25s ease-out;
}
.account-flash svg { color: var(--warning); flex: 0 0 auto; }
@keyframes account-flash-in {
    from { opacity: 0; transform: translateY(-4px); }
    to   { opacity: 1; transform: translateY(0); }
}
html[data-theme="dark"] .account-flash {
    background: rgba(251, 191, 36, .12);
    border-color: rgba(251, 191, 36, .4);
}

.empty-state { padding: 4rem 1.5rem; text-align: center; color: var(--text-muted); }
.empty-state .emoji { font-size: 2.5rem; display: block; margin-bottom: .75rem; }
.empty-state h5 { color: var(--text); font-weight: 700; margin-bottom: .35rem; }
.empty-state p { color: var(--text-muted); margin-bottom: 1.25rem; }

/* Pagination */
.pagination { margin: 1rem 0; justify-content: center; }
.pagination .page-link { color: var(--text); border: 1px solid var(--border); border-radius: 6px; margin: 0 .15rem; font-size: 12px; padding: .35rem .75rem; }
/* Current page: the house no-filled-active treatment - ink weight + ink
   hairline, never a violet fill (the filled number read as a button). */
.pagination .page-item.active .page-link { background: color-mix(in srgb, var(--text) 7%, transparent); color: var(--text); font-weight: 600; border-color: transparent; }

/* Premium pagination bar - flex layout with 1 / auto / 1 sizing so the nav
   controls stay dead-center regardless of how wide the left/right sides grow. */
.pagination-bar {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: .65rem 1.25rem;
    background: var(--surface);
    border-top: 1px solid var(--border);
    box-shadow: 0 -4px 12px -10px rgba(15,23,42,.08);
}
/* `flex-wrap: nowrap` is locked in here, NOT just at narrow
   breakpoints, so the count text + kebab can NEVER stack on
   separate rows. Earlier the default `flex-wrap: wrap` let the
   kebab fall under the count when the cell narrowed (the bug
   the user kept hitting); pinning nowrap means the two stay
   visually grouped at every viewport width. The kebab uses
   `flex-shrink: 0` so it stays a fixed 26x26 icon even when the
   count text would otherwise eat into its space; if the cell
   becomes truly tiny, the count text is the one that
   ellipsises, never the action button. */
.pagination-range {
    flex: 1 1 0;
    font-size: 12px;
    color: var(--text-muted);
    display: flex;
    align-items: center;
    gap: .6rem;
    flex-wrap: nowrap;
    min-width: 0;
    /* No `overflow: hidden` here. The inner text span (rule below) carries
       its own `overflow: hidden + text-overflow: ellipsis` to truncate the
       range text on narrow widths, so a clip on the parent is redundant.
       It would also chop the kebab dropdown - an absolute descendant that
       opens UPWARD beyond this 26px-tall row - leaving only the box-shadow
       blur leaking through (the "blurry rect" symptom). */
}
.pagination-range > .pg-more { flex: 0 0 auto; }
.pagination-range > span:first-child {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.pagination-controls {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 2px;
}
.pagination-jump-wrap { flex: 1 1 0; display: flex; justify-content: flex-end; min-width: 0; }
.pg-single-page { color: var(--text-subtle); font-size: 11px; }
@media (max-width: 900px) {
    /* Stack the zones, each on its own row, center-aligned. The
       range row's inner flex stays nowrap so the kebab sits next
       to the count instead of falling to a sub-row. Jumper drops
       off (the user can paginate by clicking buttons or by URL
       at narrow widths). */
    .pagination-bar { flex-wrap: wrap; gap: .5rem; padding: .5rem .75rem; }
    .pagination-range {
        flex: 1 1 100%;
        justify-content: center;
    }
    .pagination-controls {
        flex: 1 1 100%;
        justify-content: center;
        flex-wrap: wrap;
        row-gap: 4px;
    }
    .pagination-jump-wrap { display: none; }
    /* Drop first/last (««/»») on narrow screens - prev/next +
       page numbers are enough. */
    .pagination-controls .pg-btn[title="First page"],
    .pagination-controls .pg-btn[title="Last page"] { display: none; }
}
@media (max-width: 560px) {
    /* Compact: keep only Prev / [active] / Next on the controls
       row. No ellipses, no out-of-range numeric buttons. */
    .pagination-controls .pg-btn:not(.active):not([title="Previous"]):not([title="Next"]) { display: none; }
    .pagination-controls .pg-ellipsis { display: none; }
    /* Drop the kebab entirely - its menu items (Mark all read /
       unread) are already first-class buttons on the bulk-action
       toolbar above the list. Showing the same actions twice in
       a tight viewport is just clutter. */
    .pagination-range .pg-more { display: none; }
    /* Tight horizontal padding so the count sits closer to the
       container edges and reads as the only thing on its row. */
    .pagination-bar { padding: .4rem .5rem; }
    .pagination-range { gap: .35rem; }
}
/* Legacy compact class no-op - kept in case lingering refs point at it */
.pagination-bar-compact { }
.pagination-range strong { color: var(--text); font-weight: 600; }
.pg-action {
    display: inline-flex; align-items: center; gap: .35rem;
    padding: .25rem .6rem; height: 26px;
    font-size: 11px; font-weight: 500; color: var(--text-muted);
    background: transparent; border: 1px solid var(--border); border-radius: 6px;
    cursor: pointer; transition: all .1s ease;
}
.pg-action:hover { color: var(--text); border-color: color-mix(in srgb, var(--text) 25%, transparent); background: color-mix(in srgb, var(--text) 5%, transparent); }
.pg-action svg { color: currentColor; }

/* Compact kebab dropdown for per-view actions (mark-all-read, etc.) */
.pg-more { position: relative; }
.pg-more-btn {
    display: inline-flex; align-items: center; justify-content: center;
    /* One control height per bar: level with the 32px .pg-btn squares at
       the other end of the pagination footer. */
    width: 32px; height: 32px; padding: 0;
    background: transparent; border: 1px solid var(--border);
    border-radius: 6px; cursor: pointer; color: var(--text-muted);
    transition: all .1s ease;
}
.pg-more-btn:hover, .pg-more-btn.is-open { color: var(--text); border-color: color-mix(in srgb, var(--text) 25%, transparent); background: color-mix(in srgb, var(--text) 5%, transparent); }
.pg-more-menu {
    position: absolute; bottom: calc(100% + 6px); left: 0; z-index: 30;
    background: var(--surface); border: 1px solid var(--border);
    border-radius: 10px; box-shadow: 0 12px 32px rgba(15,23,42,.15);
    min-width: 200px; padding: .35rem;
}
.pg-more-menu[hidden] { display: none; }
.pg-more-item {
    display: flex; align-items: center; gap: .5rem; width: 100%;
    padding: .5rem .65rem; background: transparent; border: 0;
    border-radius: 6px; cursor: pointer; text-align: left;
    font-size: 12px; color: var(--text);
}
.pg-more-item:hover { background: var(--surface-2); }
.pg-more-item svg { color: var(--text-muted); flex: 0 0 auto; }
.pg-more-item:hover svg { color: var(--text); }
/* Compact bar (single page): no pagination controls, no jumper - the range row
   sits alone and should left-align, not sit in a 3-column grid. */
.pagination-bar-compact { grid-template-columns: 1fr; }
.pagination-bar-compact .pagination-range { justify-self: start; }

/* ==== Action toolbar above the message list ============================
   Always visible (stable-height row) so the list never jumps when the
   selection state changes. One zone:
     - .bulk-bar-left   : swaps between "empty" hint and bulk actions
   The body class .has-selection drives the swap (set by updateBar()). */
/* ── THE TOOLBAR RULE ─────────────────────────────────────────
   A toolbar declares a MINIMUM height, never a fixed one.

   This is the single mechanical cause of the mail page's recurring
   resize breakage, and it recurs because it is invisible until the
   row happens to get too long: `height: 52px` on a flex row that
   wraps does not clip and does not scroll - the second line renders
   OUTSIDE the box, straight over whatever sits below. Every feature
   that adds one more control brings the day that happens closer,
   and the person who adds it has no reason to suspect the container.

   min-height keeps the resting look identical to the pixel and lets
   the box grow the moment its content needs a second row. Pinned by
   MailToolbarResizeContractTests, which fails the build if any mail
   toolbar goes back to a fixed height or loses its row-gap - so this
   stops being something anyone has to remember. */
.bulk-bar {
    flex: 0 0 auto;
    display: flex; align-items: center; gap: .4rem;
    padding: .5rem 1rem;
    min-height: 52px;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    transition: background .18s ease, border-color .18s ease, box-shadow .18s ease;
}
/* Selection-active treatment for the bulk-bar. Background flips to
   a soft brand-tinted wash and the bottom border picks up a brand
   accent so the toolbar reads as "you're in selection mode -
   actions on the right are armed". Smooth transition both ways
   so toggling the selection feels like a state change, not a
   layout jump. */
body.has-selection .bulk-bar {
    background: color-mix(in srgb, var(--primary) 8%, var(--surface));
    border-bottom-color: color-mix(in srgb, var(--primary) 35%, var(--border));
    box-shadow: 0 1px 0 color-mix(in srgb, var(--primary) 25%, transparent);
}
.bulk-bar-left {
    flex: 1 1 auto;
    min-width: 0;
    display: flex; align-items: center; gap: .4rem;
    /* No overflow:hidden - it clips the absolutely-positioned label
       popover and the chooser menu. When the content doesn't fit,
       bulkbar-fit.js collapses the buttons (is-compact / is-tight)
       instead of clipping or scrolling. */
}
/* Selection chooser: master checkbox + caret menu (Gmail pattern).
   Always visible. The checkbox toggles select-all-on-page; the caret
   opens a menu of advanced rules (All / None / Read / Unread / Starred /
   Unstarred). Sits at the very left of the toolbar in both empty and
   selection states - the action buttons follow it when selection > 0. */
.bulk-chooser {
    position: relative;
    flex: 0 0 auto;
    display: inline-flex; align-items: center;
    gap: 0;
    padding: 1px 4px 1px 2px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--surface);
    /* 32px is the list-furniture tier (pg-btn squares, .bulk-btn) - the
       chooser shares its row with .bulk-btn controls in both states. */
    height: 32px;
}
.bulk-chooser-cb {
    display: inline-flex; align-items: center; justify-content: center;
    width: 26px; height: 100%;
    cursor: pointer;
    margin: 0;
}
.bulk-chooser-cb input[type="checkbox"] {
    position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none;
}
/* ── THE platform checkbox (owner law: ONE checkbox style everywhere) ──
   Every selection checkbox - mail/issues chooser, directory rows, license
   rows - renders THIS visual: 17px box, 5px radius, strong hairline,
   primary fill + white tick when checked, white dash when indeterminate.
   Canonical markup:
     <label class="bf-check-wrap"><input type="checkbox" class="bf-check-input" …>
       <span class="bf-check" aria-hidden="true"></span></label>
   The legacy spellings (.contacts-check*, .bulk-chooser-box) are folded
   into the SAME declarations - one definition, several class names, until
   their markup migrates. Never restyle one of them alone. */
.bf-check-input, .contacts-check-input {
    position: absolute; width: 1px; height: 1px; opacity: 0; margin: 0; pointer-events: none;
}
.bf-check-wrap { display: inline-flex; align-items: center; justify-content: center; cursor: pointer; margin: 0; padding: 2px; }
.bf-check, .contacts-check, .bulk-chooser-box {
    flex: 0 0 auto;
    display: inline-flex; align-items: center; justify-content: center;
    width: 17px; height: 17px;
    border: 1.5px solid var(--border-strong);
    border-radius: 6px;
    background: var(--surface);
    transition: background .12s ease, border-color .12s ease;
}
.bf-check::after, .contacts-check::after, .bulk-chooser-box::after {
    content: "";
    width: 4px; height: 8px; margin-top: -1px;
    /* The tick sits on the always-accent fill in BOTH themes: fixed white,
       the same on-accent colour .btn-primary uses. */
    border: solid white;
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
    opacity: 0;
    transition: opacity .12s ease;
}
.bf-check-wrap:hover .bf-check, .contact-row-select:hover .contacts-check, .bulk-chooser-cb:hover .bulk-chooser-box { border-color: color-mix(in srgb, var(--text) 25%, transparent); }
.bf-check-input:checked ~ .bf-check, .contacts-check-input:checked ~ .contacts-check, .bulk-chooser-cb input:checked + .bulk-chooser-box {
    background: var(--primary); border-color: var(--primary);
}
.bf-check-input:checked ~ .bf-check::after, .contacts-check-input:checked ~ .contacts-check::after, .bulk-chooser-cb input:checked + .bulk-chooser-box::after { opacity: 1; }
.bf-check-input:indeterminate ~ .bf-check, .bulk-chooser-cb input:indeterminate + .bulk-chooser-box {
    background: var(--primary); border-color: var(--primary);
}
.bf-check-input:indeterminate ~ .bf-check::after, .bulk-chooser-cb input:indeterminate + .bulk-chooser-box::after {
    opacity: 1;
    width: 8px; height: 0; margin-top: 0;
    border-width: 0 0 2px 0;
    transform: none;
}
.bf-check-input:focus-visible ~ .bf-check, .contacts-check-input:focus-visible ~ .contacts-check { outline: 2px solid var(--primary); outline-offset: 2px; }
.bulk-chooser-caret {
    width: 18px; height: 100%;
    border: 0;
    background: transparent;
    cursor: pointer;
    color: var(--text-muted);
    display: inline-flex; align-items: center; justify-content: center;
    border-radius: 4px;
    transition: background .1s ease, color .1s ease;
}
.bulk-chooser-caret:hover { background: var(--surface-2); color: var(--text); }
.bulk-chooser-caret[aria-expanded="true"] { background: var(--surface-2); color: var(--text); }
.bulk-chooser-menu {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    z-index: 60;
    min-width: 160px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: .25rem;
    box-shadow: 0 12px 28px -8px rgba(15,23,42,.18), 0 2px 6px -1px rgba(15,23,42,.06);
    animation: bulk-fade-in .1s ease;
}
.bulk-chooser-menu[hidden] { display: none; }
.bulk-chooser-item {
    display: block;
    width: 100%;
    text-align: left;
    padding: .35rem .55rem;
    border: 0;
    background: transparent;
    color: var(--text);
    font-size: 12px;
    border-radius: 6px;
    cursor: pointer;
    transition: background .08s ease, color .08s ease;
}
.bulk-chooser-item:hover { background: color-mix(in srgb, var(--text) 5%, transparent); color: var(--text); }
.bulk-chooser-sep {
    height: 1px;
    background: var(--surface-2);
    margin: .25rem .25rem;
}

.bulk-bar-actions {
    display: none;
    align-items: center; gap: .4rem;
    /* ONE line, always (owner, 2026-09-06: "never allow a 2-line top bar…
       the top bar must never move the UI"): the row never wraps, so the
       measured ladder in bulkbar-fit.js sees the overflow and folds - the
       words first, then the glyph-only squares, then the trailing verbs
       into the ⋯ menu. A wrapping row hid the overflow from the measure
       and grew a second line under the selection. */
    flex-wrap: nowrap;
    flex: 1 1 auto;
    min-width: 0;
    animation: bulk-fade-in .14s ease;
}
/* "N selected" and its clear button never wrap away from the front:
   losing sight of what the verbs will act on is worse than a
   slightly tighter row. */
.bulk-bar-actions .bulk-cancel,
.bulk-bar-actions .bulk-count { flex: 0 0 auto; }

/* A split control: one concept, its directions inside. Read, Star
   and Status each used to cost one slot per direction. */
.bulk-split { position: relative; display: inline-flex; }
.bulk-split-pop {
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    z-index: 60;
    min-width: 176px;
    padding: .25rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    box-shadow: 0 16px 40px -10px rgba(15,23,42,.22), 0 4px 10px -2px rgba(15,23,42,.08);
}
.bulk-split-pop[hidden] { display: none; }
.bulk-split-item {
    display: flex; align-items: center; gap: .5rem;
    width: 100%;
    padding: .4rem .5rem;
    border: 0;
    background: transparent;
    border-radius: 8px;
    cursor: pointer;
    text-align: left;
    font: inherit;
    font-size: 12px;
    color: var(--text);
    white-space: nowrap;
    transition: background .08s ease;
}
.bulk-split-item:hover { background: var(--surface-2); }
/* A split item that cannot produce anything right now (nothing selected it
   could act on) stays VISIBLE but disabled - stable menu geometry, macOS
   grammar: muted ink, no hover wash, default cursor. */
.bulk-split-item:disabled {
    color: var(--text-subtle);
    cursor: default;
}
.bulk-split-item:disabled:hover { background: transparent; }
.bulk-split-item:disabled svg { opacity: 0.55; }
/* Section break inside a split pop (the grouped More menu). */
.bulk-split-sep {
    height: 1px;
    background: var(--border);
    margin: 0.25rem 0.35rem;
}
/* The dropdown hint on a split's toggle - the class form of the inline
   chevron the mail bar started with (is-compact hides it with the other
   trailing svgs). */
.bulk-btn-chev { margin-left: 0.1rem; opacity: 0.7; }
body.has-selection .bulk-bar-actions { display: flex; }
@keyframes bulk-fade-in { from { opacity: 0; transform: translateY(-2px); } to { opacity: 1; transform: none; } }
.bulk-cancel {
    width: 30px; height: 30px;
    display: inline-flex; align-items: center; justify-content: center;
    background: transparent; border: 1px solid var(--border);
    border-radius: 6px; cursor: pointer; color: var(--text-muted);
    transition: all .1s ease;
}
.bulk-cancel:hover { background: var(--surface-2); color: var(--text); border-color: var(--border-strong); }
.bulk-count {
    font-size: 13px;
    color: var(--text);
    margin: 0 .5rem 0 .25rem;
    /* Keep "1 selected" / "12 selected" on a single line at every
       width. Without this, narrow viewports wrap the text into
       "1 / selected" which looks like a layout bug and also
       inflates the row height. */
    white-space: nowrap;
    flex: 0 0 auto;
}
.bulk-count strong { color: var(--primary); font-weight: 700; }
.bulk-btn {
    display: inline-flex; align-items: center; gap: .35rem;
    /* 32px: level with the .bulk-chooser it shares the bulk bar with. */
    padding: 0 .7rem; height: 32px;
    background: var(--surface); border: 1px solid var(--border);
    border-radius: 6px; cursor: pointer;
    font-size: 12px; font-weight: 500; color: var(--text);
    transition: all .1s ease;
    /* Keep button labels on a single line at every viewport width.
       Without this, narrow viewports wrap "Mark read" -> "Mark"
       above "read" inside the fixed-height button, which both
       breaks the 30px height invariant AND looks awful. Toolbar
       overflow is handled by the measured is-compact collapse
       below - buttons never lose their single-line layout. */
    white-space: nowrap;
    flex: 0 0 auto;
}
.bulk-btn:hover { background: color-mix(in srgb, var(--text) 5%, transparent); color: var(--text); border-color: color-mix(in srgb, var(--text) 25%, transparent); }
.bulk-btn:disabled { opacity: .6; cursor: default; }
.bulk-btn svg { color: currentColor; flex: 0 0 auto; }
.bulk-spin {
    display: inline-block; width: 12px; height: 12px;
    border: 2px solid var(--border-strong);
    border-top-color: var(--primary);
    border-radius: 50%;
    animation: bulk-spin .8s linear infinite;
}
@keyframes bulk-spin { to { transform: rotate(360deg); } }
.bulk-divider { width: 1px; height: 20px; background: var(--border); margin: 0 .2rem; }
/* Trash button reads as destructive: red label / hover, soft red ring
   on focus. Restore (its sibling state) reads as a normal action - we
   just give it a slightly green tint on hover so the verb-flip is
   immediately legible. */
.bulk-btn-trash:hover {
    background: color-mix(in srgb, #b91c1c 8%, var(--surface));
    border-color: color-mix(in srgb, #b91c1c 28%, var(--border));
    color: #b91c1c;
}
.bulk-btn-restore:hover {
    background: color-mix(in srgb, #16a34a 8%, var(--surface));
    border-color: color-mix(in srgb, #16a34a 28%, var(--border));
    color: #15803d;
}
/* Bulk-mode Delete button. Same red palette as .bulk-btn-trash
   (which is used by the Mail bulk toolbar's permanent-delete
   variant) so destructive actions read consistently across
   workspace types. The hover override BEATS the generic
   .bulk-btn:hover (primary-bg) because this rule is later in the
   cascade. */
.bulk-btn-danger:hover {
    background: color-mix(in srgb, #b91c1c 8%, var(--surface));
    border-color: color-mix(in srgb, #b91c1c 28%, var(--border));
    color: #b91c1c;
}
/* ── Toolbar too narrow for labels: collapse to icon-only ──
   `is-compact` is set by js/bulkbar-fit.js (loaded from _Layout)
   whenever the .bulk-bar-left content MEASURES wider than the
   zone - the fit-point is whatever the current language's labels
   actually need, not a hardcoded width (the old 1100px container
   query was tuned for English; French overflowed and overlapped
   at ~1600px). Every button collapses to a 32×32 icon-only
   square. Label text in these buttons lives as bare text nodes
   directly inside the button (no <span> wrapper), so we can't
   selectively hide it via a child selector. Instead we set
   `font-size: 0` on the button: text nodes collapse to zero size
   while SVG icons stay rendered at their explicit width/height
   attributes (13×13). Trailing dropdown chevrons (the second SVG
   sibling) are hidden via :not(:first-child) on the SVGs. Screen
   readers still announce the button via title / aria-label. */
/* The fit pass (js/bulkbar-fit.js) measures with every transition off: a
   .bulk-btn animates its padding and font-size, so a measure taken the instant
   is-compact came off read the glyph widths, called the words a fit, and the bar
   then grew its words out over the reading pane, carrying an open popover with
   it (#623, 2026-10-06: "la liste est instable et disparait"). */
.bulk-bar.is-measuring,
.bulk-bar.is-measuring * {
    transition: none !important;
}
.bulk-bar.is-compact .bulk-btn {
    width: 32px;
    padding: 0 !important;
    justify-content: center;
    gap: 0;
    font-size: 0;
    overflow: hidden;
}
.bulk-bar.is-compact .bulk-btn > svg:not(:first-child),
.bulk-bar.is-compact .bulk-btn > kbd,
.bulk-bar.is-compact .bulk-btn > span:not(.bulk-spin) {
    display: none;
}
.bulk-bar.is-compact .bulk-bar-actions {
    flex-wrap: nowrap;
    overflow: visible;
    gap: .25rem;
}
.bulk-bar.is-compact {
    gap: .35rem;
    padding: .45rem .65rem;
}

/* ── Even icon-only overflows: drop the "X selected" count too ──
   Second measured tier (same script); the count is already
   implied by the highlighted row checkboxes. */
.bulk-bar.is-tight .bulk-count { display: none; }
.bulk-bar.is-tight { padding: .35rem .4rem; }
.bulk-bar.is-tight .bulk-bar-actions { gap: .15rem; }
/* ── Final overflow tier ──────────────────────────────────────
   Even icon-only, ten actions overflow a narrow bar - the last
   tier folds trailing buttons into a "⋯" menu (bulkbar-fit.js
   moves the REAL nodes) until the bar fits. Toolbars wrap or
   fold; they never clip and never scroll. */
.bulk-more {
    flex: 0 0 auto;
    width: 32px;
    justify-content: center;
}
.bulk-more[hidden] { display: none; }
.bulk-more-menu {
    position: fixed;
    z-index: 1300;
    min-width: 200px;
    padding: 6px;
    display: flex;
    flex-direction: column;
    gap: 2px;
    background: var(--surface, #fff);
    border: 1px solid var(--border, #e2e8f0);
    border-radius: 12px;
    box-shadow: 0 14px 36px rgba(15, 23, 42, .14);
}
.bulk-more-menu[hidden] { display: none; }
/* Folded buttons read as menu rows: full width, label leading -
   they escaped the .bulk-bar scope, so the compact/tight label
   hiders no longer apply and the full wording shows. */
.bulk-more-menu .bulk-btn {
    width: 100%;
    justify-content: flex-start;
    padding: 7px 10px;
    border-radius: 8px;
}
html[data-theme="dark"] .bulk-more-menu {
    background: var(--surface, #0f172a);
    border-color: rgba(148, 163, 184, .18);
}
/* Ensure archive-main is positioned so the bulk bar anchors to it */
.archive-main { position: relative; }

/* ── Right-click context menu on mail rows ──────────────────────────
   Anchored to the cursor on `contextmenu`, populated dynamically based
   on which bulk-action buttons are currently rendered (so a viewer
   without CanEditLabels never sees "Label as"). position:fixed +
   z-index above the bulk-bar / sidebars so scroll containers don't
   misplace it. The wrapper is hidden by default; the JS toggles
   `hidden` and writes left/top inline. */
.mail-ctx-menu {
    position: fixed;
    z-index: 1100;
    min-width: 200px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: .25rem;
    box-shadow: 0 12px 28px -8px rgba(15, 23, 42, .25), 0 2px 6px -1px rgba(15, 23, 42, .08);
    user-select: none;
    animation: mail-ctx-fade-in .1s ease;
}
.mail-ctx-menu[hidden] { display: none; }
@keyframes mail-ctx-fade-in {
    from { opacity: 0; transform: translateY(-4px); }
    to   { opacity: 1; transform: none; }
}
.mail-ctx-item {
    display: flex; align-items: center; gap: .55rem;
    width: 100%;
    padding: .4rem .6rem;
    border: 0;
    background: transparent;
    color: var(--text);
    font: inherit;
    font-size: 12px;
    text-align: left;
    border-radius: 6px;
    cursor: pointer;
    transition: background .08s ease, color .08s ease;
}
.mail-ctx-item:hover,
.mail-ctx-item:focus-visible {
    background: color-mix(in srgb, var(--text) 5%, transparent);
    color: var(--text);
    outline: none;
}
.mail-ctx-item[hidden] { display: none; }
.mail-ctx-item.is-destructive { color: #b91c1c; }

/* Trash / Restore dual-icon swap. The button always carries
   both SVGs; CSS shows exactly one based on whether the
   button is currently in restore mode (which the JS toggles
   to mirror the bulk-toolbar's flipped label). Same shape
   for Spam / Not spam. */
.mail-ctx-item .mail-ctx-icon-restore,
.mail-ctx-item .mail-ctx-icon-notspam { display: none; }
.mail-ctx-item.is-restore-mode .mail-ctx-icon-trash { display: none; }
.mail-ctx-item.is-restore-mode .mail-ctx-icon-restore { display: inline-block; }
.mail-ctx-item.is-notspam-mode .mail-ctx-icon-spam { display: none; }
.mail-ctx-item.is-notspam-mode .mail-ctx-icon-notspam { display: inline-block; }
.mail-ctx-item.is-destructive:hover,
.mail-ctx-item.is-destructive:focus-visible {
    background: color-mix(in srgb, #b91c1c 8%, var(--surface));
    color: #b91c1c;
}
.mail-ctx-item svg:first-child { flex: 0 0 auto; color: currentColor; }
.mail-ctx-item .mail-ctx-chev { margin-left: auto; opacity: .7; }
.mail-ctx-sep {
    height: 1px;
    background: var(--surface-2);
    margin: .25rem .15rem;
}
.mail-ctx-sep[hidden] { display: none; }

/* Flyout sub-menus (Label as / Assign to). Each parent menu item
   sits inside a .mail-ctx-flyout-host wrapper which becomes the
   positioning context for the flyout. Hidden by default; opened on
   hover/focus by the JS. The flyout slides out to the right of the
   parent (left-aligned with the parent's right edge) and is
   reflowed to the LEFT (.is-flipped) when there's no horizontal
   space. The 4px overlap (negative margin) lets the user's pointer
   travel from parent to child without crossing a gap that would
   close the flyout. */
.mail-ctx-flyout-host {
    position: relative;
}
.mail-ctx-flyout-host[hidden] { display: none; }
.mail-ctx-flyout-host.is-open > .mail-ctx-item {
    background: color-mix(in srgb, var(--text) 7%, transparent);
    color: var(--text);
}
.mail-ctx-flyout {
    position: absolute;
    top: -.25rem;
    left: calc(100% - 4px);
    z-index: 10;
    min-width: 240px;
    max-width: 320px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: .25rem;
    box-shadow: 0 12px 28px -8px rgba(15, 23, 42, .25), 0 2px 6px -1px rgba(15, 23, 42, .08);
    display: flex; flex-direction: column;
    animation: mail-ctx-fade-in .1s ease;
}
.mail-ctx-flyout[hidden] { display: none; }
.mail-ctx-flyout.is-flipped {
    left: auto;
    right: calc(100% - 4px);
}

.mail-ctx-search {
    display: flex; align-items: center; gap: .4rem;
    margin: .15rem .25rem .25rem;
    padding: .35rem .55rem;
    background: var(--surface-2);
    border-radius: 6px;
    border: 1px solid transparent;
    transition: border-color .12s ease, background .12s ease;
}
.mail-ctx-search:focus-within {
    background: var(--surface);
    border-color: var(--primary);
}
.mail-ctx-search svg { color: var(--text-subtle); flex: 0 0 auto; }
.mail-ctx-search input {
    flex: 1 1 auto;
    min-width: 0;
    border: 0;
    background: transparent;
    color: var(--text);
    font: inherit;
    font-size: 12px;
    outline: none;
}

.mail-ctx-list {
    max-height: 280px;
    /* Zero-CLS: the flyout opens on a one-line "Loading…" and the
       label / folder / assignee rows land one fetch later. Reserve
       a realistic list height from the first frame so the open
       flyout never grows when the rows arrive - loading and empty
       states center inside the same fixed frame. */
    min-height: 120px;
    overflow-y: auto;
    /* Keep wheel scrolling inside the list: without this, wheeling
       past the top/bottom edge chains to the page, which fires a
       window scroll and dismisses the whole context menu. */
    overscroll-behavior: contain;
    padding: .15rem 0;
}
.mail-ctx-empty {
    padding: .65rem .65rem;
    color: var(--text-muted);
    font-size: 11px;
    font-style: italic;
    text-align: center;
}

.mail-ctx-create-btn {
    display: flex; align-items: center; gap: .4rem;
    width: calc(100% - .5rem);
    margin: .15rem .25rem .15rem;
    padding: .4rem .55rem;
    border: 1px dashed var(--border-strong);
    background: transparent;
    color: var(--primary);
    font: inherit;
    font-size: 11px;
    font-weight: 600;
    text-align: left;
    border-radius: 6px;
    cursor: pointer;
}
.mail-ctx-create-btn:hover, .mail-ctx-create-btn:focus-visible {
    background: color-mix(in srgb, var(--text) 5%, transparent);
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
    outline: none;
}
.mail-ctx-create-btn[hidden] { display: none; }

/* Label rows inside the labels sub-pane. Custom checkbox pseudo-
   element so we can show "all" (filled), "some" (dash) and "none"
   (empty) states without an actual <input>. */
.mail-ctx-label-row {
    display: flex; align-items: center; gap: .55rem;
    width: 100%;
    padding: .35rem .55rem;
    border: 0;
    background: transparent;
    color: var(--text);
    font: inherit;
    font-size: 12px;
    text-align: left;
    border-radius: 6px;
    cursor: pointer;
    transition: background .08s ease;
}
.mail-ctx-label-row:hover, .mail-ctx-label-row:focus-visible {
    background: color-mix(in srgb, var(--text) 5%, transparent);
    color: var(--text);
    outline: none;
}
.mail-ctx-label-name-text {
    flex: 1 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* Leading glyph on a Move-to target row. Takes the place of the
   Label-as checkbox: the move flyout is single-select, so the tag
   icon signals "file here" without a toggle state. */
.mail-ctx-move-icon {
    flex: 0 0 auto;
    color: currentColor;
    opacity: .6;
}
.mail-ctx-cb {
    flex: 0 0 auto;
    width: 14px; height: 14px;
    border-radius: 4px;
    border: 1.5px solid var(--border-strong);
    background: var(--surface);
    position: relative;
    transition: background .1s ease, border-color .1s ease;
}
.mail-ctx-cb.is-checked {
    background: var(--primary);
    border-color: var(--primary);
}
.mail-ctx-cb.is-checked::after {
    content: "";
    position: absolute;
    left: 3px; top: 0px;
    width: 4px; height: 8px;
    border-right: 2px solid #fff;
    border-bottom: 2px solid #fff;
    transform: rotate(42deg);
}
.mail-ctx-label-row.is-some .mail-ctx-cb {
    background: var(--primary);
    border-color: var(--primary);
}
.mail-ctx-label-row.is-some .mail-ctx-cb::after {
    content: "";
    position: absolute;
    left: 2px; top: 5px;
    width: 8px; height: 2px;
    background: #fff;
    border-radius: 1px;
}

/* Assignee rows. Avatar + 2-line text. Uses the shared --avatar-bg
   token so the letter bubble matches every other surface. */
.mail-ctx-assignee-row {
    display: flex; align-items: center; gap: .55rem;
    width: 100%;
    padding: .4rem .55rem;
    border: 0;
    background: transparent;
    color: var(--text);
    font: inherit;
    font-size: 12px;
    text-align: left;
    border-radius: 6px;
    cursor: pointer;
    transition: background .08s ease;
}
.mail-ctx-assignee-row:hover, .mail-ctx-assignee-row:focus-visible {
    background: color-mix(in srgb, var(--text) 5%, transparent);
    outline: none;
}
.mail-ctx-assignee-avatar {
    width: 24px; height: 24px;
    border-radius: 50%;
    background: var(--avatar-bg);
    color: var(--avatar-color);
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 10px; font-weight: 700;
    flex: 0 0 auto;
    overflow: hidden;
}
img.mail-ctx-assignee-avatar { object-fit: cover; }
.mail-ctx-assignee-text {
    display: flex; flex-direction: column;
    min-width: 0;
    line-height: 1.15;
}
.mail-ctx-assignee-name {
    font-size: 12px; font-weight: 600;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.mail-ctx-assignee-email {
    font-size: 10px; color: var(--text-muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.mail-ctx-assignee-self {
    font-size: 10px; color: var(--text-muted); font-weight: 500;
}
.mail-ctx-assignee-clear {
    border-bottom: 1px solid var(--surface-2);
    margin-bottom: .25rem;
    padding-bottom: .5rem;
    color: var(--text-muted);
}
.mail-ctx-assignee-clear-icon {
    width: 24px; height: 24px;
    display: inline-flex; align-items: center; justify-content: center;
    border-radius: 50%;
    background: var(--surface-2);
    color: var(--text-muted);
    font-size: 14px;
    flex: 0 0 auto;
}

/* Right-clicked row visual highlight (subtle band on the left) so
   the user knows which row the menu acts on without us touching the
   checkbox. Survives until the menu closes. */
.mail-row.is-ctx-target {
    background: color-mix(in srgb, var(--primary) 6%, var(--surface));
    box-shadow: inset 3px 0 0 0 var(--primary);
}

/* ── Assignee picker popover ─ shared between the message-detail
   pill and the Mail/Index bulk "Assign to" toolbar button. Originally
   page-scoped to Message.cshtml; promoted to site.css so the bulk
   popover (rendered inside .label-pop on the Mail/Index page) gets
   the same look. The defensive overflow + object-fit on the avatar
   guards against an img child blowing past the 28x28 frame when the
   rules from a parent class don't otherwise apply. */
.msg-assignee-pop-list { max-height: 320px; overflow-y: auto; padding: .25rem; }
.msg-assignee-pop-item {
    display: flex; align-items: center; gap: .55rem;
    width: 100%;
    padding: .4rem .5rem;
    border: 0;
    background: transparent;
    border-radius: 8px;
    cursor: pointer;
    text-align: left;
    font: inherit;
    color: var(--text);
    transition: background .08s ease;
}
.msg-assignee-pop-item:hover { background: var(--surface-2); }
.msg-assignee-pop-item.is-current { background: var(--primary-bg); }
.msg-assignee-pop-item.is-current:hover { background: color-mix(in srgb, var(--text) 5%, transparent); }
.msg-assignee-pop-avatar {
    width: 28px; height: 28px;
    min-width: 28px; max-width: 28px;
    min-height: 28px; max-height: 28px;
    border-radius: 50%;
    flex: 0 0 auto;
    overflow: hidden;
    background: var(--avatar-bg); color: var(--avatar-color);
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 10px; font-weight: 700;
    object-fit: cover;
    object-position: center;
}
img.msg-assignee-pop-avatar { display: block; }
.msg-assignee-pop-text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.msg-assignee-pop-name { font-size: 12px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msg-assignee-pop-email { font-size: 10px; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msg-assignee-pop-check { color: var(--primary); flex: 0 0 auto; }
.msg-assignee-pop-empty { padding: 1.2rem .8rem; text-align: center; color: var(--text-muted); font-size: 12px; }
/* Clear-assignment footer. Reads as a clearly destructive secondary
   action: muted-red label on a faint warning band, slim height, and
   it's the only thing on its row so users can't mistake it for one
   of the picker items above. The picker hides this entirely when
   there's nothing currently assigned (the JS sets [hidden]). */
.msg-assignee-pop-clear {
    width: 100%;
    display: flex; align-items: center; justify-content: center; gap: .4rem;
    padding: .55rem .75rem;
    background: color-mix(in srgb, #b91c1c 5%, var(--surface));
    border: 0;
    border-top: 1px solid color-mix(in srgb, #b91c1c 18%, var(--surface-2));
    color: #b91c1c;
    cursor: pointer;
    font-family: inherit;
    font-size: 11px;
    font-weight: 600;
    line-height: 1;
    transition: background .1s ease, color .1s ease;
}
.msg-assignee-pop-clear:hover {
    background: color-mix(in srgb, #b91c1c 14%, var(--surface));
    color: #991b1b;
}
.msg-assignee-pop-clear[hidden] { display: none; }
.msg-assignee-pop-clear svg { opacity: .85; }

/* Bulk-assign subtitle. Sits between the search input and the
   teammate list; tells the user what's already on the selection so
   "Clear" / "Assign to X" is unambiguous. */
.bulk-assign-subtitle {
    margin-top: .35rem;
    padding: .25rem .55rem;
    background: var(--surface-2);
    border-radius: 6px;
    font-size: 10px;
    font-weight: 600;
    color: var(--text-muted);
    line-height: 1.3;
}
.bulk-assign-subtitle[hidden] { display: none; }

/* Per-row assignee chip - tucked into the .mail-line-1 between the
   thread-count badge and the open-in-new-tab affordance. Small and
   muted so it never competes with the sender / date typography but
   visible enough to scan ownership at a glance. */
.mail-assignee {
    display: inline-flex; align-items: center;
    flex: 0 0 auto;
    margin-left: .35rem;
}
/* Several holders (#613): one pile, each face ringed by the surface. */
.mail-assignee > .mail-assignee-avatar + .mail-assignee-avatar { margin-left: -5px; }
.mail-assignee-more {
    margin-left: 4px;
    font-size: 10px; font-weight: 600;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}
.mail-assignee-avatar {
    width: 18px; height: 18px;
    border-radius: 50%;
    background: var(--avatar-bg); color: var(--avatar-color);
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 9px; font-weight: 700;
    text-transform: uppercase;
    border: 1.5px solid var(--surface);
    box-shadow: 0 0 0 1px var(--border);
}

/* ── Compose CTA in the mail sidebar + floating compose card ════════
   The button at the top of the sidebar opens a Gmail-style compose
   card pinned to the bottom-right of the viewport. The card hosts
   the same rich-text editor used in the reply flow, plus chip-style
   recipient inputs and a Cc/Bcc reveal toggle. */
/* Compose CTA - slim, premium, single-color. The earlier chunky
   gradient + tinted icon-box version felt loud against the calm
   sidebar; this version reads as a confident primary action without
   shouting. Solid primary background, refined shadow, no decorative
   icon wrapper. */
.mail-compose-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: .45rem;
    width: calc(100% - 1.2rem);
    margin: .9rem .6rem .75rem;
    padding: .5rem .9rem;
    height: 36px;
    background: var(--text);
    color: var(--surface, #fff);
    border: 0;
    border-radius: 10px;
    cursor: pointer;
    font-family: inherit;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: -.005em;
    line-height: 1;
    box-shadow:
        0 1px 1px rgba(15, 23, 42, .06),
        0 4px 10px -4px rgba(15, 23, 42, .28);
    transition: background .12s ease, box-shadow .15s ease, transform .15s ease;
}
.mail-compose-btn:hover {
    background: color-mix(in srgb, var(--text) 84%, var(--surface));
    box-shadow:
        0 1px 2px rgba(15, 23, 42, .08),
        0 6px 14px -4px rgba(15, 23, 42, .32);
    transform: translateY(-1px);
}
.mail-compose-btn:active {
    transform: translateY(0);
    box-shadow: 0 1px 2px rgba(15, 23, 42, .08);
}
.mail-compose-btn:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}
.mail-compose-btn-icon {
    flex: 0 0 auto;
    opacity: .95;
}
.mail-compose-btn-label { flex: 0 0 auto; }
/* Read-only / archive mailboxes hide the Compose button - its
   .9rem top margin would otherwise have provided the breathing
   room before "All mail". Without compensation, the folder list
   sat flush against the sidebar header (the user reported it
   looked cramped). When .list-group is the first child of the
   sidebar body (i.e. the compose CTA wasn't rendered), give it
   the equivalent top margin the button's wrapper used to. */
.archive-sidebar-body > .list-group:first-child {
    margin-top: .9rem;
}

/* Compose shell - the full-viewport overlay that hosts the floating card.
   Backdrop is faint so the user can still see the inbox they're
   composing from; clicking it does NOT close (avoids losing a draft to
   a stray click - close requires the explicit ✕ button). */
.mail-compose-shell {
    position: fixed;
    inset: 0;
    z-index: 1080;
    pointer-events: none;
}
.mail-compose-shell[hidden] { display: none; }
.mail-compose-shell-backdrop {
    position: absolute; inset: 0;
    background: rgba(15, 23, 42, .12);
    backdrop-filter: blur(1px);
    pointer-events: auto;
    animation: mail-compose-fade .18s ease;
}
@keyframes mail-compose-fade { from { opacity: 0; } to { opacity: 1; } }

.mail-compose-card {
    position: absolute;
    bottom: 16px; right: 16px;
    width: min(620px, calc(100vw - 32px));
    max-height: calc(100vh - 32px);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    box-shadow: 0 24px 60px -12px rgba(15, 23, 42, .35),
                0 6px 14px -4px rgba(15, 23, 42, .15);
    display: flex; flex-direction: column;
    overflow: hidden;
    pointer-events: auto;
    animation: mail-compose-pop .22s cubic-bezier(.2,.8,.3,1.2);
}
@keyframes mail-compose-pop {
    from { opacity: 0; transform: translateY(20px) scale(.96); }
    to   { opacity: 1; transform: none; }
}
.mail-compose-shell.is-minimized .mail-compose-card {
    width: 320px;
    max-height: 48px;
    border-radius: 12px 12px 0 0;
    bottom: 0;
}
.mail-compose-shell.is-minimized .mail-compose-card-form,
.mail-compose-shell.is-minimized .mail-compose-shell-backdrop { display: none; }
.mail-compose-shell.is-maximized .mail-compose-card {
    inset: 24px;
    width: auto;
    max-height: none;
}

/* Neutral premium window chrome: quiet surface + hairline, ink title - the
   violet gradient title bar was the last filled-accent surface standing
   (user: "still the purple color in mail composer"). */
.mail-compose-card-head {
    flex: 0 0 auto;
    display: flex; align-items: center; justify-content: space-between;
    padding: .55rem .65rem .55rem .85rem;
    background: var(--surface-2, #f1f5f9);
    color: var(--text);
    border-bottom: 1px solid var(--border);
    cursor: default;
    user-select: none;
}
.mail-compose-card-title {
    display: inline-flex; align-items: center; gap: .5rem;
    font-size: 12px; font-weight: 700;
    letter-spacing: -.005em;
}
.mail-compose-card-icon { opacity: .9; color: var(--text-muted); }
.mail-compose-card-actions { display: inline-flex; gap: 2px; }
.mail-compose-card-btn {
    width: 28px; height: 28px;
    display: inline-flex; align-items: center; justify-content: center;
    background: transparent;
    border: 0;
    border-radius: 6px;
    color: var(--text-muted);
    cursor: pointer;
    transition: background .12s ease, color .12s ease;
}
.mail-compose-card-btn:hover { background: color-mix(in srgb, var(--text) 8%, transparent); color: var(--text); }
.mail-compose-card-btn-danger:hover { background: rgba(239, 68, 68, .85); color: #fff; }

.mail-compose-card-form {
    flex: 1 1 auto;
    overflow-y: auto;
    display: flex; flex-direction: column;
    padding: .85rem 1rem 1rem;
    gap: .55rem;
}

/* Inline send-error banner inside the compose card. Shown when the
   AJAX submit comes back with success=false; the form keeps its
   content so the user can fix the issue and retry without losing
   their typed message. */
.mail-compose-card-error {
    background: var(--danger-bg);
    border: 1px solid color-mix(in srgb, var(--danger) 35%, var(--border));
    color: var(--danger);
    padding: .55rem .75rem;
    border-radius: 8px;
    font-size: 12px;
    line-height: 1.45;
    word-break: break-word;
}
.mail-compose-card-error[hidden] { display: none; }
.mail-compose-card-from {
    display: flex; align-items: center; gap: .5rem;
    padding: .25rem 0 .55rem;
    border-bottom: 1px solid var(--surface-2);
    font-size: 12px;
}
.mail-compose-card-from-label { color: var(--text-subtle); width: 50px; flex: 0 0 auto; font-weight: 500; }
.mail-compose-card-from-value { color: var(--text); font-weight: 500; }
.mail-compose-card-from-email { color: var(--text-muted); font-weight: 400; }
/* "you are sending as [name]" identity chip on the new-mail
   composer. Sits at the right end of the From row, after a faint
   separator, so the workspace identity stays primary and the
   teammate identity reads as a delegate-of relationship. The
   `as` separator is muted; the avatar + name picks up the
   canonical hue paint via .bf-avatar-letter.
   Pinned by MailLocalSenderAttributionTests. */
.mail-compose-card-as {
    display: inline-flex; align-items: center; gap: .35rem;
    margin-left: auto;
    padding-left: .65rem;
    color: var(--text);
    font-weight: 500;
    font-size: 11px;
}
.mail-compose-card-as-sep {
    color: var(--text-subtle);
    font-weight: 400;
    margin-right: .15rem;
}
.mail-compose-card-as-avatar {
    width: 20px !important; height: 20px !important;
    font-size: 10px !important; line-height: 20px !important;
    border-radius: 50% !important;
    flex: 0 0 auto;
}
.mail-compose-card-as-name {
    color: var(--text);
    font-weight: 500;
    max-width: 12rem;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.mail-compose-card-row {
    display: flex; align-items: center; gap: .5rem;
    padding: .15rem 0;
    border-bottom: 1px solid var(--surface-2);
}
.mail-compose-card-row label {
    width: 50px; flex: 0 0 auto;
    color: var(--text-subtle);
    font-size: 12px;
    font-weight: 500;
    margin: 0;
}
.mail-compose-card-row input[type="text"] {
    flex: 1 1 auto;
    border: 0;
    background: transparent;
    padding: .35rem 0;
    font-size: 13px;
    color: var(--text);
    outline: none;
    min-width: 0;
}
.mail-compose-card-row input[type="text"]:focus { background: transparent; }
.mail-compose-card-row input::placeholder { color: var(--text-subtle); }
/* Compose-flavoured chip-input: strips the standalone-picker chrome
   (border + background + 48px min-height + rounded corners) so the
   To / Cc / Bcc rows visually match the plain input rows around them.
   ANY row wrapper that hosts a `.mail-compose-chip-input` picker (the
   compose-new card AND the inline reply card both do) gets the same
   inline aesthetic - we target the chip-input class directly rather
   than scoping under a row-class prefix because the project has TWO
   row-class names for what is fundamentally the same compose row
   (`.mail-compose-card-row` on Pages/Mail/Index.cshtml,
   `.msg-compose-row` on Pages/Mail/Message.cshtml). Scoping under
   one of them produced exactly the user-reported drift where the
   compose-new card had the right height but the reply card was
   still a stack of 48-px min-height boxes. Pinned by
   ChipInvitePickerHiddenFieldsAreLiveTests.MailCompose_ChipInputClass_IsTheSharedHook. */
/* Selectors chain `.chip-input.mail-compose-chip-input` (two
   classes) so they out-rank the bare `.chip-input` rule that
   lives further down the file (which sets min-height: 48px /
   padding: .35rem .5rem / border / radius). Equal-specificity
   rules would lose to whichever comes later in source order;
   the chained selector wins by specificity instead, regardless
   of rule order. */
.chip-input.mail-compose-chip-input {
    flex: 1 1 auto;
    min-width: 0;
    min-height: auto;
    padding: 0;
    border: 0;
    background: transparent;
    border-radius: 0;
    box-shadow: none;
    align-items: center;
}
.chip-input.mail-compose-chip-input.is-focused {
    box-shadow: none;
    border: 0;
}
.chip-input.mail-compose-chip-input .chip-input-entry {
    padding: .35rem 0;
    font-size: 13px;
    min-width: 0;
    flex: 1 1 120px;
}
.chip-input.mail-compose-chip-input .chip-input-entry::placeholder {
    color: var(--text-subtle);
}
.mail-compose-card-cc-toggle {
    flex: 0 0 auto;
    background: transparent;
    border: 0;
    color: var(--text-muted);
    font-size: 11px;
    font-weight: 600;
    padding: .25rem .5rem;
    border-radius: 6px;
    cursor: pointer;
    transition: background .12s ease, color .12s ease;
}
.mail-compose-card-cc-toggle:hover { background: var(--surface-2); color: var(--text); }

.mail-compose-card-editor {
    margin-top: .35rem;
    flex: 1 1 auto;
    min-height: 200px;
}
.mail-compose-card-editor .compose-body {
    min-height: 200px;
    max-height: 320px;
}

.mail-compose-card-attachments {
    display: flex; flex-wrap: wrap; align-items: center; gap: .5rem;
    margin-top: .25rem;
}
.mail-compose-card-attach-pick {
    display: inline-flex; align-items: center; gap: .35rem;
    padding: .35rem .65rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--surface);
    color: var(--text-muted);
    font-size: 11px;
    font-weight: 500;
    cursor: pointer;
    transition: border-color .12s ease, color .12s ease, background .12s ease;
    margin: 0;
}
.mail-compose-card-attach-pick:hover { border-color: color-mix(in srgb, var(--text) 25%, transparent); color: var(--text); background: color-mix(in srgb, var(--text) 5%, transparent); }
.mail-compose-card-attach-pick input[type="file"] { display: none; }
.mail-compose-card-attach-hint { color: var(--text-subtle); font-size: 11px; }
.mail-compose-card-attach-list { display: flex; flex-wrap: wrap; gap: .35rem; flex: 1 1 100%; }
.mail-compose-card-attach-list:empty { display: none; }
.mail-compose-card-attach-chip {
    display: inline-flex; align-items: center; gap: .35rem;
    padding: .25rem .25rem .25rem .55rem;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 999px;
    font-size: 11px;
    color: var(--text);
}
.mail-compose-card-attach-chip-name { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mail-compose-card-attach-chip-size { color: var(--text-muted); font-size: 10px; }
.mail-compose-card-attach-chip-remove {
    width: 18px; height: 18px;
    display: inline-flex; align-items: center; justify-content: center;
    border: 0;
    background: transparent;
    color: var(--text-muted);
    border-radius: 50%;
    cursor: pointer;
    transition: background .12s ease, color .12s ease;
}
.mail-compose-card-attach-chip-remove:hover { background: rgba(239, 68, 68, .15); color: #b91c1c; }

/* Inline warning rendered by the shared mail-attach-picker
   helper when a pick exceeds the per-file / total / count limit.
   Sits just below the chip list. Auto-dismisses after 5s; the
   :not(:empty) gate keeps an empty placeholder from rendering as
   a visible bar. Same warning element is reused across both
   compose-new (Mail/Index.cshtml) and reply (Mail/Message.cshtml)
   surfaces - one rule, one look. */
.mail-attach-warning {
    display: block;
    margin: 6px 0 0;
    padding: 6px 10px;
    font-size: 11px;
    line-height: 1.4;
    color: #b91c1c;
    background: color-mix(in srgb, #b91c1c 8%, var(--surface));
    border: 1px solid color-mix(in srgb, #b91c1c 28%, var(--border));
    border-radius: 8px;
}
.mail-attach-warning:empty { display: none; }

.mail-compose-card-validation {
    color: #b91c1c;
    background: color-mix(in srgb, #b91c1c 8%, var(--surface));
    border: 1px solid color-mix(in srgb, #b91c1c 28%, var(--border));
    border-radius: 8px;
    padding: .5rem .65rem;
    font-size: 11px;
}

.mail-compose-card-foot {
    display: flex; align-items: center; justify-content: space-between;
    gap: .85rem;
    padding-top: .5rem;
    border-top: 1px solid var(--surface-2);
    margin-top: .25rem;
}
.mail-compose-card-hint { flex: 1 1 auto; min-width: 0; color: var(--text-muted); transition: color .12s; }
.mail-compose-card-hint.is-saved { color: var(--success, #059669); }
.mail-compose-card-hint.is-error { color: var(--danger, #b91c1c); }
.mail-compose-card-foot-actions { flex: 0 0 auto; display: inline-flex; gap: .35rem; }
.mail-compose-card-foot-actions .btn-primary {
    display: inline-flex; align-items: center;
}
.mail-compose-card-foot-actions .btn.is-sending {
    /* In-flight send: the spinner inside replaces the icon, the label
       reads "Sending…", the button stays in a clearly-active visual
       (no greyed-out cancel-look) so the user reads it as "your send
       is in progress" not "something failed". */
    opacity: 1;
    cursor: progress;
    background: color-mix(in srgb, var(--text) 84%, var(--surface));
    pointer-events: none;
}
/* Send button while a fetch is in flight. We drop the disabled
   attribute (Bootstrap's :disabled cascade dims the button + its
   children, hiding the spinner) and rely on `.is-sending` +
   pointer-events to block re-clicks. !important on background and
   opacity defeats any later .btn-primary rule that might creep in. */
.mail-compose-card-foot-actions .btn.is-sending,
.mail-compose-card-foot-actions .btn-primary.is-sending {
    background: var(--text) !important;
    color: #fff !important;
    opacity: 1 !important;
    border-color: var(--primary) !important;
    cursor: progress;
    pointer-events: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: .35rem;
}
.mail-compose-card-foot-actions .btn.is-sending .bulk-spin {
    /* The spinner itself: white track, brighter white head, bigger
       than the chrome's default so it's legible on the small btn-sm
       footprint. !important pins the colors against any later rule
       that would re-inherit the button's color. */
    border: 2px solid rgba(255, 255, 255, .4) !important;
    border-top-color: #fff !important;
    width: 14px !important;
    height: 14px !important;
    flex: 0 0 auto;
    vertical-align: middle;
}
.mail-compose-card-foot-actions .btn.is-sending > span:not(.bulk-spin) {
    color: #fff !important;
}

/* The compose card during a send: every input + button (except
   the Discard / X close button) is .disabled so the user can't
   mutate the draft mid-flight. The shell-level class adds a
   subtle desaturation + no-pointer cursor so the frozen state
   reads as "in progress" rather than "broken". */
.mail-compose-shell.is-sending .mail-compose-card-form {
    pointer-events: none;
    filter: saturate(.85);
}
.mail-compose-shell.is-sending .mail-compose-card-form * {
    cursor: progress !important;
}
/* But the Discard button MUST stay clickable - it's the user's
   cancel-the-send escape hatch. Re-enable pointer-events on the
   discard control + restore its normal cursor. */
.mail-compose-shell.is-sending [data-compose-new-close] {
    pointer-events: auto;
    cursor: pointer !important;
    opacity: 1;
}
/* The Send button keeps `is-sending` styling regardless of the
   shell-level filter so the spinner reads as bright purple, not
   washed out. */
.mail-compose-shell.is-sending .btn.is-sending {
    filter: none;
    pointer-events: none;
}

@media (max-width: 720px) {
    .mail-compose-card { right: 0; bottom: 0; width: 100%; border-radius: 10px 10px 0 0; max-height: 90vh; }
}
.mail-compose-card-row.is-invalid input { color: #b91c1c; }

/* ── "Select all matching the filter" banner ════════════════════════
   Sits between the bulk-bar and the message list. Shows up when every
   row on the page is ticked AND the matching set is bigger than the
   page (so there's actually more to select). Visual is borrowed from
   Gmail / Superhuman: a slim tinted strip with centered copy, bold
   primary link, no harsh borders. Has two states:
     - normal  : "All N on this page are selected. <Select all M …>"
     - extended: "All M in Folder are selected. <Clear selection>"
   The extended variant uses a green tint to flag that the action will
   target far more than the visible page.

   CLS guard: the slot keeps a FIXED height regardless of state. When
   the banner has nothing to say it stays in flow with visibility:hidden
   so the message list never shifts when content appears or changes
   between the two banner phrasings. */
.bulk-banner {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: nowrap;            /* don't grow vertically when text is long */
    gap: .55rem;
    padding: 0 1rem;
    height: 36px;                  /* fixed - same across all states */
    box-sizing: border-box;
    background: var(--primary-bg);
    border-bottom: 1px solid color-mix(in srgb, var(--primary) 22%, var(--surface-2));
    font-size: 12px;
    line-height: 1.2;
    color: var(--text);
    text-align: center;
    overflow: hidden;
}
/* Override UA's [hidden] default of display:none - the slot must keep
   its 36px so toggling on/off doesn't push the message list. */
.bulk-banner[hidden] {
    display: flex;
    visibility: hidden;
    background: transparent;
    border-bottom-color: var(--surface-2);
}
.bulk-banner-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.bulk-banner-text {
    color: var(--text);
    font-weight: 500;
}
.bulk-banner-action {
    background: transparent;
    border: 0;
    padding: 0;
    margin: 0;
    color: var(--primary);
    font-weight: 700;
    font-size: inherit;
    cursor: pointer;
    text-decoration: none;
    transition: color .12s ease, text-decoration-color .12s ease;
    text-decoration: underline;
    text-decoration-color: transparent;
    text-underline-offset: 3px;
    text-decoration-thickness: 2px;
}
.bulk-banner-action:hover,
.bulk-banner-action:focus-visible {
    color: var(--text);
    text-decoration-color: var(--text);
    outline: 0;
}
/* Extended state - the user already opted into "all matching"; the tint
   shifts to a "this is a heavy operation" green so it doesn't look
   identical to the suggestion banner. */
.bulk-banner.is-extended {
    background: color-mix(in srgb, #15803d 12%, var(--surface));
    border-bottom-color: color-mix(in srgb, #15803d 28%, var(--surface-2));
}
/* Issues-list flavour. The Mail toolbar reserves a 36px slot for the
   banner even when hidden so its strict flex layout doesn't reflow;
   the Issues list has no such constraint, so we let `hidden` truly
   collapse the slot - cleaner empty state when no cross-page extension
   is offered. */
.issues-bulk-banner[hidden] { display: none; }
.issues-bulk-banner {
    border-radius: 8px;
    margin: 0 0 .5rem 0;
    border: 1px solid color-mix(in srgb, var(--primary) 22%, var(--surface-2));
}
.bulk-banner.is-extended .bulk-banner-text { color: #14532d; }
.bulk-banner.is-extended .bulk-banner-action {
    color: #15803d;
}
.bulk-banner.is-extended .bulk-banner-action:hover,
.bulk-banner.is-extended .bulk-banner-action:focus-visible {
    color: #166534;
    text-decoration-color: #166534;
}
@keyframes bulk-banner-in {
    from { opacity: 0; transform: translateY(-3px); }
    to   { opacity: 1; transform: none; }
}
html[data-theme="dark"] .bulk-banner {
    background: color-mix(in srgb, var(--primary) 14%, var(--surface));
}
html[data-theme="dark"] .bulk-banner.is-extended {
    background: color-mix(in srgb, #15803d 18%, var(--surface));
}
html[data-theme="dark"] .bulk-banner.is-extended .bulk-banner-text { color: #bbf7d0; }
html[data-theme="dark"] .bulk-banner.is-extended .bulk-banner-action { color: #4ade80; }
@media (max-width: 720px) {
    .bulk-banner { padding: .55rem .75rem; font-size: 11px; }
}

/* ==== "Label as" popover anchored to the bulk-bar button ============== */
.bulk-label-wrap { position: relative; display: inline-flex; }
.label-pop {
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    z-index: 60;
    width: 280px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    box-shadow: 0 12px 32px -8px rgba(15,23,42,.2), 0 4px 8px -2px rgba(15,23,42,.08);
    padding: 0;
    animation: label-pop-in .12s ease;
    display: flex; flex-direction: column;
    max-height: 380px;
    overflow: hidden;
}
@keyframes label-pop-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
.label-pop[hidden] { display: none; }
.label-pop-head {
    flex: 0 0 auto;
    padding: .55rem .55rem .4rem;
    border-bottom: 1px solid var(--surface-2);
}
.label-pop-search {
    width: 100%;
    padding: .4rem .55rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    font-size: 12px;
    color: var(--text);
    background: var(--surface);
    outline: 0;
    transition: border-color .12s ease, box-shadow .12s ease;
}
.label-pop-search:focus { border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-bg); }
.label-pop-list {
    flex: 1 1 auto;
    overflow-y: auto;
    padding: .25rem .25rem;
    /* Zero-CLS: every .label-pop consumer (message toolbar Label
       as / Move to, bulk-selection Label / Move) opens on a
       one-line "Loading…" and fills after a fetch. Reserve the
       typical filled height (~6 rows) so the popover paints at its
       final size and rows land into the reserved frame; longer
       lists scroll inside the 380px cap instead of growing it. */
    min-height: 208px;
}
.label-pop-item {
    display: flex; align-items: center; gap: .55rem;
    padding: .4rem .55rem;
    border-radius: 6px;
    cursor: pointer;
    font-size: 12px;
    color: var(--text);
    user-select: none;
    transition: background .08s ease;
}
.label-pop-item:hover { background: var(--surface-2); }
/* Single-select Move-to row: a button, not a checkbox <label>. Reset
   the native button chrome so it sits flush like the label rows, and
   mute the leading folder/tag glyph that stands in for the checkbox. */
.label-pop-move-row {
    width: 100%;
    border: 0;
    background: transparent;
    color: var(--text);
    font: inherit;
    font-size: 12px;
    text-align: left;
}
.label-pop-move-row:focus-visible { outline: none; background: var(--surface-2); }
.label-pop-move-icon { flex: 0 0 auto; color: currentColor; opacity: .55; }
/* Spam / Trash pinned below the scrolling folder list in the Move-to
   popover. A hairline sets them apart from the user folders; each
   carries its triage tint on hover so the destructive intent reads
   before the click (red Trash, amber Spam). */
.label-pop-fixed {
    flex: 0 0 auto;
    border-top: 1px solid var(--surface-2);
    padding: .25rem;
}
.label-pop-fixed .label-pop-move-icon { opacity: .8; }
/* Trash reads destructive - red at rest (matching the main-view
   Trash), red-tinted background on hover. Icon is currentColor so it
   reddens with the text. */
.label-pop-item.is-trash { color: #b91c1c; }
.label-pop-item.is-trash:hover,
.label-pop-item.is-trash:focus-visible {
    background: color-mix(in srgb, #b91c1c 8%, var(--surface));
    color: #b91c1c;
}
.label-pop-item.is-spam:hover,
.label-pop-item.is-spam:focus-visible {
    background: color-mix(in srgb, var(--warning) 10%, var(--surface));
    color: #b45309;
}
.label-pop-item input[type="checkbox"] {
    width: 14px; height: 14px;
    accent-color: var(--primary);
    cursor: pointer;
    flex: 0 0 auto;
}
.label-pop-item .label-pop-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.label-pop-item .label-pop-state {
    font-size: 10px; font-weight: 600;
    color: var(--text-muted);
    flex: 0 0 auto;
    text-transform: uppercase;
    letter-spacing: .03em;
}
.label-pop-item.is-some .label-pop-state { color: var(--primary); }
.label-pop-item.is-pending-add { background: var(--primary-bg); }
.label-pop-item.is-pending-add .label-pop-state { color: var(--primary); }
.label-pop-item.is-pending-add .label-pop-state::before { content: "+ "; }
.label-pop-item.is-pending-remove .label-pop-state { color: #b91c1c; }
.label-pop-item.is-pending-remove .label-pop-state::before { content: "− "; }
/* Brief highlight for a label the user just created via "+ Create label".
   Pulses the row's outline so the eye lands on its (alphabetical)
   resting place after the auto-scroll. */
.label-pop-item.is-just-created {
    animation: label-pop-flash 1.4s ease;
}
@keyframes label-pop-flash {
    0% { box-shadow: inset 0 0 0 2px var(--primary); background: color-mix(in srgb, var(--primary) 18%, transparent); }
    60% { box-shadow: inset 0 0 0 2px var(--primary); background: var(--primary-bg); }
    100% { box-shadow: inset 0 0 0 0 transparent; }
}
.label-pop-empty {
    padding: 1.2rem .8rem;
    text-align: center;
    color: var(--text-muted);
    font-size: 11px;
    line-height: 1.4;
}
.label-pop-create {
    flex: 0 0 auto;
    border-top: 1px solid var(--surface-2);
    padding: .35rem;
}
.label-pop-create-btn {
    width: 100%;
    display: inline-flex; align-items: center; gap: .5rem;
    padding: .45rem .55rem;
    border: 0;
    background: transparent;
    color: var(--primary);
    border-radius: 6px;
    cursor: pointer;
    font-size: 12px; font-weight: 600;
    text-align: left;
    transition: background .08s ease;
}
.label-pop-create-btn:hover { background: color-mix(in srgb, var(--text) 5%, transparent); }
.label-pop-create-btn strong { font-weight: 700; }
.label-pop-foot {
    flex: 0 0 auto;
    padding: .5rem;
    border-top: 1px solid var(--surface-2);
    display: flex; gap: .4rem; justify-content: flex-end;
    background: linear-gradient(to top, var(--surface-2), var(--surface));
}
/* The action toolbar can scroll horizontally on narrow screens; the
   popover should stay anchored visually even then. */
@media (max-width: 820px) {
    .label-pop { width: 260px; }
}

.pagination-controls {
    display: flex;
    align-items: center;
    gap: 2px;
    flex: 1 1 auto;
    justify-content: center;
    flex-wrap: wrap;
}
.pagination-controls .pg-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 32px;
    height: 32px;
    padding: 0 .55rem;
    font-size: 12px;
    font-weight: 500;
    color: var(--text);
    background: transparent;
    border: 1px solid transparent;
    border-radius: 8px;
    text-decoration: none;
    transition: all .1s ease;
    cursor: pointer;
    user-select: none;
}
.pagination-controls .pg-btn:hover {
    background: var(--surface-2);
    border-color: var(--border);
    color: var(--text);
}
.pagination-controls .pg-btn.active {
    background: color-mix(in srgb, var(--text) 7%, transparent);
    color: var(--text);
    font-weight: 600;
    border-color: transparent;
}
.pagination-controls .pg-btn.active:hover {
    background: color-mix(in srgb, var(--text) 11%, transparent);
    border-color: transparent;
    color: var(--text);
}
.pagination-controls .pg-btn.disabled,
.pagination-controls .pg-btn[aria-disabled="true"] {
    opacity: .35;
    pointer-events: none;
    cursor: default;
}
.pagination-controls .pg-ellipsis {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 24px;
    height: 32px;
    color: var(--text-subtle);
    font-size: 13px;
    padding: 0 .15rem;
    user-select: none;
}
.pagination-controls .pg-btn[title="First page"],
.pagination-controls .pg-btn[title="Last page"],
.pagination-controls .pg-btn[title="Previous"],
.pagination-controls .pg-btn[title="Next"] {
    color: var(--text-muted);
    font-weight: 500;
    padding: 0 .7rem;
}
.pagination-controls .pg-btn[title="Previous"],
.pagination-controls .pg-btn[title="Next"] {
    padding: 0 .9rem;
}

.pagination-jump {
    display: flex;
    align-items: center;
    gap: .4rem;
    font-size: 12px;
    color: var(--text-muted);
    flex: 0 0 auto;
}
.pagination-jump label { margin: 0; color: var(--text-muted); }
.pagination-jump input {
    width: 56px;
    height: 32px; /* level with the 32px .pg-btn squares beside it */
    padding: 0 .45rem;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    font-size: 12px;
    font-variant-numeric: tabular-nums;
    color: var(--text);
    background: var(--surface);
    text-align: center;
}
.pagination-jump input:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 3px rgba(79,70,229,.15);
}
.pagination-jump .pg-of { color: var(--text-muted); font-variant-numeric: tabular-nums; }

@media (max-width: 720px) {
    .pagination-bar { justify-content: center; }
    .pagination-range, .pagination-jump { flex: 1 1 100%; justify-content: center; text-align: center; }
}

/* Message view */
.msg-header { padding: 1.25rem 1.5rem; border-bottom: 1px solid var(--border); background: var(--surface); }
.msg-subject { font-size: 1.25rem; font-weight: 700; margin-bottom: .5rem; }
.msg-meta { font-size: 12px; color: var(--text-muted); }
.msg-body { padding: 1.5rem; }
.msg-attachments { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1rem; }
.msg-attachment-chip {
    display: inline-flex; align-items: center; gap: .5rem;
    padding: .5rem .85rem;
    background: var(--surface-2);
    border-radius: 8px;
    font-size: 12px;
    color: var(--text);
    text-decoration: none;
    border: 1px solid var(--border);
}
.msg-attachment-chip:hover { background: color-mix(in srgb, var(--text) 5%, transparent); color: var(--text); }

/* Modal */
.modal-content { border: 1px solid var(--border); border-radius: 12px; box-shadow: var(--shadow); }
.modal-header { border-bottom: 1px solid var(--border); }
.modal-footer { border-top: 1px solid var(--border); }

/* Chip-based invitee picker -------------------------------------------- */
.chip-input {
    position: relative;
    display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem;
    min-height: 48px;
    padding: 0.35rem 0.5rem;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--surface);
    cursor: text;
    transition: border-color 120ms ease, box-shadow 120ms ease;
}
.chip-input.is-focused { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15); }
.chip-input.is-invalid { border-color: #ef4444; animation: chip-shake 240ms ease; }
@keyframes chip-shake {
    0%, 100% { transform: translateX(0); }
    25% { transform: translateX(-3px); }
    75% { transform: translateX(3px); }
}
.chip-input-chips { display: inline-flex; flex-wrap: wrap; gap: 0.3rem; }
/* Recipient chips wear the platform's quiet grammar: hairline
   border, plain ink, small radius - never a filled colored pill. */
.chip {
    display: inline-flex; align-items: center; gap: 0.35rem;
    padding: 0.2rem 0.35rem 0.2rem 0.25rem;
    border-radius: 8px;
    background: transparent;
    border: 1px solid var(--border);
    color: var(--text);
    font-size: 12px;
    line-height: 1.3;
    max-width: 280px;
}
.chip-avatar {
    width: 20px; height: 20px; border-radius: 50%;
    background: var(--avatar-bg); color: var(--avatar-color);
    font-size: 10px; font-weight: 700;
    display: inline-flex; align-items: center; justify-content: center;
    flex: 0 0 auto;
}
.chip-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
/* Team chip variant: same pill geometry but a richer slug + dual-line
   readout (name + member count). Visually distinct from a user chip
   so the user sees at a glance which entries are individuals and
   which are whole teams. */
.chip.chip-team {
    background: transparent;
    border: 1px solid var(--border);
    padding-right: .55rem;
    max-width: 360px;
}
.chip-team .chip-team-icon {
    background: var(--text);
    color: var(--surface, #fff);
    width: 22px; height: 22px;
    display: inline-flex; align-items: center; justify-content: center;
}
.chip-team .chip-team-icon svg { width: 12px; height: 12px; }
/* Member count = a quiet fact, not a badge: plain muted text. */
.chip-team-count {
    margin-left: .35rem;
    font-size: 11px; font-weight: 500;
    color: var(--text-muted);
}
.chip-remove {
    background: transparent; border: none; cursor: pointer;
    color: var(--text-muted);
    font-size: 14px; line-height: 1; padding: 0 0.25rem;
    opacity: 0.55;
}
.chip-remove:hover { opacity: 1; }

/* Chip drag-and-drop between fields in the same drag-group (mail
   To / Cc / Bcc). A draggable chip shows a grab cursor; the one
   being dragged dims; the field under the pointer signals it will
   accept the drop with a dashed primary border (no filled tint -
   the affordance rides on border + ring, consistent with the
   focus / invalid states above). */
.chip-draggable { cursor: grab; }
.chip-draggable:active { cursor: grabbing; }
.chip-dragging { opacity: 0.4; }
.chip-input.chip-drop-active {
    border-style: dashed;
    border-color: var(--primary);
    box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
}

.chip-input-entry {
    flex: 1 1 160px;
    min-width: 140px;
    border: none; outline: none; background: transparent;
    font-size: 13px; padding: 0.35rem 0.25rem;
    color: var(--text);
}

/* Suggestion dropdown. Closed it sits in its slot under the field; OPEN it
   floats in viewport space - the picker portals it under <body> and sets
   its geometry from the field's live rect (chip-invite-picker.js), so a
   host that clips or scrolls (a modal card, the signature assistant's
   step) never hides a suggestion below the fold. */
.chip-suggest {
    position: absolute;
    top: calc(100% + 6px);
    left: 0; right: 0;
    max-height: 260px;
    overflow-y: auto;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
    z-index: 30;
    padding: 0.25rem;
}
/* The same layer as the bf-select list: above every surface that can host
   the picker, modals included. */
.chip-suggest.is-floating { position: fixed; top: auto; right: auto; z-index: 6100; }
.chip-suggest-item {
    display: flex; align-items: center; gap: 0.65rem;
    padding: 0.45rem 0.6rem;
    border-radius: 8px;
    cursor: pointer;
}
.chip-suggest-item.is-active,
.chip-suggest-item:hover { background: color-mix(in srgb, var(--text) 5%, transparent); }
.chip-suggest-avatar {
    width: 28px; height: 28px; border-radius: 50%;
    background: var(--avatar-bg);
    color: var(--avatar-color);
    display: inline-flex; align-items: center; justify-content: center;
    font-weight: 600; font-size: 11px;
    flex: 0 0 auto;
}
.chip-suggest-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.3; }
.chip-suggest-label { font-weight: 600; color: var(--text); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chip-suggest-sub { color: var(--text-muted); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Team row in the suggester. Same shape as a user row but the avatar
   slot is a team-icon glyph instead of a person photo, and a small
   "team" tag sits next to the name so the user knows picking expands
   to all members. */
.chip-suggest-item.is-team .chip-suggest-team-icon {
    background: color-mix(in srgb, var(--text) 6%, transparent);
    color: var(--text-muted);
    display: inline-flex; align-items: center; justify-content: center;
}
/* The TEAM tag follows the badge grammar: hairline, muted ink,
   small radius - never a filled colored pill. */
.chip-suggest-team-tag {
    margin-left: .4rem;
    font-size: 9px; font-weight: 600;
    color: var(--text-muted); background: transparent;
    border: 1px solid var(--border);
    padding: 1px 6px; border-radius: 4px;
    letter-spacing: .04em; text-transform: uppercase;
    vertical-align: middle;
}

/* Section title ------------------------------------------------------- */
.collab-list-section { margin-top: 1.5rem; }
.collab-list-head {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    margin: 0 0 0.6rem 0.1rem;
}
.collab-section-title {
    font-size: 12px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
    margin: 0;
}
.collab-list-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 22px;
    height: 22px;
    padding: 0 7px;
    border-radius: 12px;
    background: var(--primary-bg);
    color: var(--primary);
    font-size: 10px;
    font-weight: 600;
}

.coworker-list { list-style: none; padding: 0; margin: 0; }
.coworker-row {
    display: flex; align-items: center; gap: .85rem;
    padding: .85rem 1.4rem;
    transition: background .12s ease;
}
.coworker-row + .coworker-row { border-top: 1px solid var(--border); }
.coworker-row:hover { background: var(--surface-2); }
.coworker-avatar {
    flex: 0 0 auto;
    width: 40px; height: 40px;
    border-radius: 50%;
    background: var(--avatar-bg);
    color: var(--avatar-color);
    display: inline-flex; align-items: center; justify-content: center;
    font-weight: 700; font-size: 13px;
    text-transform: uppercase;
    box-shadow: 0 0 0 2px var(--surface);
    overflow: hidden;
}
img.coworker-avatar { object-fit: cover; }
.coworker-text {
    flex: 1 1 auto; min-width: 0;
    display: flex; flex-direction: column; gap: 1px;
    line-height: 1.3;
}
.coworker-name {
    font-size: 13px; font-weight: 600; color: var(--text);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    display: inline-flex; align-items: center; gap: .45rem;
}
.coworker-email {
    font-size: 11px; color: var(--text-muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    display: inline-flex; align-items: center; gap: 4px;
}
.coworker-email-text {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    min-width: 0;
}
.coworker-row:hover .coworker-email .copy-text-btn,
.coworker-email .copy-text-btn:focus-visible { opacity: 1; }

/* Canonical copy-button (used wherever <partial name="_CopyTextButton" />
   renders). Icon-only, hover-revealed by default, flashes a check
   glyph on success via the .is-copied class added by copy-text.js.
   The two SVGs sit stacked - only one is visible at a time per
   .is-copied. Layout-only here; per-surface placement uses an
   ancestor hover selector (see .coworker-row:hover above) to
   reveal the resting button. */
.copy-text-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--text-muted);
    border-radius: 6px;
    cursor: pointer;
    opacity: 0;
    flex-shrink: 0;
    transition: opacity .14s ease, color .14s ease, background-color .14s ease;
}
.copy-text-btn:hover {
    color: var(--text);
    background: color-mix(in srgb, var(--text) 5%, transparent);
}
.copy-text-btn:focus-visible {
    outline: none;
    opacity: 1;
    box-shadow: 0 0 0 2px color-mix(in oklch, var(--primary) 35%, transparent);
}
.copy-text-btn-check { display: none; }
.copy-text-btn.is-copied .copy-text-btn-icon { display: none; }
.copy-text-btn.is-copied .copy-text-btn-check {
    display: inline-block;
    color: var(--success);
}
.copy-text-btn.is-copied {
    color: var(--success);
    opacity: 1;
}

/* Canonical "workspace settings & info" affordance (used wherever
   <partial name="_WorkspaceManageLink" /> renders - every workspace
   type's home page header). Same visual language as the copy button:
   icon-only, muted, brightens to the primary accent on hover / focus.
   Discreet by design - it sits quietly next to the workspace name at
   rest (low opacity) and comes forward on hover so it never competes
   with the title, yet stays discoverable. */
.ws-manage-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--text-muted);
    border-radius: 6px;
    cursor: pointer;
    opacity: .5;
    flex-shrink: 0;
    text-decoration: none;
    transition: opacity .14s ease, color .14s ease, background-color .14s ease;
}
.ws-manage-link:hover {
    opacity: 1;
    color: var(--text);
    background: color-mix(in srgb, var(--text) 5%, transparent);
}
.ws-manage-link:focus-visible {
    outline: none;
    opacity: 1;
    color: var(--primary);
    box-shadow: 0 0 0 2px color-mix(in oklch, var(--primary) 35%, transparent);
}
.ws-manage-link-icon {
    width: 15px;
    height: 15px;
}
/* Groups a workspace home-page title (heading or breadcrumb) with its
   trailing _WorkspaceManageLink so the discreet manage icon sits on the
   same line, right after the workspace name. flex (not inline-flex) so
   an ellipsizing title still gets a shrinkable track; the icon holds its
   size. */
.ws-manage-row {
    display: flex;
    align-items: center;
    gap: 6px;
    min-width: 0;
}
.ws-manage-row > :first-child { min-width: 0; }

/* Inline "email address + copy" unit (Pages/Shared/_EmailAddress.cshtml).
   Groups a displayed address with the canonical copy button so the
   button reveals on hover / focus and a long address truncates with an
   ellipsis. Layout + reveal only - the address keeps its own surface
   class for colour and size. Used on every surface that shows an email. */
.bf-email {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    min-width: 0;
    max-width: 100%;
    vertical-align: baseline;
}
.bf-email-text {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}
.bf-email:hover .copy-text-btn,
.bf-email:focus-within .copy-text-btn { opacity: 1; }

/* Mail header recipient lines (to / cc / reply-to) keep the address
   list free to wrap, so the copy button can't live in a .bf-email
   wrapper - reveal it on hover of the whole line instead. */
.sender-recip:hover .copy-text-btn,
.sender-recip:focus-within .copy-text-btn { opacity: 1; }

.coworker-meta {
    font-size: 11px; color: var(--text-muted);
    display: inline-flex; gap: .25rem; flex-wrap: wrap;
}
.coworker-meta strong { color: var(--text); font-weight: 600; }
.coworker-meta-sep { color: var(--text-subtle); }
.coworker-tag {
    flex: 0 0 auto;
    font-size: 9px; font-weight: 700; letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--primary);
    background: var(--primary-bg);
    padding: 1px 7px;
    border-radius: 999px;
    border: 1px solid color-mix(in srgb, var(--primary) 25%, var(--border));
    line-height: 1.4;
}
/* Clickable variant: full-row button that opens the shared-context modal.
   Resets the default <button> chrome (background, border) so the row
   reads as a card row, not a control. The chevron telegraphs that the
   row leads somewhere - cheap UX cue without needing a tooltip. */
button.coworker-row-clickable {
    width: 100%;
    background: transparent;
    border: 0;
    text-align: left;
    cursor: pointer;
    color: inherit;
    font: inherit;
}
button.coworker-row-clickable:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: -2px;
    background: var(--surface-2);
}
.coworker-row-chevron {
    flex: 0 0 auto;
    color: var(--text-subtle);
    opacity: .5;
    transition: opacity .12s ease, transform .12s ease;
}
button.coworker-row-clickable:hover .coworker-row-chevron {
    opacity: 1;
    transform: translateX(2px);
}

/* Flash animation for /Teams#team-N anchor focus. Pulse the border +
   shadow once so the eye locks onto the right card after navigating
   from the coworker-context modal. */
@keyframes team-card-focus-flash {
    0%   { box-shadow: 0 0 0 0    color-mix(in srgb, var(--primary) 50%, transparent); border-color: var(--primary); }
    50%  { box-shadow: 0 0 0 12px color-mix(in srgb, var(--primary) 0%,  transparent); border-color: var(--primary); }
    100% { box-shadow: 0 0 0 0    transparent; }
}
.team-card.is-focus-flash,
.invite-card.is-focus-flash,
.account-card.is-focus-flash,
.note.is-focus-flash {
    animation: team-card-focus-flash 1.6s ease-out;
    border-color: var(--primary) !important;
}

/* Sub-page-title rhythm. */
.page-title-secondary { font-size: 1.3rem; }

/* Collaborator list --------------------------------------------------- */
.collab-list { list-style: none; padding: 0; margin: 0; }
.collab-row {
    border-bottom: 1px solid var(--border);
    padding: 1.1rem 1.4rem;
}
.collab-row:last-child { border-bottom: none; }

.collab-row-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
}
.collab-identity {
    display: flex;
    align-items: flex-start;
    gap: 0.85rem;
    flex: 1 1 auto;
    min-width: 0;
}
.collab-identity-text { min-width: 0; flex: 1 1 auto; }

.collab-avatar {
    width: 40px; height: 40px;
    border-radius: 50%;
    background: var(--avatar-bg); color: var(--avatar-color);
    display: inline-flex; align-items: center; justify-content: center;
    font-weight: 600;
    font-size: 14px;
    flex-shrink: 0;
}

.collab-email {
    font-weight: 600;
    color: var(--text);
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    font-size: 14px;
}
.collab-email-sub {
    font-size: 12px;
    color: var(--text-muted);
    margin-top: 0.05rem;
}
.collab-meta {
    margin-top: 0.3rem;
    font-size: 12px;
    color: var(--text-muted);
    display: flex;
    gap: 0.35rem;
    align-items: center;
    flex-wrap: wrap;
}
.collab-meta strong { color: var(--text); font-weight: 600; }
.collab-meta-sep { opacity: 0.5; }

/* Badges ------------------------------------------------------------- */
.collab-badge {
    display: inline-block;
    padding: 0.12rem 0.55rem;
    border-radius: 999px;
    font-size: 10px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    line-height: 1.5;
}
.collab-badge-pending { background: #fef3c7; color: #92400e; }

/* Granted/pending workspace rows ------------------------------------- */
.collab-ws-list {
    margin-top: 0.85rem;
    margin-left: 3.4rem;
    border-left: 2px solid var(--border);
    padding-left: 0.9rem;
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}
.collab-ws-list-pending { border-left-color: #fcd34d; }

.collab-ws-row {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.35rem 0;
    font-size: 12px;
}
.collab-ws-row-icon {
    width: 20px; height: 20px;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}
/* a bare glyph in its status's colour: never a pastel plate behind a picto (owner, 2026-10-21) */
.collab-ws-row-icon svg { width: 14px; height: 14px; }
.collab-ws-row-icon-granted { color: #047857; }
.collab-ws-row-icon-pending { color: #b45309; }

.collab-ws-email-text { color: var(--text); }
.collab-ws-granted {
    color: var(--text-muted);
    font-size: 11px;
    margin-left: auto;
}
.collab-ws-row-action {
    display: inline-flex;
    gap: 0.3rem;
    margin-left: 0.5rem;
}
.collab-ws-row .collab-copy.is-copied { color: var(--success, #047857); }

/* Generic icon-only button + copy-link affordance.
   Used by the [data-copy-link] buttons on Resend rows across
   /Notifications, /Teams, and the Workspaces Manage modal. The
   .is-copied class is toggled briefly by Burofolk.copyLink when the
   write-to-clipboard succeeds, giving the operator a green-flash
   confirmation. */
.btn-icon-only {
    padding: 0.3rem 0.45rem;
    line-height: 1;
}
.btn-icon-only svg { display: block; }
.invite-copy-btn {
    color: var(--text-muted);
    transition: color 0.12s ease, background 0.12s ease;
}
.invite-copy-btn:hover { color: var(--text); }
.invite-copy-btn.is-copied { color: var(--success, #047857); }

/* Grant another workspace ------------------------------------------- */
.collab-grant-form {
    margin: 0.85rem 0 0 3.4rem;
    padding-top: 0.75rem;
    border-top: 1px dashed var(--border);
    display: flex;
    gap: 0.5rem;
    align-items: center;
}
.collab-grant-form .form-select { flex: 1 1 auto; max-width: 360px; }

/* btn-xs (extra-small button) -------------------------------------- */
.btn-xs {
    padding: 0.2rem 0.55rem;
    font-size: 11px;
    border-radius: 6px;
    line-height: 1.3;
}

/* Flash error variant ---------------------------------------------- */
.account-flash-error {
    background: #fee2e2;
    color: #7f1d1d;
    border: 1px solid #fca5a5;
}

/* The workspace permissions modal's styles retired 2026-08-05 with the
   modal: member permissions render as inline .ws-perm-* category rows
   on the Detail page (see the collapsible-category block). */

/* ======================================================================
   Incoming-invitations inbox on /Workspaces
   Design goals: make it feel like a distinct interactive zone, clear
   inviter identity, obvious Accept / Decline. One card per invitation so
   dense multi-invite inboxes stay scannable.
   ====================================================================== */
.invite-inbox { display: flex; flex-direction: column; gap: 0.75rem; width: 100%; }
.invite-inbox-header {
    display: flex; flex-direction: column; gap: 0.25rem;
    padding: 0 0.15rem;
}
.invite-inbox-pill {
    align-self: flex-start;
    display: inline-flex; align-items: center; gap: 0.4rem;
    padding: 0.3rem 0.75rem;
    border-radius: 999px;
    background: var(--primary-bg);
    color: var(--primary);
    font-size: 11px; font-weight: 600;
    letter-spacing: 0.02em;
}
.invite-inbox-pill svg { width: 12px; height: 12px; }
.invite-inbox-sub { font-size: 12px; color: var(--text-muted); line-height: 1.4; }

/* Card stack - each invitation its own surface */
.invite-card-stack { display: flex; flex-direction: column; gap: 0.65rem; }
.invite-card {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 1.1rem 1.25rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
    position: relative;
    overflow: hidden;
    transition: border-color 140ms ease, box-shadow 140ms ease, transform 140ms ease;
}
.invite-card::before {
    content: "";
    position: absolute;
    left: 0; top: 0; bottom: 0;
    width: 3px;
    background: var(--primary);
}
.invite-card:hover {
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
    box-shadow: 0 4px 18px rgba(99, 102, 241, 0.12);
}

.invite-card-main { display: flex; align-items: center; gap: 0.9rem; min-width: 0; flex: 1 1 auto; }
.invite-card-avatar {
    width: 44px; height: 44px;
    border-radius: 50%;
    background: var(--avatar-bg);
    color: var(--avatar-color);
    font-weight: 700;
    font-size: 15px;
    display: inline-flex; align-items: center; justify-content: center;
    flex-shrink: 0;
    box-shadow: 0 2px 8px rgba(99, 102, 241, 0.25);
}

.invite-card-text { min-width: 0; flex: 1 1 auto; }
.invite-card-line {
    font-size: 14px;
    color: var(--text);
    line-height: 1.4;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.25rem;
}
.invite-card-inviter { font-weight: 600; color: var(--text); }
.invite-card-connector { color: var(--text-muted); font-weight: 400; }
.invite-card-workspace { font-weight: 600; color: var(--primary); }

.invite-card-meta { display: flex; gap: 0.4rem; margin-top: 0.45rem; flex-wrap: wrap; }
.invite-card-chip {
    display: inline-flex; align-items: center; gap: 0.3rem;
    padding: 0.18rem 0.55rem;
    border-radius: 999px;
    font-size: 10px;
    font-weight: 500;
    border: 1px solid var(--border);
    color: var(--text-muted);
    background: var(--surface-2);
}
.invite-card-chip svg { flex-shrink: 0; }
.invite-card-chip-clock { background: #fff7ed; color: #9a3412; border-color: #fed7aa; }

.invite-card-actions {
    display: flex;
    gap: 0.45rem;
    flex-shrink: 0;
    align-items: center;
}

/* Dark theme */
[data-theme="dark"] .invite-card-chip-clock {
    background: rgba(234, 88, 12, 0.12);
    color: #fdba74;
    border-color: rgba(234, 88, 12, 0.35);
}
[data-theme="dark"] .invite-inbox-pill {
    background: var(--primary-bg);
}

/* Responsive: stack actions below meta on narrow viewports */
@media (max-width: 640px) {
    .invite-card { flex-direction: column; align-items: stretch; }
    .invite-card-actions { justify-content: flex-end; }
}

/* ======================================================================
   Universal avatar image rule
   -----------------------------------------------------------------------
   When a user has uploaded a profile photo, AvatarHelper.UserAvatar emits
   `<img class="{existing-bubble-class} avatar-img" src=...>` - same
   container class as the initial bubble (so size/border-radius/shadow
   carry over) plus this one rule that makes the photo fill the circle
   correctly without stretching.
   ====================================================================== */
img.avatar-img {
    object-fit: cover;
    object-position: center;
    background: var(--surface-2);
}

/* ======================================================================
   Profile page
   ====================================================================== */
.profile-page { max-width: 720px; margin: 0 auto; }
.profile-card .card-body { padding: 2rem 2.2rem; }
.profile-header { display: flex; align-items: center; gap: 1.25rem; margin-bottom: 1.75rem; }

/* Avatar + "click to change" camera button overlay. The wrap is
   position:relative so the edit button can sit in the bottom-right. */
.profile-avatar-wrap { position: relative; flex: 0 0 auto; }
.profile-avatar {
    width: 72px; height: 72px; border-radius: 50%;
    background: var(--avatar-bg); color: var(--avatar-color);
    font-size: 1.9rem; font-weight: 700;
    display: inline-flex; align-items: center; justify-content: center;
    flex: 0 0 auto;
    box-shadow: 0 4px 14px rgba(99, 102, 241, 0.2);
    overflow: hidden;
}
.profile-avatar-img { object-fit: cover; }
.profile-avatar-edit-btn {
    position: absolute; right: -2px; bottom: -2px;
    width: 28px; height: 28px; border-radius: 50%;
    background: var(--surface);
    color: var(--primary);
    border: 2px solid var(--surface);
    display: inline-flex; align-items: center; justify-content: center;
    cursor: pointer;
    box-shadow: 0 2px 8px rgba(15, 23, 42, 0.15);
    transition: transform 120ms ease, background-color 120ms ease;
}
.profile-avatar-edit-btn:hover { background: var(--text); color: var(--surface, #fff); transform: scale(1.05); }
.profile-avatar-remove { margin-top: 0.4rem; }
.profile-header-meta { min-width: 0; flex: 1 1 auto; }
.profile-name { font-size: 17px; font-weight: 650; letter-spacing: -0.01em; color: var(--text); overflow-wrap: anywhere; }
.profile-email { font-size: 13px; font-weight: 500; color: var(--text-muted); margin-top: 0.1rem; }
.profile-meta { font-size: 12px; color: var(--text-muted); margin-top: 0.2rem; display: flex; gap: 0.4rem; flex-wrap: wrap; align-items: center; }
.profile-meta-sep { opacity: 0.5; }

.profile-form .form-control-lg { font-size: 14px; padding: 0.7rem 0.9rem; border-radius: 10px; }
/* Two-column row for short profile fields - role+location and
   linkedin+website each share width on desktop, stack under 720px. */
.profile-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: .75rem 1rem;
}
@media (max-width: 720px) {
    .profile-row { grid-template-columns: 1fr; }
}

.profile-hint {
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 0.9rem 1.1rem;
    margin: 1rem 0 1.5rem;
}
.profile-hint-title { font-weight: 600; font-size: 12px; color: var(--text); margin-bottom: 0.25rem; }

.profile-actions { display: flex; justify-content: flex-end; gap: 0.5rem; padding-top: 0.25rem; border-top: 1px solid var(--border); margin-top: 0.75rem; padding-top: 1rem; }

.profile-danger .card-body { padding: 1.5rem 2rem; }
/* The two session verbs share ONE row, 8px apart, and drop whole onto the
   next line when the frame is too narrow - never a wrapped label. */
.profile-session-verbs { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.profile-session-verbs .btn { white-space: nowrap; }

/* Avatar crop modal ------------------------------------------------ */
.avatar-picker {
    display: flex; align-items: center; gap: 0.75rem;
    flex-wrap: wrap;
    padding: 0.75rem 0.9rem;
    border: 1px dashed var(--border);
    border-radius: 10px;
    background: var(--surface-2);
    margin-bottom: 1rem;
}
.avatar-picker-btn { cursor: pointer; margin: 0; }
.avatar-picker-hint { flex: 1 1 180px; }

.crop-area {
    position: relative;
    width: 256px; height: 256px;
    margin: 0 auto 1rem;
    background: #000;
    border-radius: 12px;
    overflow: hidden;
    touch-action: none;
}
.crop-area canvas {
    display: block;
    width: 256px; height: 256px;
    cursor: grab;
    user-select: none;
}
.crop-area canvas:active { cursor: grabbing; }
/* Circular preview mask - the crop is actually saved as a 256x256 PNG
   (square) but we show the circle so the user sees how it will look
   everywhere else in Burofolk. */
.crop-mask {
    position: absolute; inset: 0;
    pointer-events: none;
    background:
        radial-gradient(circle at center, transparent 0 50%, rgba(15, 23, 42, 0.55) 51% 100%);
    border-radius: 12px;
}

.crop-zoom { display: flex; align-items: center; gap: 0.7rem; }
.crop-zoom-label { font-size: 12px; color: var(--text-muted); flex-shrink: 0; }
.crop-zoom input[type="range"] { flex: 1 1 auto; }

/* Dark theme overrides --------------------------------------------- */
[data-theme="dark"] .collab-badge-pending { background: rgba(252, 211, 77, 0.15); color: #fcd34d; }
[data-theme="dark"] .collab-ws-row-icon-granted { color: var(--success); }
[data-theme="dark"] .collab-ws-row-icon-pending { color: #fcd34d; }
[data-theme="dark"] .account-flash-error { background: rgba(239, 68, 68, 0.12); color: #fca5a5; border-color: rgba(239, 68, 68, 0.35); }

/* Responsive ------------------------------------------------------- */
@media (max-width: 640px) {
    .collab-ws-list, .collab-grant-form { margin-left: 0; border-left: none; padding-left: 0; }
}

/* Avatars rendered with a userId become clickable profile-card triggers
   via the global JS in _Layout.cshtml. The class adds the affordances
   (pointer cursor, focus ring, gentle hover lift) without touching the
   per-surface bubble styling. */
.is-clickable {
    cursor: pointer;
    transition: transform 120ms ease, box-shadow 120ms ease, filter 120ms ease;
}
.is-clickable:hover { transform: translateY(-1px); filter: brightness(1.05); }
.is-clickable:focus-visible {
    outline: 2px solid var(--bs-primary, #4f46e5);
    outline-offset: 2px;
}

/* Profile card loading skeleton. While the /Users/{id}/profile fetch
   is in flight the modal carries .is-loading and we draw shimmer
   placeholders in each slot - feels much faster than a blank card.
   The skeleton blocks are sized to the typical content they replace
   so the resolved content doesn't shift the layout. */
@keyframes profile-skeleton-shimmer {
    0%   { background-position: -200% 0; }
    100% { background-position:  200% 0; }
}
.profileCardModal--skeleton-bg,
[data-profile-card].is-loading [data-profile-name]::after,
[data-profile-card].is-loading [data-profile-email]::after,
[data-profile-card].is-loading .collab-profile-avatar::after {
    background: linear-gradient(90deg,
        var(--surface-2) 0%,
        color-mix(in srgb, var(--primary) 8%, var(--surface-2)) 50%,
        var(--surface-2) 100%);
    background-size: 200% 100%;
    animation: profile-skeleton-shimmer 1.2s ease-in-out infinite;
}
[data-profile-card].is-loading [data-profile-name],
[data-profile-card].is-loading [data-profile-email] { position: relative; min-height: 1.2em; }
[data-profile-card].is-loading [data-profile-name]::after {
    content: ""; position: absolute; left: 50%; top: 50%;
    width: 60%; height: 1.2em; transform: translate(-50%, -50%);
    border-radius: 6px;
}
[data-profile-card].is-loading [data-profile-email]::after {
    content: ""; position: absolute; left: 50%; top: 50%;
    width: 75%; height: .85em; transform: translate(-50%, -50%);
    border-radius: 4px;
}
[data-profile-card].is-loading .collab-profile-avatar { position: relative; color: transparent; }
[data-profile-card].is-loading .collab-profile-avatar::after {
    content: ""; position: absolute; inset: 0; border-radius: 50%;
}
[data-profile-card].is-error .collab-profile-avatar {
    background: var(--surface-2);
    color: var(--text-subtle);
    box-shadow: 0 0 0 5px var(--surface), 0 8px 18px rgba(15, 23, 42, .15);
}
[data-profile-card].is-error .collab-profile-avatar::before {
    content: "?";
    font-size: 2rem; font-weight: 700;
}
[data-profile-card].is-error .collab-profile-name { color: var(--text-muted); font-size: 15px; }
[data-profile-card].is-error .collab-profile-email { color: var(--text-subtle); font-size: 11px; line-height: 1.5; }

/* ============================================================
   Team-card popover. Sibling of the profile-card popover so the
   two surfaces share visual language. Reuses .collab-profile-card
   shell + .collab-profile-section + .collab-profile-footer; the
   .collab-team-* classes only add team-specific bits (icon glyph
   over the banner, owner row, member list, "Owner" badge).
   ============================================================ */
.collab-team-card .collab-team-banner {
    /* Banner doubles as the icon-glyph stage. Centre the glyph
       over the gradient so the team's identity reads first. */
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 110px;
}
.collab-team-card .collab-team-glyph-wrap {
    /* The glyph wrapper sits OVER the banner. The .bf-icon
       inside owns its own size + radius via the canonical CSS
       custom properties; we just bump them here so the bubble
       reads as the dominant visual anchor on the card. */
    --bf-icon-size: 72px;
    --bf-icon-radius: 18px;
    box-shadow: 0 0 0 5px var(--surface), 0 12px 28px rgba(15, 23, 42, .18);
    border-radius: 18px;
    overflow: hidden;
}
.collab-team-card .collab-team-glyph-wrap .bf-icon {
    /* Force the bubble to draw exactly inside the wrap so the
       drop-shadow + radius read cleanly without bleed. */
    margin: 0;
}

/* Member-row / owner-row shared paint. Both rows look the same
   so the card reads as one rhythm, with an "Owner" badge as the
   only distinguishing mark. */
.collab-team-card .collab-team-roster {
    display: flex;
    flex-direction: column;
    gap: .35rem;
}
.collab-team-card .collab-team-member-row {
    display: flex;
    align-items: center;
    gap: .65rem;
    width: 100%;
    padding: .45rem .55rem;
    border: 1px solid transparent;
    border-radius: 10px;
    background: var(--surface);
    text-align: left;
    cursor: pointer;
    transition: background-color .15s ease, border-color .15s ease;
}
.collab-team-card .collab-team-member-row:hover,
.collab-team-card .collab-team-member-row:focus-visible {
    background: var(--surface-2);
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
    outline: none;
}
.collab-team-card .collab-team-member-avatar {
    flex: 0 0 auto;
    width: 32px;
    height: 32px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Constrain the canonical avatar render. Burofolk.renderAvatarInto
       drops an <img class="avatar-img"> at its natural size into the
       wrapper; without overflow:hidden + a child width/height rule the
       img blows past the 32px box and explodes the row. Same shape as
       .collab-profile-avatar for the user-card. */
    overflow: hidden;
    border-radius: 50%;
    background: var(--surface-2);
}
.collab-team-card .collab-team-member-avatar img,
.collab-team-card .collab-team-member-avatar .avatar-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 50%;
}
/* Letter-bubble fallback: when no photo is uploaded, the canonical
   helper paints a .bf-avatar-letter span on the wrapper itself, so
   the wrapper IS the bubble. Centre the letter glyph. */
.collab-team-card .collab-team-member-avatar.bf-avatar-letter {
    font-size: 11px;
    font-weight: 700;
}
.collab-team-card .collab-team-member-text {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    min-width: 0;
}
.collab-team-card .collab-team-member-name {
    font-weight: 600;
    font-size: 13px;
    color: var(--text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.collab-team-card .collab-team-member-meta {
    font-size: 10px;
    color: var(--text-subtle);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.collab-team-card .collab-team-member-badge {
    flex: 0 0 auto;
    font-size: 9px;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--text-muted);
    background: transparent;
    padding: .15rem .4rem;
    border-radius: 6px;
}

/* Members-hidden hint for outside viewers. Soft text so it reads
   as a status note, not a rejection. */
.collab-team-card .collab-team-members-hidden {
    font-size: 11px;
    color: var(--text-subtle);
    background: var(--surface-2);
    padding: .55rem .75rem;
    border-radius: 8px;
    line-height: 1.45;
}

/* Loading + error states symmetric to the profile-card. */
[data-team-card].is-loading [data-team-name] { position: relative; min-height: 1.2em; color: transparent; }
[data-team-card].is-loading [data-team-name]::after {
    content: ""; position: absolute; left: 50%; top: 50%;
    width: 60%; height: 1.2em; transform: translate(-50%, -50%);
    border-radius: 6px;
    background: linear-gradient(90deg,
        var(--surface-2) 0%,
        color-mix(in srgb, var(--primary) 8%, var(--surface-2)) 50%,
        var(--surface-2) 100%);
    background-size: 200% 100%;
    animation: profile-skeleton-shimmer 1.2s ease-in-out infinite;
}
[data-team-card].is-error [data-team-name] {
    color: var(--text-muted);
    font-size: 15px;
}

/* ============================================================
   Home page premium redesign (brand moves 1-6).
   Asymmetric hero, brand-color stat numbers, per-workspace
   identity stripes, illustrated empty state, brand-tinted
   backdrop, and a sticky bottom utility rail. Each block
   overrides the centered/generic version above; nothing
   here changes layout outside .home-landing.
   ============================================================ */

/* Subtle brand-tinted backdrop on every non-inbox page (the
   inbox keeps a clean reading surface). Pinned via fixed
   positioning so it stays put as the page scrolls; pointer-
   events disabled so it never intercepts a click. body class
   `is-inbox` opts out (set by _Layout.cshtml on /Mail routes). */
body.is-app::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: -1;
    background:
        radial-gradient(ellipse 80% 60% at 15% 10%, rgba(6, 182, 212, .06), transparent 60%),
        radial-gradient(ellipse 70% 60% at 85% 30%, rgba(6, 182, 212, .05), transparent 60%),
        radial-gradient(ellipse 90% 70% at 50% 110%, rgba(236, 72, 153, .05), transparent 60%);
    pointer-events: none;
}
html[data-theme="dark"] body.is-app::before {
    background:
        radial-gradient(ellipse 80% 60% at 15% 10%, rgba(6, 182, 212, .14), transparent 60%),
        radial-gradient(ellipse 70% 60% at 85% 30%, rgba(34, 211, 238, .10), transparent 60%),
        radial-gradient(ellipse 90% 70% at 50% 110%, rgba(236, 72, 153, .12), transparent 60%);
}

/* Retired split-hero layout. The "Welcome back." stats cluster
   was deleted in the home-as-work-surface redesign - the
   landing page now renders the three primary widgets directly
   on viewport entry. The minimal welcome hero used by the
   brand-new-user path (no workspaces yet) carries its own
   `.home-hero` base rules above. Pinned by
   HomeHeroLegacyTilesRetiredTests. */

/* ──────────────────────────────────────────────────────────
   Move 3: per-workspace identity stripe on each card. Hue is
   passed in as a CSS custom property by the Razor view (from
   WorkspaceAccent.CssColor). 4px stripe on the leading edge,
   60% saturation / 60% lightness so it reads on both light
   and dark themes.
   ────────────────────────────────────────────────────────── */
.quick-open-card { position: relative; overflow: hidden; }
/* Hairline workspace accent: pulled from 4px to 2px and softened
   with a 70% opacity so a stack of pinned + recent rows reads as
   a calm column, not a rainbow of competing identity bars. The
   colour vocabulary (per-workspace hue) is preserved; only the
   visual weight comes down. Earlier iteration ran the stripe at
   full 4px / 100%, which dominated the eye and made the workspace
   name + counts read as secondary metadata. */
.quick-open-stripe {
    position: absolute;
    left: 0; top: 0; bottom: 0;
    width: 2px;
    background: var(--bf-color, var(--sys-blue));
    opacity: .7;
    border-radius: 2px 0 0 2px;
}
/* Lift accent on hover so the row still telegraphs identity when
   the user is actually engaging with it. */
.quick-open-card:hover .quick-open-stripe { opacity: 1; }

/* Ghost "connect another" tile drops the stripe entirely - it
   doesn't represent a real workspace identity, and the calmer
   visual reads as a tertiary affordance per the redesign. */
.quick-open-card.quick-open-add .quick-open-stripe {
    display: none;
}

/* ──────────────────────────────────────────────────────────
   Hot-workspace dot. A small colored bead on the icon's top-
   right corner that signals "this workspace saw notable
   activity in the last 24h" - mounted by _WorkspaceCardIcon
   when Activity24hCount crosses HotWorkspaceDotThreshold.
   Tooltip carries the exact count; the dot itself is a pure
   yes / no signal (no numbers in the visual). Paint uses the
   workspace's own accent color so the dot reads as part of
   the same identity vocabulary as the stripe. The white ring
   (box-shadow) lifts it off the icon bubble at any background
   shade. Subtle pulse animation in light mode invites the eye
   without becoming a busy spinner; reduced-motion suppresses
   it entirely.
   ────────────────────────────────────────────────────────── */
.quick-open-icon-wrap {
    position: relative;
    flex: 0 0 auto;
    display: inline-flex;
    line-height: 0;
}
/* "Hot workspace" dot: single workspace-accent colour, three
   intensity tiers driven by OPACITY only. Same canonical hue
   the rest of the workspace chrome uses (var(--bf-color)) so
   the dot reads as "this workspace's signal" - not as a
   parallel colour scheme the user has to decode against.
   Differentiation rides on transparency alone, which matches
   the user's intent "no colour change, just transparency".

   Tiers (set by _WorkspaceCardIcon.cshtml):
     .is-warm    (3-9 events)   → 40% opacity  (faint)
     .is-hot     (10-29 events) → 70% opacity  (clear)
     .is-blazing (30+ events)   → 100% opacity (full intensity)

   Size stays fixed at 9px across tiers so the row's right-edge
   layout never shifts when activity level changes. */
.quick-open-hot-dot {
    position: absolute;
    top: -2px;
    right: -2px;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: var(--bf-color, var(--sys-blue));
    box-shadow: 0 0 0 2px var(--bf-dash-row-bg, var(--surface));
    pointer-events: none;
    /* Default tier is full intensity. Tier overrides below dial
       opacity down for lower activity levels; if no tier class
       lands the dot still reads strongly. */
    opacity: 1;
    transition: opacity 200ms ease;
}
.quick-open-hot-dot.is-warm    { opacity: 0.40; }
.quick-open-hot-dot.is-hot     { opacity: 0.70; }
.quick-open-hot-dot.is-blazing { opacity: 1;    }
html[data-theme="dark"] .quick-open-hot-dot {
    box-shadow: 0 0 0 2px var(--bg);
}

/* ──────────────────────────────────────────────────────────
   Move 4: illustrated empty-state for Recent activity.
   Replaces the apologetic one-liner with a centered 3-bar
   mark, friendly heading, and a single CTA.
   ────────────────────────────────────────────────────────── */
.home-activity-empty {
    display: flex; flex-direction: column; align-items: center;
    text-align: center;
    padding: 2.5rem 1.25rem;
    gap: .85rem;
}
.home-activity-empty-mark {
    width: 56px; height: 56px;
    opacity: .85;
    transition: transform .25s cubic-bezier(.34, 1.56, .64, 1);
}
.home-card-activity:hover .home-activity-empty-mark { transform: rotate(-4deg) scale(1.04); }
.home-activity-empty-title {
    font-family: "Plus Jakarta Sans", "Inter", -apple-system, system-ui, sans-serif;
    font-weight: 700;
    font-size: 15px;
    color: var(--text);
    margin: 0;
    letter-spacing: -.01em;
}
.home-activity-empty-sub {
    font-size: 12px;
    color: var(--text-muted);
    line-height: 1.55;
    max-width: 320px;
    margin: 0;
}
.home-activity-empty-cta {
    margin-top: .35rem;
    display: inline-flex; align-items: center; gap: .35rem;
    /* Tighter pill: vertical padding pulled back, horizontal too,
       so the button hugs its label like the buttons in Apple's
       marketing surfaces. Inset 1px highlight at the top gives
       the dark surface a soft bevel that lifts it off the page
       without a heavy drop-shadow. Backdrop-filter is harmless
       here even though the CTA sits over a flat surface - if
       the empty-state ever gets a tinted background, the glass
       feel kicks in automatically. */
    padding: .42rem .75rem;
    border-radius: 999px;
    background: var(--text);
    color: var(--surface);
    font-weight: 600; font-size: 12px;
    letter-spacing: -.005em;
    text-decoration: none;
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, .08),
        0 1px 2px rgba(15, 23, 42, .12);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    transition: background .15s ease, transform .08s ease, box-shadow .15s ease;
}
.home-activity-empty-cta:hover {
    background: var(--text);
    color: var(--surface, #fff);
    transform: translateY(-1px);
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, .14),
        0 6px 16px -4px color-mix(in srgb, var(--primary) 40%, transparent);
}
.home-activity-empty-cta svg { transition: transform .15s ease; }
.home-activity-empty-cta:hover svg { transform: translateX(2px); }

/* ============================================================
   Workspace identity stripes + accent picker. Same hue source
   as the home-page workspace tiles (Burofolk.Services.WorkspaceAccent),
   so a workspace reads with the same color across surfaces.
   ============================================================ */

/* Stripe on /Workspaces card. Anchored top-to-bottom on the
   leading edge. The .account-row already has overflow visible
   for status dots etc., so the stripe is positioned absolutely
   inside the row's box - no clip needed. */
.account-row { position: relative; }

/* Color trigger button on each owner card. A dot in the
   workspace's accent color opens the accent modal. Sits in the
   icon-button row beside the gear and remove buttons. */
.ws-accent-trigger { padding: .35rem !important; }
.ws-accent-dot {
    display: inline-block;
    width: 16px; height: 16px;
    border-radius: 50%;
    background: var(--bf-color, var(--sys-blue));
    box-shadow: 0 0 0 2px var(--surface), 0 0 0 3px color-mix(in srgb, var(--bf-color, var(--sys-blue)) 25%, transparent);
    transition: transform .12s ease;
}
.ws-accent-trigger:hover .ws-accent-dot { transform: scale(1.1); }

/* Accent picker modal -------------------------------------- */
.accent-picker { margin: 0; }
.accent-preview {
    display: flex;
    align-items: center;
    gap: .85rem;
    padding: .85rem .95rem;
    background: var(--surface-2);
    border-radius: 12px;
    margin-bottom: 1rem;
    position: relative;
    overflow: hidden;
}
.accent-preview-stripe {
    position: absolute;
    left: 0; top: 0; bottom: 0;
    width: 4px;
    background: var(--bf-color, var(--sys-blue));
    border-radius: 4px 0 0 4px;
}
.accent-preview-text {
    flex: 1 1 auto;
    min-width: 0;
    display: flex; flex-direction: column;
    gap: .15rem;
    padding-left: .35rem;
}
.accent-preview-label {
    font-weight: 600;
    color: var(--text);
    font-size: 13px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.accent-preview-name {
    font-size: 11px;
    color: var(--text-muted);
}
.accent-preview-swatch {
    flex: 0 0 auto;
    width: 28px; height: 28px;
    border-radius: 50%;
    background: var(--bf-color, var(--sys-blue));
    box-shadow: inset 0 0 0 2px rgba(255,255,255,.4), 0 1px 3px rgba(15,23,42,.15);
}

/* 4 x 3 swatch grid. Smaller circles than before so they read
   as a palette, not a clown poster. */
.accent-swatches {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    gap: .5rem;
}
.accent-swatch {
    aspect-ratio: 1 / 1;
    border-radius: 50%;
    border: 0;
    background: var(--bf-color, var(--sys-blue));
    cursor: pointer;
    padding: 0;
    position: relative;
    transition: transform .12s ease;
}
.accent-swatch:hover { transform: scale(1.08); }
/* Active state: white check inside a soft inner ring - no
   secondary box-shadow halo (the previous "green ring around a
   yellow swatch" was hue-derived but read as a UI bug). */
.accent-swatch.is-active {
    box-shadow: inset 0 0 0 3px rgba(255,255,255,.55);
}
.accent-swatch.is-active::after {
    content: "";
    position: absolute; inset: 0;
    background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='20 6 9 17 4 12'/></svg>") center/45% no-repeat;
    filter: drop-shadow(0 1px 1px rgba(0,0,0,.25));
}

/* Custom-colour swatch: rainbow conic gradient at rest with a
   hidden native <input type="color"> on top so the OS picker
   takes over on click. */
.accent-swatch-custom {
    background: conic-gradient(
        from 0deg,
        #EF4444, #F59E0B, #EAB308, #84CC16, #22C55E,
        #14B8A6, #06B6D4, #3B82F6, #8B5CF6, #EC4899, #EF4444);
    color: var(--text);
    display: inline-flex; align-items: center; justify-content: center;
    cursor: pointer;
    overflow: hidden;
}
.accent-swatch-custom svg {
    width: 18px; height: 18px;
    background: var(--surface);
    border-radius: 50%;
    padding: 4px;
    box-shadow: 0 1px 2px rgba(15,23,42,.2);
}
.accent-swatch-custom input[type="color"] {
    position: absolute;
    inset: 0;
    opacity: 0;
    cursor: pointer;
    border: 0;
    padding: 0;
}

/* No-colour swatch - picks "transparent" as a first-class
   colour choice. Hollow circle with a diagonal slash inside
   reads as "ban / none" universally. When active, the slash
   stays visible (it's the swatch's identity) AND the
   canonical white-check overlay still appears on top. */
.accent-swatch-transparent {
    background: var(--surface, #fff);
    color: var(--text-muted);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-shadow: inset 0 0 0 1px var(--border, #e2e8f0);
}
.accent-swatch-transparent svg {
    width: 60%;
    height: 60%;
}
.accent-swatch-transparent.is-active {
    color: var(--text);
    box-shadow: inset 0 0 0 2px var(--primary);
}
.accent-swatch-transparent.is-active::after {
    /* Override the standard active check with a tinted version
       so it sits well on the white surface. */
    background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23111827' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='20 6 9 17 4 12'/></svg>") center/45% no-repeat;
    filter: none;
}
.accent-swatch-transparent.is-active svg {
    /* Hide the slash when active - the check takes over as the
       active-state indicator. */
    opacity: 0;
}

/* Preview pane when "no colour" is the pending pick: drop
   the gradient + name colour so the preview row reflects
   what the page will actually look like. */
.accent-preview.is-transparent .accent-preview-swatch {
    background: var(--surface, #fff);
    box-shadow: inset 0 0 0 1px var(--border, #e2e8f0);
}
.accent-preview.is-transparent .accent-preview-stripe {
    background: transparent;
    box-shadow: inset 1px 0 0 var(--border, #e2e8f0);
}

/* Footer row inside the modal-body: Reset on the left,
   Shuffle on the right. Both are text buttons, no heavy
   chrome; the Shuffle one gets the primary tint to read
   as the "do something different" action. */
.accent-modal-foot {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 1.1rem;
    padding-top: .85rem;
    border-top: 1px solid var(--border);
}
.accent-text-btn {
    display: inline-flex; align-items: center; gap: .35rem;
    background: transparent;
    border: 0;
    padding: .35rem .55rem;
    border-radius: 8px;
    font-size: 12px;
    color: var(--text-muted);
    cursor: pointer;
    transition: background .12s ease, color .12s ease;
}
.accent-text-btn:hover { background: var(--surface-2); color: var(--text); }
.accent-text-btn-primary { color: var(--primary); }
.accent-text-btn-primary:hover { background: color-mix(in srgb, var(--text) 5%, transparent); color: var(--text); }

/* ── Review-state mark (.bf-reviewmark) ─────────────────────────────
   The ONE mark for the document approval workflow, rendered solely by
   Html.ReviewMark (ReviewMarkHelper) - panel chips, round history, row
   badges, and the filter facet all wear the same glyphs: check-circle =
   approved, clock = in review, revision arrows = changes requested,
   slash = cancelled, gray clock = closed round / no decision. Shape is
   the signal (the plain colored dot stays reserved for the search-index
   dimension); the hue only reinforces within this set. Defined ONCE
   here - never restate these colors per surface.
   is-any is the family's NEUTRAL member (the Review facet's "Any status"
   resting row): a grid rather than a ring, quiet ink, so it reads as
   "no state filter" and never as a fourth state.
   The svg carries no width/height of its own (viewBox only), so the
   fill rule below is what bounds it: without it the mark renders at its
   intrinsic replaced size and swamps whatever box a surface gave it. */
.bf-reviewmark { display: inline-flex; align-items: center; justify-content: center; width: 13px; height: 13px; flex: 0 0 auto; }
.bf-reviewmark svg { width: 100%; height: 100%; }
.bf-reviewmark.is-inreview { color: var(--warning); }
.bf-reviewmark.is-approved { color: #16a34a; }
.bf-reviewmark.is-changes { color: #dc2626; }
.bf-reviewmark.is-cancelled, .bf-reviewmark.is-nodecision, .bf-reviewmark.is-any { color: var(--text-subtle); }
.bf-reviewmark.is-refused { color: #dc2626; }
.bf-reviewmark.is-failed { color: var(--warning); }
.bf-reviewmark.is-skipped { color: var(--text-subtle); }

/* ── Diffusion-state mark (.bf-diffmark) ─────────────────────────────
   The ONE mark family of the read-acknowledgment lane (issue #381),
   rendered solely by Html.DiffusionMark (DiffusionMarkHelper): the Records
   card, the read page, the coverage roster, the waiting rows, the list badge
   and the tile flag all wear the same OPEN BOOK. pending = book (amber),
   acknowledged / complete = book + check (green), late = book + bang (red),
   closed / superseded = gray. Shape is the signal; color only reinforces. */
.bf-diffmark { display: inline-flex; align-items: center; justify-content: center; width: 14px; height: 14px; flex: 0 0 auto; }
.bf-diffmark svg { width: 100%; height: 100%; }
.bf-diffmark.is-pending { color: var(--warning); }
.bf-diffmark.is-acknowledged, .bf-diffmark.is-complete { color: #16a34a; }
.bf-diffmark.is-late { color: #dc2626; }
.bf-diffmark.is-closed, .bf-diffmark.is-superseded, .bf-diffmark.is-none { color: var(--text-subtle); }

/* ── Signature-state mark (.bf-signmark) ─────────────────────────────
   The ONE mark family of the electronic-signature lane (issue #121),
   rendered solely by Html.SignatureMark (SignatureMarkHelper): the details
   card, the list-row badge, the waiting rows and the signing pages all wear
   the same nib. pending = nib on a clock (amber), signed = nib + check
   (green), declined = nib struck through (red), expired = hourglass, waiting
   / cancelled / none = gray. Shape is the signal; color only reinforces.
   Same viewBox-only svg as the review mark: the fill rule bounds it. */
.bf-signmark { display: inline-flex; align-items: center; justify-content: center; width: 14px; height: 14px; flex: 0 0 auto; }
.bf-signmark svg { width: 100%; height: 100%; }
.bf-signmark.is-pending { color: var(--warning); }
.bf-signmark.is-signed { color: #16a34a; }
.bf-signmark.is-declined { color: #dc2626; }
.bf-signmark.is-expired, .bf-signmark.is-waiting, .bf-signmark.is-cancelled, .bf-signmark.is-none { color: var(--text-subtle); }

/* ── Redaction mark (.bf-redactmark) ────────────────────────────────────
   The ONE mark family of the redaction lane (issue #225), rendered solely by
   Html.RedactionMark (RedactionMarkHelper): the list-row badge, the lightbox's
   beside-the-name signal and the Records block all wear the same picture -
   text lines with the middle one blacked out. Shape is the signal; the words
   beside it carry the face (original / redacted copy / being prepared). Same
   viewBox-only svg as the other marks: the fill rule bounds it. */
.bf-redactmark { display: inline-flex; align-items: center; justify-content: center; width: 14px; height: 14px; flex: 0 0 auto; }
.bf-redactmark svg { width: 100%; height: 100%; }
.bf-redactmark.is-marked, .bf-redactmark.is-original { color: var(--text-muted); }
.bf-redactmark.is-redacted { color: var(--text); }
.bf-redactmark.is-withheld { color: var(--text-subtle); }

/* ── Publication-lifecycle mark (.bf-pubmark) ────────────────────────
   The ONE mark family for the DAM publication states, rendered solely by
   Html.PublicationMark (PublicationMarkHelper) - the filter facet, the
   Overview fact, the Publication tab's state line and the thumbnail corner
   flag all wear the same speaking pictos: eye = to validate, push-up = to
   publish, globe = published (live on the portal), off-slash = not in
   publication. Shape is the signal; consumers only size the box.
   The svg has no intrinsic width/height (viewBox only): the fill rule
   below bounds it to that box. Drop the fill rule and the mark renders
   full-size and overruns the box - a globe across the whole thumbnail. */
.bf-pubmark { display: inline-flex; align-items: center; justify-content: center; width: 14px; height: 14px; flex: 0 0 auto; }
.bf-pubmark svg { width: 100%; height: 100%; }
.bf-pubmark.is-tovalidate { color: var(--warning, #b45309); }
.bf-pubmark.is-topublish { color: var(--info, #2563eb); }
.bf-pubmark.is-published { color: var(--success, #16a34a); }
.bf-pubmark.is-none { color: var(--text-subtle); }

/* ----- Status / Error pages -----
   Used by /Status/{code} and /Error. Centered single-column layout
   inside the auth chrome. Branded but neutral - no stack traces, no
   request internals beyond the optional request id (which we surface
   so a user reporting a bug can quote a value we can correlate to
   logs). Mirrors the auth pages' visual rhythm. */
.status-page {
    max-width: 480px;
    margin: 5vh auto 0;
    padding: 2.5rem 1.5rem;
    text-align: center;
}
.status-glyph {
    font-size: 4rem; line-height: 1;
    margin-bottom: .75rem;
    filter: grayscale(.1);
}
.status-code {
    font-family: "Plus Jakarta Sans", "Inter", sans-serif;
    font-weight: 800;
    font-size: 14px;
    letter-spacing: .25em;
    color: var(--text-muted);
    margin-bottom: .25rem;
}
.status-title {
    font-family: "Plus Jakarta Sans", "Inter", sans-serif;
    font-size: 1.65rem;
    font-weight: 800;
    letter-spacing: -.02em;
    color: var(--text);
    margin: 0 0 .65rem;
}
.status-message {
    color: var(--text-muted);
    font-size: 14px;
    line-height: 1.55;
    margin: 0 auto 1.5rem;
    max-width: 36ch;
}
.status-request-id {
    color: var(--text-subtle);
    font-size: 12px;
    margin: 0 0 1.5rem;
}
.status-request-id code {
    background: var(--surface-2);
    padding: 1px 6px;
    border-radius: 6px;
    font-size: 11px;
}
/* Who to ask, on a refusal. Two typographic lines - a quiet label over the
 * names in ink - so the page NAMES the way forward instead of leaving a person
 * at a wall. No chips, no avatars, no dots: this is a sentence, not a roster. */
.status-ask {
    margin: 0 auto 1.5rem;
    max-width: 36ch;
    display: flex;
    flex-direction: column;
    gap: .2rem;
}
.status-ask-label {
    color: var(--text-subtle);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .06em;
    text-transform: uppercase;
}
.status-ask-names {
    color: var(--text);
    font-size: 14px;
    font-weight: 600;
    line-height: 1.45;
}
/* The message and the ask read as one paragraph pair, so the message gives up
 * its full trailing gap when an ask follows it. */
.status-page:has(.status-ask) .status-message {
    margin-bottom: .85rem;
}
.status-actions {
    display: inline-flex; gap: .55rem;
    justify-content: center;
}

/* ══════════════════════════════════════════════════════════════════════════
   THE SLIDE-OVER (.bf-slideover)
   ══════════════════════════════════════════════════════════════════════════
   One right-side panel grammar for the whole product. Panels are server-
   rendered hidden and opened by bf-slideover.js, so nothing about them is
   fetched, measured or laid out at open time - there is no shift to absorb.

   Head / body / foot is a fixed three-region frame: only the body scrolls, so
   the identity at the top and the verbs at the bottom never travel away from
   the reader. The panel owns ONE border and separates its regions by spacing,
   not by rules.
   ────────────────────────────────────────────────────────────────────────── */

.bf-slideover-backdrop {
    position: fixed;
    inset: 0;
    z-index: 1090;
    background: rgba(15, 23, 42, 0.28);
    opacity: 0;
    transition: opacity 0.18s ease-out;
}

.bf-slideover-backdrop.is-open { opacity: 1; }

.bf-slideover {
    position: fixed;
    inset-block: 0;
    inset-inline-end: 0;
    z-index: 1091;
    width: min(30rem, 100vw);
    display: flex;
    flex-direction: column;
    background: var(--surface);
    border-inline-start: 1px solid var(--border);
    box-shadow: -18px 0 48px -24px rgba(15, 23, 42, 0.45);
    transform: translateX(100%);
    transition: transform 0.22s cubic-bezier(0.32, 0.72, 0, 1);
}

.bf-slideover.is-open { transform: translateX(0); }

/* Focus lands on the DIALOG when it opens (so Tab walks into it, trapped); the
   container itself is not a control and wears no ring - the controls inside do. */
.bf-slideover:focus,
.bf-slideover:focus-visible { outline: none; }

[dir="rtl"] .bf-slideover { transform: translateX(-100%); }
[dir="rtl"] .bf-slideover.is-open { transform: translateX(0); }

@media (prefers-reduced-motion: reduce) {
    .bf-slideover,
    .bf-slideover-backdrop { transition: none; }
}

.bf-slideover-head {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 1.1rem 1.25rem 0.9rem;
    flex: 0 0 auto;
}

.bf-slideover-title {
    margin: 0;
    font-family: "Plus Jakarta Sans", "Inter", sans-serif;
    font-size: 17px;
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    /* A clipped line keeps its letters' ink (descenders, accents): 1.3 times its font, in the rule itself. */
    line-height: 1.3;
}

.bf-slideover-close {
    margin-inline-start: auto;
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    padding: 0;
    border: 1px solid transparent;
    border-radius: 8px;
    background: none;
    color: var(--text-muted);
    cursor: pointer;
    transition: color 0.12s ease-out, border-color 0.12s ease-out;
}

.bf-slideover-close:hover {
    color: var(--text);
    border-color: var(--border);
}

.bf-slideover-close:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

/* The only scrolling region. Fixed head and foot mean a long roster of roles
 * never pushes the person's name or the verbs off screen. */
.bf-slideover-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: 0.35rem 1.25rem 1.25rem;
}

.bf-slideover-foot {
    flex: 0 0 auto;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.45rem;
    padding: 0.9rem 1.25rem 1.1rem;
}

/* Controls sharing the footer share ONE height. */
.bf-slideover-foot .btn { height: 32px; }

@media (max-width: 600px) {
    .bf-slideover {
        width: 100vw;
        border-inline-start: 0;
    }
}

/* ========================================================================
   MOBILE & TABLET RESPONSIVE LAYER
   ========================================================================
   Single block of @media overrides that targets every layout flagged in
   the mobile audit. Kept at the end of the file (cascade wins) so the
   desktop rules stay readable above and the mobile concerns are easy
   to find. Three breakpoints:
     · max-width: 1024px → tablet (iPad landscape down)
     · max-width: 768px  → tablet portrait + small laptop
     · max-width: 540px  → mobile (iPhone, narrow Android)
     · max-width: 380px  → very narrow mobile
   Plus a (hover: none) block for touch devices regardless of width.
   ======================================================================== */

/* ----- Touch devices: reveal hover-only actions -----
   Notes today hide delete / resolve / react buttons until the parent
   is hovered. Touch has no hover, so on a phone the actions are
   permanently invisible. Pin them visible on any pointer:coarse
   device. Tradeoff: slightly noisier visual on tablets, worth it. */
@media (hover: none) {
    .note .note-del,
    .note .note-resolve,
    .note .note-react-btn {
        opacity: 1;
    }
    /* Tap targets: bump every icon-only button to a 44px minimum so
       fingers can hit them comfortably (iOS HIG / Android M3 both
       recommend 44dp / 48dp respectively). Padding extends the hit
       area without enlarging the visual icon. */
    .btn.icon-only,
    .nav-icon-btn {
        min-width: 44px;
        min-height: 44px;
    }
    /* The member pile keeps its die-cut overlap on touch: inflating
       the pip BOX to 44px tears the pile apart (gaps between avatars,
       a misaligned +N disc - owner, 2026-09-02). The HIG hit target
       comes from an invisible overlay instead: zero layout change,
       ~44px effective touch zone on a 32px circle. */
    .ws-members-pip::after,
    .ws-members-pip-more::after {
        content: "";
        position: absolute;
        inset: -6px;
        border-radius: 50%;
    }
    /* Touch devices (iPad / iPhone / Android tablets): hide the
       hover-revealed action affordances (trash icons next to
       saved views and user labels) ENTIRELY rather than pinning
       them permanently visible. Pinning was the original fix for
       iOS's first-tap-fires-:hover problem, but the side effect
       was a permanent red trash icon on every list row - clutter
       the user reported as a rendering bug.

       Display:none keeps them out of the layout so a tap on the
       row routes directly to the anchor. Counts stay visible on
       hover (no hover-fade emulation needed since the trash
       sibling that would have replaced them is gone). Users on
       touch devices delete labels / saved views from the desktop
       UI or a future contextual menu - the icons are not the
       only path. */
    /* Default touch state: keep the destructive affordances out of
       sight - hover doesn't apply, count must stay readable. The
       JS long-press below toggles `.is-actions-revealed` to opt
       a single row in to its actions on demand. */
    .archive-sidebar .view-item .view-actions,
    .archive-sidebar .view-item:hover .view-actions {
        display: none;
    }
    .archive-sidebar .view-item.is-mutable .view-count,
    .archive-sidebar .view-item.is-mutable:hover .view-count {
        opacity: 1;
    }
    .user-label-row .user-label-delete,
    .user-label-row:hover .user-label-delete {
        display: none;
    }
    .user-label-row:hover .user-label-link .sb-count,
    .user-label-row .user-label-link .sb-count {
        opacity: 1;
    }
    /* Long-press reveal: the touch-side equivalent of count-hover.
       JS adds `.is-actions-revealed` to the row after a 450ms
       press, painting the trash / team icons in place of the
       count. Tapping elsewhere or scrolling clears the class. */
    .archive-sidebar .view-item.is-actions-revealed .view-actions,
    .archive-sidebar .view-item.is-actions-revealed:hover .view-actions {
        display: flex;
        opacity: 1;
        pointer-events: auto;
    }
    .archive-sidebar .view-item.is-mutable.is-actions-revealed .view-count {
        opacity: 0;
    }
    .user-label-row.is-actions-revealed .user-label-delete,
    .user-label-row.is-actions-revealed:hover .user-label-delete {
        display: inline-flex;
        opacity: 1;
        pointer-events: auto;
    }
    .user-label-row.is-actions-revealed .user-label-link .sb-count {
        opacity: 0;
    }
}

/* ----- Tablet (≤1024px) ----- */
@media (max-width: 1024px) {
    /* Compact navbar between the navbar-expand-lg breakpoint (992px)
       and 1024px. The expanded navbar holds a lot of horizontal
       content (brand + 4-5 primary pills + presence stack + Actions
       Ctrl-K + Activity + Bell + Approvals + Shield + Theme + User
       menu). At small-laptop widths the right-side group spills
       past the viewport and the rightmost icon gets cropped. We
       trim the heaviest items (the cmd-k label/kbd, navbar
       horizontal padding, gap between tools) so everything fits
       inline without falling back to the mobile drawer.
       Below 992px Bootstrap collapses to the drawer (handled by
       the `Mobile navbar drawer` block above) so this rule only
       fires in the narrow 992-1024px band. */
    .app-nav .navbar {
        padding-left: .85rem;
        padding-right: .85rem;
    }
    .app-nav .nav-tools {
        gap: 1px;
        padding-left: .25rem;
        margin-left: .25rem;
    }
    /* Drop the user's display-name text next to the avatar - at
       this width the avatar alone identifies the menu, the name
       was eating ~150-200px and pushing the avatar off the right
       edge. The dropdown still shows the full email + name on
       open. The drawer mode (≤991.98px) re-surfaces the email
       inside the user-row card with its own rule. */
    .app-nav .user-menu .user-email {
        display: none;
    }
    /* The default Bootstrap dropdown chevron (::after triangle) on
       the user-menu adds ~12-15px of dead width once the email
       label is gone - it's just sitting next to the avatar.
       Suppress it: tapping the avatar already implies the menu. */
    .app-nav .user-menu.dropdown-toggle::after {
        display: none;
    }
    .app-nav .user-menu {
        padding: .15rem .25rem !important;
    }
    /* Pill labels on the primary-nav pills (Workspaces / Collaborators
       / Teams / Inboxes) - keep visible at iPad-landscape but the
       count pills + text are still wide. We don't hide labels
       (they're discoverable affordances) but tighten the inner
       padding so the row fits without crowding the right group. */
    .app-nav .nav-link-pill {
        padding: .4rem .55rem !important;
    }
}

/* ----- Tablet portrait + small laptop (≤768px) ----- */
@media (max-width: 768px) {
    /* Reduce page-inner padding so we recover horizontal space for
       content. Default is 2rem 1.5rem. */
    .app-page-inner {
        padding: 1.25rem 1rem;
    }

    /* Workspace card panel: relax the label column width and drop the
       reserved-right padding so the avatar pile gets full width. The
       Manage button moves into its own grid row instead of being
       absolute - on a phone, having the button stack below the
       members reads more clearly than overlapping. */
    .ws-members {
        grid-template-columns: 64px 1fr;
        padding: .65rem .7rem;
    }
    .ws-members-people {
        padding-right: 0;
    }
    .ws-members-manage {
        position: static;
        margin: .35rem 0 0;
        align-self: start;
        grid-column: 1 / -1;
        justify-self: end;
    }

    /* Message detail: 2rem horizontal padding on a 768px viewport
       eats too much. Trim. */
    .msg-container {
        padding: 1rem 1.25rem 2rem;
    }

    /* Activity / notifications feed entries: wrap long workspace
       labels onto a second line instead of clipping. */
    .activity-line {
        flex-wrap: wrap;
    }

    /* Message-nav preview card was 380px fixed width. Cap to viewport. */
    .msg-nav-preview {
        max-width: calc(100vw - 32px);
        width: auto;
    }
}

/* ----- Mobile (≤540px) ----- */
@media (max-width: 540px) {
    /* Page chrome: tighten further on phones. */
    .app-page-inner {
        padding: 1rem .75rem;
    }
    .page-title {
        font-size: 1.4rem;
    }

    /* Workspace card: drop the role-label column width even more so
       the avatar pile gets every available pixel. Owner avatar is
       still 32px which is the minimum we want. */
    .ws-members {
        grid-template-columns: 56px 1fr;
        padding: .6rem;
    }
    .ws-members-rolelabel {
        font-size: 9px;
    }

    /* Message detail: drop padding to 1rem all-round so emails with
       inline tables don't horizontal-scroll on a 360px viewport. */
    .msg-container {
        padding: .75rem .85rem 1.5rem;
    }

    /* Mail: archive header stacks. Identity + count summary on top,
       command bar (filters / mode toggle / sort) on its own line. */
    .archive-header {
        flex-direction: column;
        align-items: stretch;
        gap: .65rem;
    }
    .archive-commandbar {
        flex-wrap: wrap;
    }
    /* Identity row (workspace name + Inbox/GROUPED pills + conv count)
       was 443px wide on a 375px viewport - overflowed and the count
       got clipped. Allow wrap so each piece can fall to a second line. */
    .archive-identity {
        flex-wrap: wrap;
        gap: .35rem;
    }

    /* Modals: every dialog gets a viewport-respecting width fallback.
       Bootstrap's .modal-dialog already does most of this but our
       custom dialogs (kbd-help, profile-card, etc.) need an explicit
       fallback. */
    .modal-dialog {
        margin: .5rem;
        max-width: calc(100vw - 1rem);
    }
    .modal-body {
        padding: 1rem;
    }

    /* Footer: DELIBERATELY no rules here. It used to stack the two
       zones into a centered column, doubling the footer's height on
       phones; the ≤720px block near the .app-footer definition owns
       the phone posture now (ONE row: brand left, © right, the
       operated-by link dropped). Pinned by
       MobilePlatformContractTests.AppFooter_StaysOneLine_OnPhones. */

    /* Activity feed avatar: shrink so the body has room. */
    .activity-avatar,
    .notif-avatar {
        width: 32px;
        height: 32px;
    }

    /* Workspace card header: the action buttons row (Open / Resume /
       settings / icon / trash) holds its full width via flex: 0 0 auto.
       On a 360px viewport that left only ~43px for the identity (name
       + email), making the email render one character per line. Stack
       identity and actions vertically so each gets the full row. */
    .account-row-header {
        flex-direction: column;
        align-items: stretch;
        gap: .55rem;
        padding: .75rem .85rem .65rem;
    }
    .account-identity {
        width: 100%;
    }
    /* Email subline: word-break:break-all is the safety net for the
       rare case the identity STILL can't fit (e.g. a 240px viewport
       like a folded Galaxy Z Flip). Without it, an unbreakable email
       string would push the layout wider than the viewport. */
    .account-sub.account-email-sub,
    .account-identity-text > * {
        word-break: break-word;
        overflow-wrap: anywhere;
    }

    /* Status pages: tighten margins for tiny viewports. */
    .status-page {
        margin-top: 2vh;
        padding: 1.5rem 1rem;
    }
    .status-glyph {
        font-size: 3rem;
    }
    .status-title {
        font-size: 1.35rem;
    }
}

/* ----- Very narrow mobile (≤380px) ----- */
@media (max-width: 380px) {
    /* iPhone SE sized devices and similar. Push everything to the
       absolute minimum that's still usable. */
    .app-page-inner {
        padding: .75rem .5rem;
    }

    /* Workspace card: collapse role label above the avatars instead
       of beside them - 56px column eats half the screen at 320px. */
    .ws-members {
        grid-template-columns: 1fr;
        gap: .35rem;
    }
    .ws-members-rolelabel {
        margin-top: .35rem;
    }
    .ws-members-line {
        /* display:contents passes through; rolelabel + people each
           occupy a full row in the 1-column grid above. */
    }
}

/* ----- Edge-pin floating popovers on small viewports -----
   The search-help popover uses absolute positioning that can shoot
   off-screen on narrow viewports. Cap its right edge to the
   viewport. */
@media (max-width: 768px) {
    .search-help-pop {
        right: .5rem !important;
        left: .5rem !important;
        max-width: calc(100vw - 1rem);
    }
}

/* ----- Super-admin pages: the shared base the tool-page grammar
   (admin-console.css) builds on - the page width, the table wrap and
   table, the row tones, the sessions colgroup, the list pager. The
   status vocabulary lives in the grammar's marks (_SaMark). ----- */
.admin-page { max-width: 1280px; }
.admin-table-wrap {
    overflow-x: auto;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
}
.admin-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 12px;
}

/* Sessions table: lock column widths via <colgroup> so the three
   stacked tables (Online / Recent / All accounts) align column-by-
   column. table-layout: fixed forces the browser to honour the
   col widths instead of auto-sizing on content. */
.admin-sessions-table { table-layout: fixed; }
.admin-sessions-table .col-user   { width: 22%; }
.admin-sessions-table .col-ping   { width: 14%; }
.admin-sessions-table .col-tabs   { width: 6%;  }
.admin-sessions-table .col-where  { width: 22%; }
.admin-sessions-table .col-device { width: 12%; }
.admin-sessions-table .col-ip     { width: 12%; }
.admin-sessions-table .col-login  { width: 12%; }
.admin-sessions-table td { word-break: break-word; }
.admin-sessions-table td code {
    font-size: 10px;
    word-break: break-all;
}

.admin-table th {
    text-align: left;
    padding: .75rem .85rem;
    border-bottom: 1px solid var(--border);
    font-weight: 600;
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--text-muted);
    background: var(--surface-2);
}
.admin-table td {
    padding: .75rem .85rem;
    border-bottom: 1px solid var(--border);
    vertical-align: top;
}
.admin-row.is-corrupt   td,
.admin-row.is-error     td { background: color-mix(in srgb, #ef4444 4%, var(--surface)); }
.admin-row.is-orphaned  td { background: color-mix(in srgb, #f59e0b 4%, var(--surface)); }
/* A row mid-removal fades + collapses before the JS drops it. */
.admin-row.is-removing td {
    opacity: 0;
    transition: opacity .16s ease;
}

.admin-ws-email { font-weight: 600; color: var(--text); }
.admin-ws-meta {
    color: var(--text-muted);
    font-size: 11px;
    margin-top: 2px;
}
/* Cache-freshness meta line under the storage cell. Default is the
   muted ws-meta look; "stale" (>36h) goes amber, "never" goes red so
   the operator can scan a long table and spot rows that aren't being
   refreshed. Hover surfaces the longer explanation via title=. */
.admin-cache-meta {
    font-size: 10px;
    margin-top: 4px;
    color: color-mix(in srgb, var(--text-muted) 80%, transparent);
}
.admin-cache-meta.is-stale {
    color: var(--warning, #B45309);
    font-weight: 500;
}
.admin-cache-meta.is-never {
    color: var(--danger, #B91C1C);
    font-weight: 500;
}
/* Shared toolbar (_AdminListToolbar) + pager (_AdminPager) for the
   super-admin list pages. The toolbar inherits .admin-search's
   flex/sizing; only the filtered-total label is new. The pager is
   O(1) wide (first/prev · status · next/last) so it reads the same
   at 2 pages and at 2,000. */
.admin-list-toolbar { margin-bottom: .9rem; }
.admin-list-total {
    margin-left: auto;
    color: var(--text-muted);
    font-size: 11px;
    font-weight: 600;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
.admin-pager {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: .35rem;
    margin-top: 1rem;
}
.admin-pager-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 30px;
    height: 30px;
    padding: 0 .45rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    color: var(--text-muted);
    text-decoration: none;
    font-size: 13px;
    line-height: 1;
    background: transparent;
}
a.admin-pager-btn:hover {
    color: var(--text);
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
    text-decoration: none;
}
.admin-pager-btn.is-disabled { opacity: .35; }
.admin-pager-status {
    font-size: 11px;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
    padding: 0 .5rem;
    white-space: nowrap;
}
/* Force-remove (ghost-danger) gets the same busy treatment as the
   other ghost actions so the click feedback is uniform. */
.btn-ghost-danger.is-busy { opacity: .6; pointer-events: none; }

.admin-ws-content strong { font-weight: 700; color: var(--text); }

/* ----- Super-admin door in the account menu ----- */
.user-account-menu { min-width: 264px; }
.user-account-menu .sa-door { display: flex; align-items: center; }
.user-account-menu .sa-door .sa-shield { color: var(--primary); flex: 0 0 auto; }

/* ----- /Admin/MicrosoftLabelProbe ----- */
.ms-probe-form {
    display: flex; flex-direction: column;
    gap: .75rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 1rem 1.25rem;
    margin-bottom: 1rem;
    max-width: 720px;
}
.ms-probe-row {
    display: flex; flex-direction: column; gap: .25rem;
}
.ms-probe-row label { font-weight: 600; font-size: 12px; color: var(--text); }
.ms-probe-row input,
.ms-probe-row select {
    padding: .35rem .55rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--surface);
    color: var(--text);
}
.ms-probe-row small { color: var(--text-muted); font-size: 11px; }
.ms-probe-table th { white-space: nowrap; padding-right: 1rem; vertical-align: top; }
.ms-probe-table td { vertical-align: top; }
.ms-probe-keyword {
    display: inline-block;
    padding: 1px 6px;
    background: var(--surface-2);
    border-radius: 4px;
    font-size: 12px;
    margin-right: .25rem;
}
.ms-probe-pill {
    display: inline-block;
    padding: .15rem .55rem;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 700;
    margin-right: .5rem;
}
.ms-probe-pill-yes {
    background: color-mix(in srgb, var(--success) 15%, var(--surface));
    color: var(--success);
    border: 1px solid color-mix(in srgb, var(--success) 35%, var(--border));
}
.ms-probe-pill-no {
    background: var(--danger-bg);
    color: var(--danger);
    border: 1px solid color-mix(in srgb, var(--danger) 35%, var(--border));
}

/* ----- /Admin/BackgroundServices form ----- */
.bg-svc-form { display: flex; flex-direction: column; gap: 1.25rem; }
.bg-svc-section {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 1rem 1.25rem 1.1rem;
}
.bg-svc-section > header { margin-bottom: .75rem; }
.bg-svc-section h2 { font-size: 14px; font-weight: 700; margin: 0; }
.bg-svc-section header p {
    margin: .2rem 0 0;
    color: var(--text-muted);
    font-size: 12px;
    line-height: 1.5;
}
.bg-svc-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: .75rem 1rem;
}
.bg-svc-grid label {
    display: flex;
    flex-direction: column;
    gap: .2rem;
    font-size: 12px;
}
.bg-svc-grid label > span {
    font-weight: 600;
    color: var(--text);
    font-size: 12px;
}
.bg-svc-grid input[type="number"] {
    padding: .35rem .55rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--surface);
    color: var(--text);
    font-variant-numeric: tabular-nums;
    width: 100%;
}
.bg-svc-grid input[type="number"]:focus {
    outline: 2px solid color-mix(in srgb, var(--primary) 35%, transparent);
    outline-offset: 1px;
    border-color: var(--primary);
}
.bg-svc-grid small { color: var(--text-muted); font-size: 10px; }
.bg-svc-actions {
    display: flex;
    align-items: center;
    gap: .75rem;
    flex-wrap: wrap;
}
.bg-svc-meta {
    color: var(--text-muted);
    font-size: 11px;
    margin-left: auto;
    font-variant-numeric: tabular-nums;
}

/* Status-row action cluster: the Configure jump-link sits beside Run now. */
.bg-svc-row-actions {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
}
/* Offset the smooth-scroll landing so a section heading is never clipped at the
   top edge of the viewport when its Configure button is clicked. */
.bg-svc-section[id] { scroll-margin-top: 1rem; }
/* Brief ring highlight on the section a Configure button just scrolled to, so
   the operator sees where they landed. Transient feedback only - no persistent
   filled state; the ring fades to nothing. */
@keyframes bg-svc-section-flash-anim {
    0%   { box-shadow: 0 0 0 2px var(--primary); }
    100% { box-shadow: 0 0 0 2px transparent; }
}
.bg-svc-section-flash { animation: bg-svc-section-flash-anim 1.4s ease-out; }

/* ----- Admin dashboard stat tiles ----- */
.admin-section-title {
    font-size: 12px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .05em;
    color: var(--text-muted);
    margin: 1.5rem 0 .65rem;
    display: flex;
    align-items: baseline;
    gap: 0.6rem;
}
/* Live-progress meta tag next to a section title.
   Used by the runtime cards (live · 22:34:01) and the
   integrity-probe hydrator ("probing 3 / 12", "done · all healthy"). */
.admin-runtime-meta {
    font-size: 10px;
    font-weight: 500;
    text-transform: none;
    letter-spacing: .01em;
    color: color-mix(in srgb, var(--text-muted) 80%, transparent);
    font-style: italic;
}
.admin-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: .65rem;
    margin-bottom: 1rem;
}
.admin-stat {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: .85rem 1rem;
}
.admin-stat-label {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: .05em;
    color: var(--text-muted);
    font-weight: 600;
}
.admin-stat-value {
    font-family: "Plus Jakarta Sans", "Inter", sans-serif;
    font-size: 1.7rem;
    font-weight: 700;
    color: var(--text);
    line-height: 1.1;
    margin: .35rem 0 .2rem;
}
.admin-stat-sub {
    font-size: 10px;
    color: var(--text-muted);
}
.admin-stat.is-bad { border-color: color-mix(in srgb, #ef4444 30%, var(--border)); background: color-mix(in srgb, #ef4444 6%, var(--surface)); }
.admin-stat.is-bad .admin-stat-value { color: #b91c1c; }
.admin-stat.is-warn { border-color: color-mix(in srgb, #f59e0b 30%, var(--border)); background: color-mix(in srgb, #f59e0b 6%, var(--surface)); }
.admin-stat.is-warn .admin-stat-value { color: #b45309; }


/* Search + filter bars on Users / AuditLog / Exceptions / EmailAudit.
   Designed to flow:
     - wide viewport: single row of controls
     - narrow viewport (tablet, half-screen): controls wrap to
       multiple rows; each control keeps a sensible min-width but
       can shrink instead of overflowing the page.
   The form fills the parent block by default so wrapping rows
   align with the page gutter on the left. */
.admin-search,
.admin-filter-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .4rem;
    margin-bottom: 1rem;
}
.admin-filter-bar .form-select,
.admin-filter-bar .form-control,
.admin-search .form-control {
    /* flex: 1 1 200px = grow to fill slack, shrink to a 200px
       floor before wrapping. Replaces the old rigid
       `min-width: 200px` that forced horizontal overflow on
       narrow viewports. */
    flex: 1 1 200px;
    min-width: 0;
    max-width: 320px;
    width: auto;
}
.admin-filter-bar .btn,
.admin-search .btn {
    /* Buttons don't grow; they sit tight to their label. */
    flex: 0 0 auto;
}
/* The search-text input is the variable-width control: it should
   absorb leftover slack so the user gets a comfortable typing
   field even when only one or two filters precede it. */
.admin-filter-bar .form-control[type="text"],
.admin-filter-bar .form-control[type="search"],
.admin-search .form-control[type="text"],
.admin-search .form-control[type="search"] {
    flex: 2 1 220px;
    max-width: 480px;
}
/* Uniform compact height across the bar. The global
   `.form-control` rule (padding .55rem .85rem) shadows
   Bootstrap's `.form-control-sm` by source order at equal
   specificity, so without this scoped sizing the search input
   renders ~9px taller than the adjacent selects and buttons
   and the row reads as ragged chrome. Mirrors the same fix
   applied to `.allow-list-add-form`. */
.admin-filter-bar .form-control-sm,
.admin-filter-bar .form-select-sm,
.admin-filter-bar .btn-sm,
.admin-search .form-control-sm,
.admin-search .form-select-sm,
.admin-search .btn-sm {
    padding-block: .35rem;
    padding-inline: .65rem;
    font-size: 12px;
    line-height: 1.4;
    border-radius: 6px;
    min-height: 0;
    height: auto;
}
.admin-filter-bar .btn-sm,
.admin-search .btn-sm {
    padding-inline: 1.1rem;
}

/* Unified page-header band: title + subtitle on the left, status
   counts / quick actions on the right. Wraps on narrow viewports
   so the counts drop below the title cluster instead of colliding
   with it or pushing the chips off-page. Every admin page should
   apply this class to the title row.

   The selectors target the FIRST child (title cluster) and any
   .admin-counts descendant (meta cluster) so existing markup
   can adopt the class with just an outer class-name change. */
.admin-page-header {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px 24px;
    margin-bottom: 1rem;
}
/* Pointer from an operational admin page to the Configuration console
   section that owns its settings: one hairline row, glyph + words + chevron,
   never a duplicated form. Content-sized; wraps whole on narrow frames. */
.admin-pointer {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    max-width: 100%;
    margin-bottom: 1.25rem;
    padding: 8px 12px;
    border: 1px solid var(--border);
    border-radius: 8px;
    font-size: 13px;
    font-weight: 500;
    color: var(--text-muted);
    text-decoration: none;
    transition: color 120ms ease, border-color 120ms ease;
}
.admin-pointer:hover { color: var(--text); border-color: var(--border-strong); text-decoration: none; }
.admin-pointer svg { flex: 0 0 auto; }
.admin-pointer b { color: var(--text); font-weight: 600; }
.admin-page-header > :first-child {
    flex: 1 1 320px;
    min-width: 0;
}
.admin-page-header > .admin-counts,
.admin-page-header > .admin-page-header-meta {
    flex: 0 1 auto;
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5rem;
    /* Keep the meta cluster aligned right when both sit on one
       row; on wrap, it collapses to its content width and aligns
       left against the page gutter. */
    justify-content: flex-end;
}
.admin-page-header .page-title { margin-bottom: 0; }

/* Action-toolbar pattern: bulk operations + secondary actions
   above a table. Wraps on narrow viewports and aligns the cluster
   to the right when it fits on a single row. */
.admin-page-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: .5rem;
    margin-bottom: .5rem;
}

/* /Admin/Access "Add to allow-list" form. Column proportions are
   tuned so the primary input (Value) dominates the row, Note is a
   secondary affordance roughly two-thirds the width, Kind is a
   compact picker, and the submit button sits flush with the input
   baseline at the trailing edge. The grid template keeps the four
   columns aligned at every viewport above the wrap breakpoint;
   below it, fields stack vertically with full-width inputs. */
.allow-list-add-form {
    display: grid;
    grid-template-columns: 140px minmax(0, 3fr) minmax(0, 2fr) auto;
    align-items: end;
    gap: .5rem .75rem;
}
.allow-list-add-field {
    display: flex;
    flex-direction: column;
    min-width: 0;
}
.allow-list-add-field .form-label {
    margin-bottom: .25rem;
}
/* Submit cell shares the row's baseline alignment with the
   labelled fields. `align-self: end` anchors the button to the
   bottom of the row (next to the inputs); without a flex
   wrapper there's no cross-axis stretch and the button keeps
   its intrinsic btn-sm height - matching the controls below. */
.allow-list-add-submit {
    align-self: end;
}
/* Uniform compact height across the row: the global
   `.form-control` rule above (padding .55rem .85rem) shadows
   Bootstrap's `.form-control-sm` by source order at equal
   specificity, leaving the value/note inputs noticeably taller
   than the select and the submit button. Scoped sizing here
   collapses the gap so every cell in the row reads as one
   harmonious control bar. */
.allow-list-add-form .form-control-sm,
.allow-list-add-form .form-select-sm,
.allow-list-add-form .btn-sm {
    padding-block: .35rem;
    padding-inline: .65rem;
    font-size: 12px;
    line-height: 1.4;
    border-radius: 6px;
    min-height: 0;
    height: auto;
}
.allow-list-add-form .btn-sm {
    padding-inline: 1.1rem;
}
@media (max-width: 720px) {
    .allow-list-add-form {
        grid-template-columns: 1fr;
    }
    .allow-list-add-submit {
        justify-self: start;
    }
    .allow-list-add-submit .btn {
        width: auto;
    }
}

/* Files page breadcrumb */
.admin-breadcrumb {
    font-size: 12px;
    color: var(--text-muted);
    margin-bottom: .5rem;
    word-break: break-all;
}
.admin-breadcrumb code {
    background: var(--surface-2);
    padding: 1px 6px;
    border-radius: 4px;
}

/* Highlight super-admin user row */
.admin-row.is-superadmin td {
    background: color-mix(in srgb, var(--primary) 4%, var(--surface));
}

/* Birthday 3-select picker. Three side-by-side dropdowns sized to
   their content - capped via max-width so on a wide form the
   picker reads as a compact widget rather than spanning the whole
   row. Stacks vertically below 480px so each select gets full
   width on a phone. */
.birthday-picker-row {
    display: flex;
    gap: .5rem;
    max-width: 360px;
}
.birthday-picker-row .form-select:nth-child(1) { flex: 0 0 80px;  } /* Day */
.birthday-picker-row .form-select:nth-child(2) { flex: 1 1 auto;  } /* Month */
.birthday-picker-row .form-select:nth-child(3) { flex: 0 0 100px; } /* Year */
@media (max-width: 480px) {
    .birthday-picker-row {
        flex-direction: column;
        max-width: none;
    }
    .birthday-picker-row .form-select { flex: 1 1 auto; }
}

/* ----- /Admin/Database -----
   Database admin page: file stats, table inventory, row browser.
   The browse table can be very wide (some tables have 15+ columns)
   so it scrolls horizontally inside its wrapper instead of pushing
   the page itself wide. */
.admin-ops-row {
    display: inline-flex; align-items: center; gap: .35rem;
    margin-bottom: 1rem;
}
.admin-sql {
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: .55rem .75rem;
    margin: .35rem 0 0;
    font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
    font-size: 11px;
    line-height: 1.4;
    overflow-x: auto;
    white-space: pre-wrap;
    word-break: break-word;
    max-width: 600px;
}
.admin-browse-wrap {
    /* Horizontal scroll inside the panel keeps the rest of the
       layout sane when a wide table is shown. */
    overflow-x: auto;
    max-height: 70vh;
    overflow-y: auto;
}
.admin-browse {
    font-size: 12px;
    /* Allow column auto-sizing so identifier columns stay narrow
       while wide TEXT columns get the room they need. */
    table-layout: auto;
    min-width: 100%;
}
.admin-browse th {
    position: sticky; top: 0;
    z-index: 1;
    background: var(--surface-2);
    /* Sticky header alongside vertical scroll so the column
       names stay visible while the operator scrolls through
       a thousand rows. */
}
.admin-browse .admin-cell {
    /* Cap individual cell width visually; the underlying value
       stays in the DOM (already truncated to 240 chars by the
       handler) so a long token doesn't blow out the column. */
    display: inline-block;
    max-width: 360px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: middle;
}
.admin-pagination {
    display: inline-flex; align-items: center; gap: .65rem;
    margin: .65rem 0 1rem;
    padding: .35rem .55rem;
}
.admin-pagination .is-disabled {
    pointer-events: none;
    opacity: .45;
}

/* =====================================================================
   Workspaces page: rows grouped by workspace type, click row to open
   the dedicated detail page (/Workspaces/Detail/{id}).

   - Section headers reuse the same icon catalog as /Workspaces/New
     (WorkspaceTypeCatalog) so the visual language is shared between
     creation and management.
   - Row shell (icon, name, status dot, summary stats, actions,
     navigation chevron) is workspace-type-agnostic.
   ===================================================================== */

/* ---------- Page head: title + summary chips + tools ---------- */
.ws-page-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1.5rem;
    flex-wrap: wrap;
    margin-bottom: 2.5rem;
    padding-top: .75rem;
}
.ws-page-stats { margin-top: .25rem; }
.ws-page-summary {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .4rem .55rem;
    font-size: 12px;
    color: var(--text-muted);
}
.ws-page-summary-num strong {
    color: var(--text);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}
.ws-page-summary-chip {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    color: var(--text-muted);
    font-size: 12px;
    line-height: 1.3;
    font-variant-numeric: tabular-nums;
    background: transparent;
    border: 0;
    padding: 0;
}
.ws-page-summary-chip + .ws-page-summary-chip::before,
.ws-page-summary-num + .ws-page-summary-chip::before {
    content: "·";
    color: var(--text-subtle);
    margin-right: .35rem;
}
.ws-page-summary-chip strong { color: var(--text); font-weight: 600; }
.ws-page-summary-chip[data-state="syncing"] strong { color: var(--primary); }
.ws-page-summary-chip[data-state="paused"] strong { color: var(--warning); }
.ws-page-summary-chip[data-state="error"] strong { color: var(--danger); }
.ws-page-summary-dot {
    width: 7px; height: 7px; border-radius: 50%;
    background: var(--text-subtle);
    flex: 0 0 auto;
}
.ws-page-summary-chip[data-state="syncing"] .ws-page-summary-dot { background: var(--primary); }
.ws-page-summary-chip[data-state="idle"] .ws-page-summary-dot { background: var(--success); }
.ws-page-summary-chip[data-state="paused"] .ws-page-summary-dot { background: var(--warning); }
.ws-page-summary-chip[data-state="error"] .ws-page-summary-dot { background: var(--danger); }
.ws-page-summary-chip[data-state="syncing"] .ws-page-summary-dot {
    /* Subtle pulse ONLY here. The per-row dot uses the same name
       and same speed so the page reads as a single rhythm, not as
       independent chips and dots fighting for attention. */
    animation: ws-page-pulse 1.6s ease-in-out infinite;
}
@keyframes ws-page-pulse {
    0%, 100% { opacity: 1; transform: scale(1); }
    50%      { opacity: .55; transform: scale(.85); }
}
.ws-list-empty {
    grid-column: 1 / -1;
    padding: 3rem 1rem;
    text-align: center;
    color: var(--text-muted);
}

/* ---------- Section header (one per workspace type) ---------- */
.ws-list-section-head {
    grid-column: 1 / -1;
    display: flex;
    align-items: center;
    gap: .85rem;
    padding: 0 .25rem .9rem;
    margin: 1.5rem 0 .35rem;
    border-bottom: 1px solid var(--border);
}
.ws-list-section-head:first-child { margin-top: .25rem; }
.ws-list-section-icon {
    width: 48px; height: 48px;
    border-radius: 12px;
    background: linear-gradient(135deg, color-mix(in srgb, var(--primary) 16%, var(--surface)),
                                       color-mix(in srgb, #06B6D4 12%, var(--surface)));
    color: var(--primary);
    border: 1px solid color-mix(in srgb, var(--primary) 25%, var(--border));
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    align-self: center;
}
.ws-list-section-icon svg { width: 24px; height: 24px; }
.ws-list-section-name {
    font-size: 1.35rem;
    font-weight: 700;
    color: var(--text);
    letter-spacing: -.015em;
    line-height: 1.1;
}
.ws-list-section-count {
    font-size: 12px;
    font-weight: 500;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
    letter-spacing: -.005em;
}
.ws-list-section-sep { color: var(--text-subtle); }
.ws-list-section-tagline {
    font-size: 12px;
    color: var(--text-subtle);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    margin-left: auto;
}

/* ---------- Card shell ---------- */
.ws-row {
    position: relative;
    display: block;
    background:
        linear-gradient(135deg,
            color-mix(in srgb, var(--bf-color, var(--primary)) 2%, var(--surface)) 0%,
            var(--surface) 35%);
    border: 1px solid color-mix(in srgb, var(--bf-color, var(--primary)) 12%, var(--border));
    border-radius: 10px;
    box-shadow: 0 1px 2px rgba(15, 23, 42, .03);
    overflow: hidden;
    transition: border-color .18s ease, box-shadow .18s ease, transform .18s ease;
    flex-direction: column;
    gap: 0;
    padding: 0;
}
.ws-row::before {
    content: "";
    position: absolute;
    inset: 0 0 auto 0;
    height: 3px;
    background: linear-gradient(90deg,
        var(--bf-color, var(--primary)) 0%,
        color-mix(in srgb, var(--bf-color, var(--primary)) 55%, transparent) 100%);
    pointer-events: none;
}
.ws-row:hover {
    border-color: color-mix(in srgb, var(--bf-color, var(--primary)) 50%, var(--border));
    box-shadow: 0 14px 30px -14px color-mix(in srgb, var(--bf-color, var(--primary)) 40%, transparent);
    transform: translateY(-2px);
}
/* List-row override: ONLY the size variable. The canonical
   `.bf-icon` rule paints everything else (gradient, shadow,
   letter color, SVG color); a future style change happens in
   one place. */
.ws-row .bf-icon { --bf-icon-size: 60px; --bf-icon-radius: 14px; }
.ws-row .account-icon-btn { line-height: 0; }
.ws-row .account-icon-btn::after {
    width: 20px; height: 20px;
    background-size: 11px;
    right: -3px; bottom: -3px;
}
/* Always-on workspace-color dot. Sits at the bottom-right of
   the workspace icon and ALWAYS shows the workspace's selected
   accent - no hover required. Hovering still lifts the dot
   (scale + shadow) and the click still opens the colour picker.
   Wiki page headers keep the hover-only reveal because their
   layout doesn't need a persistent color marker - the article
   header line shows the color in other ways. */
.ws-icon-affordance .icon-color-affordance {
    opacity: 1;
}

/* Inline mini progress under the meta line. Visibility-toggled via
   `.is-poll-hidden` so the slot is reserved when idle - starting /
   stopping a sync doesn't change row height. */
.ws-row-progress.ws-mini-progress {
    /* Override the legacy `.ws-mini-progress` flex layout so it lives
       inline as a thin slim bar under the meta line, not as a separate
       block. */
    display: flex;
    align-items: center;
    gap: .55rem;
    margin-top: .35rem;
    min-height: 0;
}
.ws-row-progress-bar.ws-mini-progress-bar {
    flex: 1 1 auto;
    height: 3px;
    max-width: 240px;
    background: color-mix(in srgb, var(--bf-color, var(--primary)) 12%, var(--surface-2));
    border-radius: 999px;
    overflow: hidden;
}
.ws-row-progress-bar.ws-mini-progress-bar .account-bar {
    height: 100%;
    background: var(--bf-color, var(--primary));
    border-radius: 999px;
    /* Width transitions during sync caused visible bouncing on every
       1Hz poll. Drop the transition entirely - the discrete jumps at
       1Hz are smoother in practice than a 0.35s ease that re-fires
       on every poll, never settling. */
    transition: none;
}
.ws-row-progress .account-pct {
    font-size: 10px;
    font-weight: 600;
    color: var(--text-muted);
    min-width: 2.4rem;
    text-align: right;
    font-variant-numeric: tabular-nums;
}
.ws-row-stat-label { display: none; }

/* Collab pile: 3 avatars max + +N indicator. */
.ws-row-collab {
    display: flex;
    align-items: center;
}
.ws-row-collab.is-empty { display: none; }
.ws-row-collab-avatar {
    width: 26px; height: 26px;
    border-radius: 50%;
    object-fit: cover;
    background: var(--avatar-bg);
    color: var(--avatar-color);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 10px;
    font-weight: 600;
}
@media (max-width: 1080px) {
    .ws-row-collab { display: none; }
}

/* Navigation indicator at the right edge: a chevron that hints
   the row is clickable and opens the dedicated detail page. Slides
   right on row-hover. */
.ws-row-go {
    grid-column: chevron-start;
    width: 24px; height: 24px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--text-subtle);
    transition: color .15s ease, transform .2s ease;
    pointer-events: none;
}
.ws-row-go svg { width: 16px; height: 16px; }
.ws-row:hover .ws-row-go {
    color: var(--bf-color, var(--primary));
    transform: translateX(3px);
}

/* ---------- Detail panel ---------- */
.ws-row-detail {
    /* Smooth expand/collapse via `[hidden]` removal + height transition.
       JS toggles the hidden attribute; CSS handles the visual unfold.
       Without a max-height we'd see a jump; the transition lives on
       max-height + padding so the panel reveals in one motion. */
    border-top: 1px solid var(--border);
    background: color-mix(in srgb, var(--bf-color, var(--primary)) 3%, var(--surface));
}
.ws-row-detail[hidden] { display: none; }
.ws-row-detail .account-row-body {
    padding: 1rem 1.25rem 1.1rem;
    display: flex;
    flex-direction: column;
    gap: .85rem;
}

/* The legacy .account-metrics grid is reused inside the detail panel,
   along with .account-categories, .account-live-subject, .ws-members,
   .account-gap. Their existing styles still apply; we just adjust the
   presentation to the new context. */
.ws-row-detail .account-metrics {
    background: var(--surface);
    border: 1px solid var(--border);
}
.ws-row-detail .ws-members {
    background: var(--surface);
}

/* ---------- Responsive ---------- */
@media (max-width: 880px) {
    .ws-row-collab { display: none; }
}

/* ---------- Compatibility shims ---------- */
/* Disable the legacy account-row hover/border that fought the new
   styling. The legacy rule .account-row { background: var(--surface);
   border: 1px solid var(--border); border-radius: 12px; box-shadow: ...; }
   stays, but the hover lift below was added separately. We pin our own. */
.ws-row .account-row-header,
.ws-row .ws-row-summary > .account-row-header {
    /* The header div was removed from markup; this is belt-and-suspenders
       in case any cached rendering still has it. */
    display: contents;
}

/* When inside a .ws-row context, the legacy bar transition is removed
   because polling at 1Hz produces smoother visual feedback without it. */
.ws-row .account-bar { transition: none !important; }

/* Detail panel reveal animation - simple opacity + translate so the
   user perceives it expanding from the row. The hidden attribute
   keeps the panel out of layout when collapsed. */
.ws-row-detail {
    animation: ws-detail-reveal .22s ease-out;
}
@keyframes ws-detail-reveal {
    from { opacity: 0; transform: translateY(-4px); }
    to   { opacity: 1; transform: translateY(0); }
}

/* =====================================================================
   Workspace Detail page (/Workspaces/Detail/{id})

   Hero + stats + sections layout. The accent CSS variable propagates
   from --bf-color so each workspace's tint flavors the page.
   ===================================================================== */
/* The settings window under the ONE shell head (owner, 2026-09-18): the
   body aligns under the head's mark and keeps a reading measure of its own
   (rail 240 + gap + a ~920px pane) - never centred away from the head. */
.ws-detail-page {
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
    /* A column flex item with auto side margins shrinks to its content and
       centres (the shell head makes .app-page a column): the page keeps the
       frame's full width and its own inset instead. */
    width: 100%;
    max-width: none;
    margin: 0;
}

/* =============================================================
   Canonical workspace-icon affordance - see
   _WorkspaceIconAffordance.cshtml. ONE shape for the icon
   button + status-dot + pencil-edit overlay used by both the
   Workspaces list and the Workspace Detail hero. The previous
   `.account-icon-btn` style on the list had a `border-radius:
   50%` that clipped the rounded-square glyph (16px corners),
   producing the half-circle "dirty effect" the user flagged.
   This rule renders the affordance as a clean rounded-square
   container (matches the Detail hero), with a subtle accent-
   tinted hover lift and the pencil overlay revealed on hover
   for clickable instances only.
   ============================================================= */
.ws-icon-affordance {
    position: relative;
    display: inline-flex;
    flex: 0 0 auto;
    border-radius: 16px;
    line-height: 0;
}
button.ws-icon-affordance-btn {
    background: transparent;
    border: 0;
    padding: 0;
    cursor: pointer;
    border-radius: 16px;
    transition: transform .15s ease, box-shadow .15s ease;
}
button.ws-icon-affordance-btn:hover {
    transform: translateY(-1px) scale(1.02);
}
button.ws-icon-affordance-btn:focus-visible {
    outline: 0;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 30%, transparent);
}
.ws-icon-affordance-edit {
    position: absolute;
    right: -4px; top: -4px;
    width: 22px; height: 22px;
    border-radius: 50%;
    background: var(--surface);
    color: var(--primary);
    box-shadow: 0 2px 6px rgba(15, 23, 42, .12), 0 0 0 1px var(--border);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    transform: scale(.7);
    transition: opacity .15s ease, transform .15s ease;
    pointer-events: none;
    z-index: 2;
}
.ws-icon-affordance-edit svg { width: 11px; height: 11px; }
button.ws-icon-affordance-btn:hover .ws-icon-affordance-edit,
button.ws-icon-affordance-btn:focus-visible .ws-icon-affordance-edit {
    opacity: 1;
    transform: scale(1);
}

/* Header-side action buttons in a section (used by Access &
   permissions). Sits flush right of the title. */
.ws-detail-section-actions {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
}
/* ── The rail's identity card (owner, 2026-09-18: the hero retired - the
   head is the identity, the rail keeps the plate and the facts) ──────────
   plate 44 (the ONE icon affordance, _WorkspaceIconAffordance) · the type
   unit (glyph + word, the maintenance flag beside it when offline) · the
   type's own second line (Mail: provider · address) · the type's facts /
   state line (HeroStatePartial) - one muted line each, ellipsized, never a
   paragraph; then a hairline and the chapters. */
.ws-identity { display: flex; align-items: center; gap: 12px; padding: 6px 6px 10px; min-width: 0; }
.ws-identity-plate { flex: 0 0 auto; display: inline-flex; line-height: 0; }
/* The plate's size variables only - the canonical .bf-icon rule paints the rest. */
.ws-identity-plate .bf-icon { --bf-icon-size: 44px; --bf-icon-radius: 12px; }
.ws-identity-plate .ws-icon-affordance,
.ws-identity-plate button.ws-icon-affordance-btn { border-radius: 12px; }
.ws-identity-text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.ws-identity-type { display: inline-flex; align-items: center; gap: 6px; min-width: 0; font-size: 12px; font-weight: 600; color: var(--text); }
.ws-identity-type-icon { display: inline-flex; align-items: center; justify-content: center; width: 14px; height: 14px; flex: 0 0 auto; color: var(--text); }
.ws-identity-type-icon svg { width: 14px; height: 14px; }
.ws-identity-type-word { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ws-identity-sub { display: flex; align-items: center; gap: 5px; min-width: 0; font-size: 11px; font-weight: 500; color: var(--text-muted); }
.ws-identity-provider { display: inline-flex; align-items: center; gap: 4px; flex: 0 0 auto; }
.ws-identity-provider svg { width: 12px; height: 12px; }
.ws-identity-sep { color: var(--text-subtle); }
.ws-identity-email { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-variant-numeric: tabular-nums; }
/* The state line WRAPS in the 240px rail (a bitten "Sync error · 81 d ago" is a rejected artifact); the address above it stays one ellipsized token with the whole on its title. */
.ws-identity-state { display: flex; align-items: center; gap: 5px; min-width: 0; max-width: 100%; font-size: 11px; font-weight: 500; line-height: 1.35; color: var(--text-muted); font-variant-numeric: tabular-nums; }
/* ONE line: a glyph that says the state, the words ellipsized (whole on the tooltip). */
.ws-identity-state-glyph { display: inline-flex; flex: 0 0 auto; width: 12px; height: 12px; }
.ws-identity-state-glyph svg { display: none; width: 12px; height: 12px; }
.ws-identity-state[data-state="error"] .ws-identity-state-glyph .is-error,
.ws-identity-state[data-state="paused"] .ws-identity-state-glyph .is-paused,
.ws-identity-state:not([data-state="error"]):not([data-state="paused"]) .ws-identity-state-glyph .is-ok { display: block; }
.ws-identity-state-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Colour signals STATE on the words themselves - never a dot (charter). */
.ws-identity-state[data-state="syncing"] { color: var(--text); }
.ws-identity-state[data-state="paused"] { color: var(--warning); font-weight: 600; }
.ws-identity-state[data-state="error"] { color: var(--danger); font-weight: 600; }
.ws-identity-divider { height: 1px; margin: 0 2px 6px; background: var(--border); }

/* Progress bar shown during active sync (between hero and stats). */
.ws-detail-progress {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: .75rem 1.5rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
}
.ws-detail-progress-bar {
    flex: 1 1 auto;
    height: 6px;
    background: color-mix(in srgb, var(--bf-color, var(--primary)) 12%, var(--surface-2));
    border-radius: 999px;
    overflow: hidden;
}
.ws-detail-progress-fill {
    height: 100%;
    background: var(--bf-color, var(--primary));
    border-radius: 999px;
}
.ws-detail-progress-pct {
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--text);
    min-width: 3rem;
    text-align: right;
}

/* ===== Live Sync Activity panel ===================================
   Always rendered. Visible state controlled by `data-state` so the
   poller doesn't flicker the layout when state transitions. */
.ws-sync-panel {
    position: relative;
    /* No card inside a card (owner, 2026-10-07): the sync stands in the chapter's card, as a section of it. */
    background: transparent;
    border: 0;
    border-radius: 0;
    padding: 1rem 0 1.1rem;
    display: flex;
    flex-direction: column;
    gap: .8rem;
    transition: border-color .25s ease, background .25s ease;
}

.ws-sync-panel-head {
    display: flex; align-items: center; justify-content: space-between; gap: .75rem;
}
.ws-sync-panel-title { display: flex; align-items: center; gap: .55rem; }
/* The panel head's right: the state badge, then the sync verb it offers. */
.ws-sync-panel-side { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 8px; }
.ws-sync-verbs { display: flex; align-items: center; gap: 8px; }
.ws-sync-verbs .btn { white-space: nowrap; }
.ws-sync-panel-title h2 {
    font-size: 13px; font-weight: 700; margin: 0; color: var(--text);
    letter-spacing: .01em;
}
.ws-sync-pulse {
    display: inline-flex; align-items: center; gap: 3px; height: 12px;
}
.ws-sync-pulse span {
    width: 4px; border-radius: 2px;
    background: color-mix(in srgb, var(--bf-color, var(--primary)) 60%, var(--border));
    height: 4px;
    transition: height .25s ease, background .25s ease;
}
/* The bars move while a sync runs and are not there at rest: three still
   coloured marks beside "Up to date" read as the dots the charter bans.
   visibility keeps their box, so a run starting never moves the head. */
.ws-sync-panel:not([data-state="syncing"]) .ws-sync-pulse { visibility: hidden; }
.ws-sync-panel[data-state="syncing"] .ws-sync-pulse span {
    background: var(--bf-color, var(--primary));
    animation: ws-sync-pulse 1.1s ease-in-out infinite;
}
.ws-sync-panel[data-state="syncing"] .ws-sync-pulse span:nth-child(2) { animation-delay: .18s; }
.ws-sync-panel[data-state="syncing"] .ws-sync-pulse span:nth-child(3) { animation-delay: .36s; }
@keyframes ws-sync-pulse {
    0%, 100% { height: 4px; opacity: .55; }
    50%      { height: 12px; opacity: 1; }
}

.ws-sync-state-badge {
    font-size: 11px;
    font-weight: 600;
    padding: .25rem .6rem;
    border-radius: 999px;
    color: var(--text-muted);
    background: var(--surface-2);
    border: 1px solid var(--border);
    white-space: nowrap;
}
.ws-sync-state-badge[data-state="syncing"] {
    color: color-mix(in srgb, var(--bf-color, var(--primary)) 88%, var(--text));
    background: color-mix(in srgb, var(--bf-color, var(--primary)) 10%, var(--surface));
    border-color: color-mix(in srgb, var(--bf-color, var(--primary)) 35%, var(--border));
}
.ws-sync-state-badge[data-state="error"] {
    color: var(--danger); background: var(--danger-bg);
    border-color: color-mix(in srgb, var(--danger) 35%, var(--border));
}
.ws-sync-state-badge[data-state="paused"] {
    color: var(--warning); background: var(--warning-bg);
    border-color: color-mix(in srgb, var(--warning) 35%, var(--border));
}

.ws-sync-progress-row {
    display: flex; align-items: center; gap: .9rem;
}
/* Hide the progress bar entirely when sync is idle ("Up to date").
   The bar is meaningful while a sync is running (and even while
   paused / errored - it shows where the run stopped); leaving a
   100% bar plastered across the panel forever once sync completes
   is visual noise the operator complained about. */
.ws-sync-panel[data-state="idle"] .ws-sync-progress-row {
    display: none;
}
.ws-sync-progress-bar {
    flex: 1 1 auto; height: 8px;
    background: color-mix(in srgb, var(--bf-color, var(--primary)) 12%, var(--surface-2));
    border-radius: 999px; overflow: hidden;
}
.ws-sync-progress-fill {
    height: 100%; border-radius: 999px;
    background: var(--bf-color, var(--primary));
    transition: width .35s cubic-bezier(.2,.7,.2,1);
}
.ws-sync-panel[data-state="syncing"] .ws-sync-progress-fill {
    background-image: linear-gradient(90deg,
        color-mix(in srgb, var(--bf-color, var(--primary)) 70%, white) 0%,
        var(--bf-color, var(--primary)) 50%,
        color-mix(in srgb, var(--bf-color, var(--primary)) 70%, white) 100%);
    background-size: 200% 100%;
    animation: ws-sync-shimmer 1.6s linear infinite;
}
@keyframes ws-sync-shimmer { 0% { background-position: 0 0; } 100% { background-position: -200% 0; } }
.ws-sync-progress-pct {
    font-weight: 700; font-variant-numeric: tabular-nums;
    color: var(--text); min-width: 3rem; text-align: right;
}

.ws-sync-status-line {
    margin: 0;
    font-size: 12px;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
    line-height: 1.45;
}
.ws-sync-panel[data-state="syncing"] .ws-sync-status-line { color: var(--text); }

.ws-sync-metrics {
    margin: 0;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    gap: .55rem .9rem;
    padding: .65rem .85rem;
    background: var(--surface-2);
    border-radius: 10px;
}
.ws-sync-metric { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
.ws-sync-metric dt {
    font-size: 10px; font-weight: 600; text-transform: uppercase;
    letter-spacing: .04em; color: var(--text-subtle);
}
.ws-sync-metric dd {
    margin: 0; font-size: 13px; color: var(--text);
    font-variant-numeric: tabular-nums;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ws-sync-metric strong { font-weight: 700; }
.ws-sync-metric-of { color: var(--text-muted); font-weight: 500; font-size: 12px; }

/* Show "active" metrics only while syncing; show "idle" metric only otherwise. */
.ws-sync-panel:not([data-state="syncing"]) .ws-sync-metric[data-when="active"] { display: none; }
.ws-sync-panel[data-state="syncing"]      .ws-sync-metric[data-when="idle"]   { display: none; }

.ws-sync-cats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: .5rem;
}
.ws-sync-cat {
    display: flex; align-items: center; gap: .55rem;
    padding: .5rem .75rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    transition: opacity .2s ease, border-color .2s ease, background .2s ease;
    min-width: 0;
}
.ws-sync-cat.is-empty { opacity: .55; }
.ws-sync-cat-icon {
    flex: 0 0 auto;
    width: 22px; height: 22px;
    display: inline-flex; align-items: center; justify-content: center;
    color: color-mix(in srgb, var(--bf-color, var(--primary)) 70%, var(--text));
}
.ws-sync-cat-icon svg { width: 100%; height: 100%; }
.ws-sync-cat-label {
    flex: 1 1 auto;
    font-size: 12px;
    font-weight: 600;
    color: var(--text);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ws-sync-cat-count {
    flex: 0 0 auto;
    font-size: 12px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--text);
    padding: .1rem .45rem;
    background: var(--surface-2);
    border-radius: 999px;
    min-width: 1.75rem;
    text-align: center;
}
.ws-sync-cat:not(.is-empty) .ws-sync-cat-count {
    color: color-mix(in srgb, var(--bf-color, var(--primary)) 90%, var(--text));
    background: color-mix(in srgb, var(--bf-color, var(--primary)) 12%, var(--surface));
}

.ws-sync-current {
    display: flex; align-items: center; gap: .75rem;
    padding: .55rem .8rem;
    background: color-mix(in srgb, var(--bf-color, var(--primary)) 6%, var(--surface));
    border: 1px dashed color-mix(in srgb, var(--bf-color, var(--primary)) 30%, var(--border));
    border-radius: 10px;
    min-width: 0;
}
.ws-sync-current-label {
    font-size: 10px; font-weight: 700; text-transform: uppercase;
    letter-spacing: .06em; color: var(--bf-color, var(--primary));
    flex: 0 0 auto;
}
.ws-sync-current-msg { display: flex; flex-direction: column; gap: .1rem; min-width: 0; flex: 1 1 auto; }
.ws-sync-current-from {
    font-size: 11px; color: var(--text-muted); font-weight: 600;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ws-sync-current-subject {
    font-size: 12px; color: var(--text);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.ws-sync-error {
    display: flex; flex-direction: column; gap: .55rem;
    padding: .75rem .9rem;
    background: var(--danger-bg);
    border: 0;
    border-radius: 10px;
    color: var(--danger);
}
.ws-sync-error-head { display: flex; align-items: center; gap: .45rem; font-weight: 700; }
.ws-sync-error-head svg { width: 18px; height: 18px; flex: 0 0 auto; }
/* One error, told once: the human sentence leads; the raw exception sits
   behind the quiet "Technical details" disclosure. */
.ws-sync-error-human {
    margin: 0; font-size: 12px; color: var(--text);
    word-break: break-word;
}
.ws-sync-error-details summary {
    cursor: pointer;
    font-size: 11px; font-weight: 600;
    color: var(--text-muted);
    width: fit-content;
}
.ws-sync-error-details summary:hover { color: var(--text); }
.ws-sync-error-raw {
    margin: .5rem 0 0;
    padding: .6rem .75rem;
    font-size: 11px; line-height: 1.5;
    color: var(--text-muted);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    white-space: pre-wrap; word-break: break-word;
}
.ws-sync-error-actions { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }

/* The door into the Microsoft 365 access assistant (_MicrosoftAdminApproval). */
.ms-approve-btn { display: inline-flex; align-items: center; gap: .4rem; white-space: nowrap; }
.ms-approve-btn svg { flex: 0 0 auto; }
/* OAuth providers get guidance instead of a form that cannot work. */
.ws-sync-error-hint { margin: 0; font-size: 12px; color: var(--text-muted); }

/* Provider capability fault: the mail syncs, one optional leg of the
   provider is refused (Outlook categories through Graph). The error card's
   shape in the warning tone - a glyph and one strong line, one sentence,
   the provider's own words folded away, the verb with chrome. */
.ws-sync-fault {
    display: flex; flex-direction: column; gap: .55rem;
    padding: .75rem .9rem;
    background: var(--warning-bg);
    border: 1px solid color-mix(in srgb, var(--warning) 35%, var(--border));
    border-radius: 10px;
    color: var(--text);
}
.ws-sync-fault-head { display: flex; align-items: center; gap: .45rem; font-weight: 700; color: var(--warning); }
.ws-sync-fault-head svg { width: 18px; height: 18px; flex: 0 0 auto; }
.ws-sync-fault-line { margin: 0; font-size: 12px; color: var(--text); word-break: break-word; }
.ws-sync-fault-meta { display: flex; align-items: center; gap: .4rem; font-size: 11px; color: var(--text-muted); }
.ws-sync-fault-meta svg { width: 12px; height: 12px; flex: 0 0 auto; }
.ws-sync-fault-actions { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem .75rem; }
.ws-sync-fault-owner { margin: 0; font-size: 12px; color: var(--text-muted); }

/* A Microsoft mailbox's calendar in the reader's Burofolk calendar: the
   fault card's shape in the neutral tone - a glyph and one strong line
   (with the shown count as a quiet state), one sentence, verbs with chrome. */
.ws-cal-unit {
    display: flex; flex-direction: column; gap: .55rem;
    padding: .75rem .9rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    color: var(--text);
}
.ws-cal-head { display: flex; align-items: center; gap: .45rem; font-weight: 700; }
.ws-cal-head svg { width: 18px; height: 18px; flex: 0 0 auto; color: var(--text-muted); }
.ws-cal-state { margin-left: auto; font-size: 11px; font-weight: 600; color: var(--text-muted); }
.ws-cal-line { margin: 0; font-size: 12px; color: var(--text-muted); }
.ws-cal-actions { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }

/* The mailbox check (#567): the calendar card's shape - a glyph and ONE
   strong verdict, the two counts as chips, then one foot line (when it was
   checked, or that it is checking) beside Check now. */
.ws-check-unit {
    display: flex; flex-direction: column; gap: .6rem;
    /* No card inside a card (owner, 2026-10-07): a section of the chapter's card, one hairline between two. */
    padding: .8rem 0;
    background: transparent;
    border: 0;
    border-radius: 0;
    color: var(--text);
}
.ws-check-unit + .ws-check-unit { border-top: 1px solid var(--border); }
.ws-check-head { display: flex; align-items: center; gap: .5rem; font-weight: 700; font-size: 14px; }
.ws-check-glyph { width: 18px; height: 18px; flex: 0 0 auto; color: var(--success); }
.ws-check-unit.is-gap .ws-check-glyph { color: var(--warning); }
.ws-check-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.ws-check-chip {
    display: inline-flex; align-items: center;
    padding: .15rem .55rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    font-size: 12px; font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: var(--text);
}
.ws-check-detail { margin: 0; font-size: 12px; color: var(--text-muted); line-height: 1.5; }
.ws-check-foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: .5rem .75rem; }
.ws-check-line { font-size: 12px; color: var(--text-muted); min-height: 1.5em; line-height: 1.5; }
.ws-check-line.is-failed { color: var(--danger); }
.ws-check-verbs { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.ws-check-verbs .btn { display: inline-flex; align-items: center; gap: .4rem; white-space: nowrap; }
.ws-check-verbs .btn[data-state="checking"] svg { animation: ws-check-spin 1s linear infinite; }
@keyframes ws-check-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .ws-check-verbs .btn[data-state="checking"] svg { animation: none; } }

/* An errored sync shows NO serene progress bar - a blue 100% above a red
   failure banner said "success" and "broken" at once. The badge + error
   card carry the state; the bar returns with the next healthy state. */
.ws-sync-panel[data-state="error"] .ws-sync-progress-row { display: none; }

/* Category chips are LINKS into the mailbox scoped to the category - keep
   the tile look, add the door affordance (hover = border tint, no fill). */
a.ws-sync-cat { text-decoration: none; color: inherit; }
a.ws-sync-cat:hover {
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
}
a.ws-sync-cat:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* Thresholded last-sync escalation (SyncFreshnessRule: quiet under 48h,
   amber to 7 days, strong past that). Colour signals STATE, applied to the
   hero health line while idle (error/paused states already own their colour). */
.ws-identity-state[data-state="idle"][data-freshness="aging"] { color: var(--warning); }
.ws-identity-state[data-state="idle"][data-freshness="stale"] { color: var(--danger); font-weight: 600; }

/* ── Settings window: rail + pane ─────────────────────────────────────
   The config page is a settings window (owner decision 2026-09-05): a
   rail of chapters on the left, ONE chapter open on the right. Which
   chapter is open is decided on the server (?chapter=) so the first
   paint is final; workspace-detail-nav.js only switches on click and
   keeps the URL honest. Below 880 the rail IS the page and a chosen
   chapter drills in (data-ws-drilled). */
.ws-detail-body {
    display: grid;
    grid-template-columns: 240px minmax(0, 1fr);
    gap: var(--app-pane-gap, 16px);
    align-items: start;
    max-width: 1180px;
}
/* The chapters rail is a pane CARD (owner, 2026-09-06: "improve the left
   appearance so we better get the separation with the right content… not
   overwhelming"): the shell's own grammar - surface, hairline, the pane
   radius, the pane lift - and the 32px row. */
.ws-chapters {
    position: sticky;
    top: 1rem;
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 8px;
    min-width: 0;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--app-pane-radius, 10px);
    box-shadow: var(--app-pane-shadow);
}
.ws-chapters-h {
    padding: 14px 10px 4px;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--text-subtle);
}
.ws-chapters-spacer { height: 22px; }
/* One entry: glyph · label · one-word state. Hover is a wash; the
   current chapter is ink + a bar, never a fill (selection outranks hover). */
/* One entry: glyph · label · one-word state. A wrapping flex row: the label
   keeps its words, and a state that no longer fits beside it drops to a
   second line, right-aligned - nothing is ever bitten. Hover is a wash; the
   current chapter is ink + a bar, never a fill (selection outranks hover). */
.ws-chapter-link {
    position: relative;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    column-gap: 10px;
    min-height: 32px;
    padding: 4px 10px;
    border-radius: 6px;
    color: var(--text-muted);
    font-size: 13px;
    font-weight: 500;
    text-decoration: none;
    transition: background .12s ease, color .12s ease;
}
.ws-chapter-link:hover { background: color-mix(in srgb, var(--text) 7%, transparent); color: var(--text); text-decoration: none; }
.ws-chapter-link:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.ws-chapter-link[aria-current="true"] { color: var(--text); font-weight: 600; }
.ws-chapter-link[aria-current="true"]::before {
    content: "";
    position: absolute;
    left: 0;
    top: 8px;
    bottom: 8px;
    width: 2px;
    border-radius: 2px;
    background: var(--text);
}
.ws-chapter-link.is-danger, .ws-chapter-link.is-danger:hover { color: var(--danger); }
.ws-chapter-link.is-danger[aria-current="true"]::before { background: var(--danger); }
.ws-chapter-glyph { display: inline-flex; flex: 0 0 16px; }
.ws-chapter-glyph svg { width: 16px; height: 16px; }
.ws-chapter-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ws-chapter-state { flex: 0 1 auto; margin-left: auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; font-size: 11px; font-weight: 500; color: var(--text-subtle); white-space: nowrap; }
.ws-chapter-chevron { display: none; flex: 0 0 12px; color: var(--text-subtle); }
.ws-chapter-chevron svg { width: 12px; height: 12px; }

/* The pane is the containing block of everything positioned inside it: an
   absolutely positioned descendant (a token's hidden input, a popover) whose
   containing block sat ABOVE the pane escaped the pane's clip and stretched
   the shell's own scroll area to the depth of the row it lived in - the
   settings window scrolled 18,000px into whiteness (owner, 2026-09-14,
   Brand Studio's thesaurus). */
.ws-pane { position: relative; display: flex; flex-direction: column; gap: 1rem; min-width: 0; }
.ws-pane-back { display: none; }
.ws-chapter { display: none; flex-direction: column; gap: 1rem; min-width: 0; }
.ws-chapter.is-active { display: flex; }
/* What follows a chapter's head: its groups, one column (a headed chapter wraps
   them so they scroll together under the pinned head). */
.ws-chapter-body { display: flex; flex-direction: column; gap: 1rem; min-width: 0; }
.ws-chapter-head { display: flex; flex-wrap: wrap; align-items: center; gap: 4px .5rem; min-height: 34px; padding: 6px 2px 0; }
.ws-chapter-title { margin: 0; font-size: 17px; font-weight: 700; letter-spacing: -.01em; color: var(--text); }
.ws-cat-title-danger { color: var(--danger); }

/* Every chapter and every card inside clears nothing sticky any more, but
   an anchor jump inside a chapter (#wsReconnect) still lands cleanly. */
.ws-detail-page [data-ws-chapter], .ws-detail-page [data-ws-cat], #wsSyncPanel, #wsReconnect { scroll-margin-top: 1rem; }

/* ── The frame is pinned (owner, 2026-09-05: "pin these 2 areas when
   scrolling"): the head and the rail never move, the PANE is the one
   scroller. app-shell.css makes a page under the ONE head a flex column
   (its head seat rule); the page takes the remaining height
   (so the shell itself has nothing left to scroll), the body grid
   stretches, and the pane scrolls inside its own box with a stable gutter
   so a short chapter and a long one share one content width. A rail
   taller than the viewport scrolls on its own; its current-bar sits inside
   the scroll box. The phone keeps the document flow - there the rail IS
   the page. The page's inset aligns the rail's border under the head's
   mark (the head's 20px + the trigger's 4px - its 4px negative margin). */
@media (min-width: 881px) {
    .ws-detail-page { flex: 1 1 auto; min-height: 0; height: auto; box-sizing: border-box; padding: 14px 20px 20px; }
    .ws-detail-body { flex: 1 1 auto; min-height: 0; align-items: stretch; grid-template-rows: minmax(0, 1fr); }
    .ws-chapters { position: static; top: auto; min-height: 0; overflow-y: auto; }
    /* The chapter's TITLE stays; only what is under it scrolls, and the one
       scrollbar starts below the head (owner, 2026-09-29: "Scrollbar is
       starting from the top. we don't want that. only from the scrollable
       area. The header/title must be pinned."). The pane is a frame; the
       active chapter fills it; the scroller is the chapter's body - the
       groups after a chapter head, or a single group's own body under its
       promoted head. A chapter of another shape scrolls whole. */
    .ws-pane { min-height: 0; overflow: hidden; padding: 2px 0 0 2px; }
    .ws-pane > * { flex: 0 0 auto; }
    .ws-pane > .ws-chapter.is-active { flex: 1 1 auto; min-height: 0; overflow-y: auto; scrollbar-gutter: stable; padding: 0 6px 1.5rem 0; }
    .ws-pane > .ws-chapter.is-active:has(> .ws-chapter-body),
    .ws-pane > .ws-chapter.is-active:not(:has(> section ~ section)):has(> .ws-cat) { overflow: hidden; padding: 0; }
    .ws-pane > .ws-chapter.is-active > .ws-chapter-head { flex: 0 0 auto; padding-right: 6px; }
    .ws-pane > .ws-chapter.is-active > .ws-chapter-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; scrollbar-gutter: stable; padding: 0 6px 1.5rem 0; }
    /* A single group: its body IS the card - the card fills the pane under the
       head and its rows scroll inside its own frame. */
    .ws-pane > .ws-chapter.is-active:not(:has(> section ~ section)) > .ws-cat > .ws-cat-body { flex: 0 1 auto; min-height: 0; overflow-y: auto; margin: 0 6px 1.5rem 0; }
    .ws-pane > .ws-chapter.is-active:not(:has(> section ~ section)) > .ws-cat { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
    .ws-pane > .ws-chapter.is-active:not(:has(> section ~ section)) > .ws-cat > .ws-cat-head { flex: 0 0 auto; margin-right: 6px; }
}

/* Phone: the rail is the page until a chapter is chosen; a chosen chapter
   drills in with a way back. Entries grow to a 48px touch row with the
   chevron the desktop hides. */
@media (max-width: 880px) {
    .ws-detail-body { grid-template-columns: minmax(0, 1fr); gap: 1rem; }
    .ws-chapters { position: static; padding-top: 0; }
    .ws-chapter-link { min-height: 48px; font-size: 15px; color: var(--text); }
    .ws-chapter-link[aria-current="true"]::before { display: none; }
    .ws-chapter-chevron { display: inline-flex; }
    .ws-detail-body[data-ws-drilled="false"] .ws-pane { display: none; }
    .ws-detail-body[data-ws-drilled="true"] .ws-chapters { display: none; }
    .ws-pane-back { display: inline-flex; align-items: center; gap: 4px; align-self: flex-start; font-size: 13px; color: var(--text-muted); text-decoration: none; }
    .ws-pane-back:hover { color: var(--text); text-decoration: none; }
    .ws-pane-back svg { width: 14px; height: 14px; }
}

/* ── Landing mark ────────────────────────────────────────────────────
   Jumping to a section scrolls a long page under a sticky strip, and
   the arrival is otherwise invisible: the reader sees new content and
   has to work out whether it is the thing they asked for. So the target
   says so for a moment - a soft ring that fades on its own, never a
   fill and never a state you have to dismiss.

   Applied by workspace-detail-nav.js and removed when it expires, so
   nothing here outlives the moment it explains. */
.ws-detail-landing {
    border-radius: 10px;
    animation: wsDetailLanding 1.6s ease-out forwards;
}
@keyframes wsDetailLanding {
    0%   { box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 26%, transparent); }
    35%  { box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 26%, transparent); }
    100% { box-shadow: 0 0 0 3px transparent; }
}
/* Reduced motion still gets the answer, it just does not animate: the
   ring holds for the same beat and the script takes it away. */
@media (prefers-reduced-motion: reduce) {
    .ws-detail-landing {
        animation: none;
        box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 26%, transparent);
    }
}

/* The 2FA-required policy flag, at home beside the access story it
   configures (it left the hero identity line). Quiet lock + muted text. */
.ws-detail-access-flag {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    margin-left: auto;
    font-size: 11px;
    font-weight: 600;
    color: var(--text-muted);
    white-space: nowrap;
}
.ws-detail-section-head .ws-detail-access-flag + .ws-detail-section-actions { margin-left: 0; }
.ws-detail-access-flag svg { width: 13px; height: 13px; flex: 0 0 auto; }

/* Sections (Categories, People, Gap, Settings, Danger). */
.ws-detail-section {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 1.25rem 1.5rem;
}
.ws-detail-section-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    margin-bottom: 1rem;
}
.ws-detail-section-title {
    margin: 0;
    font-size: 14px;
    font-weight: 700;
    color: var(--text);
    letter-spacing: -.005em;
}
.ws-detail-section-warn {
    border-color: color-mix(in srgb, var(--warning) 35%, var(--border));
    background: color-mix(in srgb, var(--warning) 4%, var(--surface));
}
.ws-detail-section-danger {
    border-color: color-mix(in srgb, var(--danger) 25%, var(--border));
}

/* ── Group cards ──────────────────────────────────────────────────────
   Every block inside a chapter is a <section class="ws-detail-section
   ws-cat" data-ws-cat> whose first child is the single _CategoryHead
   header: a quiet uppercase title, the ? help, the live state at the
   right edge, then any designed units. A chapter holding ONE group
   promotes that group's head to the chapter title by structure (no
   second section beside it): the card sheds its frame and its body wears
   it, so single- and multi-group chapters paint the identical title line.
   Never :only-of-type - Blink matches it only once the chapter has finished
   PARSING, so a first paint mid-stream drew the framed card and re-drew it
   solo a frame later (owner, 2026-09-26: zero layout shift). :has() matches
   as the DOM grows. */
.ws-cat { padding: 0; }
/* The head wraps as a whole when the frame is narrow: the title keeps its
   line and the state / units drop beneath it, right-aligned - never a
   help glyph stranded on a line of its own. */
.ws-cat > .ws-cat-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px .5rem;
    min-height: 30px;
    padding: 12px 20px 2px;
}
.ws-cat > .ws-cat-head .ws-detail-section-title, .ws-chapter-title { white-space: nowrap; }
.ws-cat > .ws-cat-head .ws-detail-section-title {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--text-muted);
}
.ws-cat > .ws-cat-head .ws-detail-section-title.ws-cat-title-danger { color: var(--danger); }
/* The head must read honestly on its own: the summary is the one-line
   live state at the right edge (quiet text, never a filled badge). */
.ws-cat-summary {
    margin-left: auto;
    font-size: 12px;
    font-weight: 500;
    color: var(--text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.ws-cat-body { padding: 6px 20px 12px; }
.ws-chapter:not(:has(> section ~ section)) > .ws-cat,
.ws-chapter-body:not(:has(> section ~ section)) > .ws-cat { border: 0; border-radius: 0; background: transparent; }
.ws-chapter:not(:has(> section ~ section)) > .ws-cat > .ws-cat-head,
.ws-chapter-body:not(:has(> section ~ section)) > .ws-cat > .ws-cat-head { min-height: 34px; padding: 6px 2px 0; margin-bottom: 1rem; }
.ws-chapter:not(:has(> section ~ section)) > .ws-cat > .ws-cat-head .ws-detail-section-title,
.ws-chapter-body:not(:has(> section ~ section)) > .ws-cat > .ws-cat-head .ws-detail-section-title {
    font-size: 17px;
    font-weight: 700;
    letter-spacing: -.01em;
    text-transform: none;
    color: var(--text);
}
.ws-chapter:not(:has(> section ~ section)) > .ws-cat > .ws-cat-head .ws-detail-section-title.ws-cat-title-danger,
.ws-chapter-body:not(:has(> section ~ section)) > .ws-cat > .ws-cat-head .ws-detail-section-title.ws-cat-title-danger { color: var(--danger); }
.ws-chapter:not(:has(> section ~ section)) > .ws-cat > .ws-cat-body,
.ws-chapter-body:not(:has(> section ~ section)) > .ws-cat > .ws-cat-body {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
}
.ws-chapter:not(:has(> section ~ section)) > .ws-cat.ws-detail-section-danger > .ws-cat-body,
.ws-chapter-body:not(:has(> section ~ section)) > .ws-cat.ws-detail-section-danger > .ws-cat-body { border-color: color-mix(in srgb, var(--danger) 25%, var(--border)); }
/* Designed units at the head's right edge: glyph + word doors
   ("100.0% indexed" opens the pending-index drawer). A <button> unit
   sheds its native chrome; state colours the whole unit (--success). */
.ws-cat-units { margin-left: auto; display: inline-flex; align-items: center; gap: 1rem; min-width: 0; }
.ws-unit {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 0;
    border: 0;
    background: transparent;
    appearance: none;
    -webkit-appearance: none;
    font: inherit;
    font-size: 12px;
    font-weight: 600;
    color: var(--text-muted);
    text-decoration: none;
    white-space: nowrap;
}
.ws-unit svg { width: 14px; height: 14px; flex: 0 0 auto; }
.ws-unit.is-ok { color: var(--success); }
.ws-unit-sub { font-weight: 500; color: var(--text-subtle); }
.ws-unit-door { cursor: pointer; }
.ws-unit-door:hover { color: var(--text); text-decoration: none; }
.ws-unit-door:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; border-radius: 4px; }
/* Titled blocks inside one body (Search & AI: what search does, then
   what AI may do) - the group-title grammar, one size down the tree. */
.ws-cat-subtitle {
    margin: 14px 0 2px;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--text-muted);
}
.ws-feature-rows > .ws-cat-subtitle:first-child { margin-top: 6px; }
/* Legacy inner section cards (Mail's sync story renders several) flatten
   into divided sub-blocks inside a group body - a card inside a card
   reads as a layout bug. Warn callouts keep their tinted box. */
.ws-cat-body .ws-detail-section {
    border: 0;
    border-radius: 0;
    padding: 0;
    background: transparent;
}
.ws-cat-body .ws-detail-section + .ws-detail-section,
.ws-cat-body .ws-detail-section + .ws-sync-panel,
.ws-cat-body .ws-sync-panel + .ws-detail-section {
    border-top: 1px solid var(--border);
    padding-top: 1.1rem;
    margin-top: 1.25rem;
}
.ws-cat-body .ws-detail-section-warn {
    border: 1px solid color-mix(in srgb, var(--warning) 35%, var(--border));
    border-radius: 10px;
    background: color-mix(in srgb, var(--warning) 4%, var(--surface));
    padding: .9rem 1.1rem;
}
/* ── Inline access category ──────────────────────────────────────────
   The invite picker leads, the managed roster follows, and the
   team-grant / blocked / nomination / send-audit sub-blocks reuse the
   ws-perm-group title grammar with the share-row line styles. */
/* The invite is ONE line: the chip picker grows, the verb keeps its width,
   both share a height; the how-to rides the verb's tooltip. */
.ws-access-invite { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 1.1rem; }
.ws-access-invite .share-chip-input { flex: 1 1 auto; min-width: 0; }
.ws-access-invite .ws-access-invite-go { flex: 0 0 auto; min-height: 39px; display: inline-flex; align-items: center; white-space: nowrap; }
.ws-access-invite [data-chip-hidden-fields] { display: contents; }
/* The roster inside a chapter is ONE column of full-width rows - the shared
   grid's auto-fill columns cut names in half in the pane (owner, 2026-09-05). */
.ws-cat-body .collab-grid { grid-template-columns: minmax(0, 1fr); column-gap: 0; padding: 0; }
.ws-cat-body .collab-grid-cell { border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent); }
.ws-cat-body .collab-grid-cell:last-child { border-bottom: 0; }
.ws-cat-body .collab-grid .coworker-row { padding-left: .25rem; padding-right: .25rem; }
.ws-cat-body .collab-row-gutter { flex-direction: row-reverse; }
/* Sub-blocks (pending invitations, teams, revoked, nominations, sends): the
   group-title grammar, one hairline above each. */
.ws-access-subblock { margin-top: 1.35rem; padding-top: 1.15rem; border-top: 1px solid var(--border); }
.ws-access-subblock > .ws-perm-group-title {
    margin: 0 0 .5rem;
    font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
    color: var(--text-muted);
}
.ws-access-lines { display: flex; flex-direction: column; }
.ws-access-line {
    display: flex;
    align-items: center;
    gap: .6rem;
    padding: .4rem 0;
    font-size: 13px;
}
.ws-access-line + .ws-access-line { border-top: 1px solid var(--border); }
.ws-access-line-label { color: var(--text); font-weight: 500; }
.ws-access-line-meta { color: var(--text-muted); font-size: 12px; margin-left: auto; }
.collab-row-action-text.is-danger { color: var(--danger); }

/* Categories grid. */
.ws-detail-categories {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: .65rem;
}
.ws-detail-cat {
    display: flex;
    align-items: center;
    gap: .8rem;
    padding: .85rem 1rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    transition: border-color .15s ease, background-color .15s ease;
}
.ws-detail-cat:hover {
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
}
.ws-detail-cat-icon {
    width: 44px; height: 44px;
    border-radius: 12px;
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(135deg,
        color-mix(in srgb, var(--bf-color, var(--primary)) 18%, var(--surface)) 0%,
        color-mix(in srgb, var(--bf-color, var(--primary)) 8%, var(--surface)) 100%);
    color: var(--bf-color, var(--primary));
    box-shadow:
        inset 0 1px 0 color-mix(in srgb, white 35%, transparent),
        0 2px 6px -2px color-mix(in srgb, var(--bf-color, var(--primary)) 25%, transparent);
}
.ws-detail-cat-icon svg { width: 22px; height: 22px; }
.ws-detail-cat[data-cat="Sent"] .ws-detail-cat-icon {
    background: linear-gradient(135deg,
        color-mix(in srgb, #06B6D4 18%, var(--surface)) 0%,
        color-mix(in srgb, #06B6D4 8%, var(--surface)) 100%);
    color: #0891B2;
    box-shadow:
        inset 0 1px 0 color-mix(in srgb, white 35%, transparent),
        0 2px 6px -2px color-mix(in srgb, #06B6D4 25%, transparent);
}
.ws-detail-cat[data-cat="Draft"] .ws-detail-cat-icon {
    background: linear-gradient(135deg,
        color-mix(in srgb, #6366F1 18%, var(--surface)) 0%,
        color-mix(in srgb, #6366F1 8%, var(--surface)) 100%);
    color: #4F46E5;
    box-shadow:
        inset 0 1px 0 color-mix(in srgb, white 35%, transparent),
        0 2px 6px -2px color-mix(in srgb, #6366F1 25%, transparent);
}
.ws-detail-cat[data-cat="Spam"] .ws-detail-cat-icon {
    background: linear-gradient(135deg,
        color-mix(in srgb, var(--warning) 22%, var(--surface)) 0%,
        color-mix(in srgb, var(--warning) 10%, var(--surface)) 100%);
    color: var(--warning);
    box-shadow:
        inset 0 1px 0 color-mix(in srgb, white 35%, transparent),
        0 2px 6px -2px color-mix(in srgb, var(--warning) 30%, transparent);
}
.ws-detail-cat[data-cat="Trash"] .ws-detail-cat-icon {
    background: linear-gradient(135deg,
        color-mix(in srgb, var(--text-subtle) 18%, var(--surface)) 0%,
        color-mix(in srgb, var(--text-subtle) 8%, var(--surface)) 100%);
    color: var(--text-muted);
}
.ws-detail-cat-body {
    display: flex;
    flex-direction: column;
    min-width: 0;
}
.ws-detail-cat-name {
    font-size: 10px;
    font-weight: 600;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: .06em;
}
.ws-detail-cat-count {
    font-size: 1.15rem;
    font-weight: 700;
    color: var(--text);
    font-variant-numeric: tabular-nums;
    letter-spacing: -.01em;
}

/* People grid. Cards with name + email + a role badge. The owner
   tier gets a subtle accent so the workspace operator is visually
   anchored at the top of the grid. */
.ws-detail-people {
    display: grid;
    /* Tighter minmax so wider workspaces (4-6 collaborators) fit
       on one row at the standard 1200px page width without the
       card layout cropping. The card still ellipsis-truncates
       long names + emails so shrinking doesn't break readability. */
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: .55rem;
}
.ws-detail-person {
    display: flex;
    align-items: center;
    gap: .75rem;
    padding: .65rem .8rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    transition: border-color .12s, box-shadow .12s, background .12s;
}
.ws-detail-person:hover {
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
    box-shadow: 0 2px 6px rgba(0,0,0,.04);
}
.ws-detail-person[data-role="owner"] {
    background: linear-gradient(180deg,
        color-mix(in srgb, var(--primary) 5%, var(--surface)),
        var(--surface));
    border-color: color-mix(in srgb, var(--primary) 25%, var(--border));
}
.ws-detail-person-avatar {
    width: 38px; height: 38px;
    border-radius: 50%;
    object-fit: cover;
    background: var(--avatar-bg);
    color: var(--avatar-color);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 13px;
    font-weight: 600;
    flex: 0 0 38px;
}
.ws-detail-person-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
    flex: 1 1 auto;
    gap: .1rem;
}
.ws-detail-person-name {
    font-size: 13px;
    font-weight: 600;
    color: var(--text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    line-height: 1.3;
}
.ws-detail-person-email {
    font-size: 11px;
    color: var(--text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    line-height: 1.3;
}
.ws-detail-person-badge {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    padding: .2rem .5rem;
    border-radius: 999px;
    font-size: 10px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .04em;
    border: 1px solid var(--border);
    background: var(--surface-2, #f8fafc);
    color: var(--text-muted);
    line-height: 1;
}
.ws-detail-person-badge svg { display: block; }
.ws-detail-person-badge[data-role="owner"] {
    color: var(--primary);
    background: transparent;
    border-color: var(--primary);
}
.ws-detail-person-badge[data-role="admin"] {
    color: var(--text-muted);
    background: transparent;
    border-color: color-mix(in srgb, currentColor 35%, transparent);
}

/* ── Collaborator list partial (_CollaboratorList.cshtml) ──────────────
   Multi-column grid of premium row cells. Used by:
     • Detail page Access & permissions section
     • +N collaborators modal on /Workspaces
     • Teams page member list
   ONE source of truth, ZERO chip/pill rendering. Each cell is the
   .coworker-row pattern from /Collaborators (avatar + name + email +
   subtle role text). The grid auto-fills columns at min 280px so wide
   surfaces pack 3-4 per visual row, narrower surfaces 2, mobile 1.

   Premium contract (from feedback_no_rounded_pill_badges):
     • No `border-radius: 999px` rounded chip containers per item.
     • Borders sit between cells, never around them.
     • Role text is muted/typographic, not an uppercase-letter-spaced
       capsule. Owner reads as primary-color brand; Admin tinted;
       Viewer muted neutral. */
.collab-grid {
    list-style: none;
    margin: 0;
    display: grid;
    /* 320px min so rows with rich content (meta line + chevron) keep
       breathing room; on a 1200px page that's 3 columns, on the
       modal-lg (~720px content) that's 2, on mobile 1. */
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    /* Inter-column gap is whitespace, not a hairline. The previous
       gradient-painted column divider crossed the row hairlines at
       every intersection and read as a "+" crosshair - not premium.
       A small gap gives visual separation without the lines-on-
       lines feel. */
    column-gap: 2rem;
    row-gap: 0;
    /* Inner padding only - NO border / NO shadow / NO border-radius
       on the grid itself. Every caller (the /Workspaces modal, the
       Detail page Access panel, the /Teams card, the
       /Collaborators "People you work with" panel) already wraps the
       grid in a parent card surface. Painting another border around
       the grid produced two concentric outlines that pixel-aligned
       at the corners - the "border over border" the user reported.
       Padding here is enough to keep the rows from touching the
       parent card's edges. */
    background: transparent;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    padding: .65rem 1.75rem;
}
/* Standalone use (no parent card) - bring back the surface chrome
   so the grid still reads as a self-contained list. Triggered by
   adding `.collab-grid--standalone` on the <ul>; today no caller
   needs it but the hook is here for future contexts. */
.collab-grid--standalone {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    box-shadow: 0 1px 2px rgba(15, 23, 42, .04), 0 1px 1px rgba(15, 23, 42, .02);
}
.collab-grid-cell {
    /* Hairline row divider, BETWEEN cells only (the :last-child and
       :nth-last-child rule below clear the bottom border on the
       cell(s) that visually sit on the last row, so the card's outer
       border-bottom isn't doubled). */
    border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
    position: relative;
    transition: background .14s ease;
}
/* Last cell never gets a divider underneath - it'd double up with
   the card's outer border. */
.collab-grid-cell:last-child {
    border-bottom: 0;
}
/* When there's an even number of cells in a 2-column layout, the
   second-to-last cell shares the visual last row with :last-child;
   without this it would draw a hairline below the bottom row. The
   :nth-last-child(2):nth-child(odd) check (one before last + odd
   index) catches "left cell of the last row in a 2-col grid".
   Auto-fill makes column count viewport-dependent so we additionally
   guard with a media query so the rule only fires when 2 cols are
   actually shown. */
@media (min-width: 700px) {
    .collab-grid-cell:nth-last-child(2):nth-child(odd):not(.ws-cat-body .collab-grid-cell) {
        border-bottom: 0;
    }
}
/* Owner row: hierarchy rides the ROLE LABEL's weight alone (platform
   rule: no tinted fills, no colored accent bars on rows - the violet
   stripe + wash read as decoration and drew every eye to one row). */
.collab-grid .coworker-row {
    /* Horizontal breathing room comes from the grid container's
       1.75rem padding, so the row itself only needs vertical
       padding + a tiny horizontal nudge for hover-tint inset. */
    padding: .9rem .35rem;
    gap: .85rem;
    border-radius: 0;
}
.collab-grid .coworker-row + .coworker-row {
    border-top: 0;
}
.collab-grid .coworker-row:hover,
.collab-grid button.collab-row-clickable:hover .coworker-row,
.collab-grid button.collab-row-clickable:focus-visible .coworker-row {
    background: color-mix(in srgb, var(--text) 5%, var(--surface));
}

/* Compact variant for inside-modal use. Smaller avatar + tighter
   padding so the modal fits more rows without scrolling. */
.collab-grid--compact .coworker-row {
    padding: .5rem .85rem;
    gap: .65rem;
}
.collab-grid--compact .coworker-avatar {
    width: 32px; height: 32px;
    font-size: 11px;
}
.collab-grid--compact .coworker-name { font-size: 12px; }
.collab-grid--compact .coworker-email { font-size: 10px; }

/* Single-column fallback for very narrow surfaces (mobile, side
   panels). The auto-fill with minmax(280px) already collapses to 1
   column under 280px, but explicit override locks the layout in. */
@media (max-width: 540px) {
    .collab-grid {
        grid-template-columns: 1fr;
    }
    .collab-grid-cell {
        background-image: none;
    }
}

/* Collapsed mode (CollaboratorListModel.CollapsedVisibleCount): the
   partial SSR-renders the list with .is-collapsed and marks the rows
   beyond the visible count with --overflow, so first paint is already
   the collapsed state (no flash-then-collapse shift). The toggle
   below the list flips .is-collapsed client-side. */
.collab-grid.is-collapsed .collab-grid-cell--overflow { display: none; }
.collab-collapse-toggle {
    display: flex;
    align-items: center;
    gap: .6rem;
    width: 100%;
    padding: .7rem .35rem;
    margin: 0;
    background: none;
    border: 0;
    font: inherit;
    font-size: 12px;
    font-weight: 600;
    color: var(--text-muted);
    cursor: pointer;
    transition: color .14s ease;
}
/* Divider above the toggle: while collapsed the last VISIBLE cells
   still draw their own bottom hairline (they are not :last-child),
   so the toggle adds none. Expanded, the list's true last row is
   borderless by design (the card edge follows), so the toggle
   supplies the hairline itself. */
.collab-collapse-toggle[aria-expanded="true"] {
    border-top: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
}
.collab-collapse-toggle:hover,
.collab-collapse-toggle:focus-visible { color: var(--text); }
.collab-collapse-toggle:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; border-radius: 6px; }
/* Overlapping avatar preview of the hidden remainder. Decorative -
   the label carries the count - so it disappears once expanded. */
.collab-collapse-faces { display: inline-flex; align-items: center; }
.collab-collapse-face {
    width: 22px; height: 22px;
    border-radius: 50%;
    font-size: 9px;
    box-shadow: 0 0 0 2px var(--surface);
}
.collab-collapse-face + .collab-collapse-face { margin-left: -7px; }
.collab-collapse-chevron { margin-left: auto; transition: transform .16s ease; }
.collab-collapse-toggle[aria-expanded="true"] .collab-collapse-chevron { transform: rotate(180deg); }
.collab-collapse-toggle[aria-expanded="true"] .collab-collapse-faces { display: none; }

/* Right-side gutter inside each row: holds the role indicator and any
   optional inline action (e.g. the Teams remove-from-team X button).
   margin-left: auto pushes the whole gutter to the row's right edge so
   .coworker-text can flex to fill the middle. */
.collab-row-gutter {
    flex: 0 0 auto;
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    gap: .55rem;
    white-space: nowrap;
}

/* Avatar + presence dot wrapper. The dot is positioned in the
   bottom-right corner of the avatar; without the wrapper it would
   anchor to the row, not to the (variable-width) avatar. */
.collab-row-avatar-wrap {
    position: relative;
    flex: 0 0 auto;
    display: inline-flex;
}
.collab-row-presence {
    position: absolute;
    right: -1px;
    bottom: -1px;
    width: 11px;
    height: 11px;
    border-radius: 50%;
    box-shadow: 0 0 0 2px var(--surface);
    pointer-events: none;
}
.collab-row-presence.is-active {
    background: #16a34a; /* green */
}
.collab-row-presence.is-recent {
    background: #f59e0b; /* amber */
}
.collab-row-presence.is-stale {
    background: var(--text-subtle);
    opacity: .65;
}
/* Compact row gets a smaller dot to match the smaller avatar. */
.collab-grid--compact .collab-row-presence {
    width: 9px;
    height: 9px;
}

/* "you" tag next to the current user's name. Subtle inline pill-free
   marker - small uppercase letterspacing in a soft primary tint, kept
   physically separate from the role indicator so the eye reads it as
   self-identification, not as a role. NOT a rounded badge. */
.collab-row-self-tag {
    display: inline-block;
    margin-left: .35rem;
    font-size: 9px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--primary);
    line-height: 1;
    vertical-align: middle;
}

/* Optional inline name tag (e.g. "Workspace owner" on /Collaborators).
   Same micro-typographic style as the "you" tag - no pill chrome.
   Uses the muted text color instead of primary so the tag describes
   the person without competing visually with their name. */
.collab-row-name-tag {
    display: inline-block;
    margin-left: .35rem;
    font-size: 9px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--text-muted);
    line-height: 1;
    vertical-align: middle;
}

/* Forces single-column when the per-row content is rich (meta line
   + chevron). Used by /Collaborators where 2+ columns would crowd
   the meta line text. */
.collab-grid--single {
    grid-template-columns: 1fr !important;
}
.collab-grid--single .collab-grid-cell {
    background-image: none;
}

/* Toolbar row above the list: filter input + sort dropdown. Sits
   inside a card edge so it reads as part of the same surface
   without an explicit divider. */
.collab-toolbar {
    display: flex;
    align-items: center;
    gap: .85rem;
    padding: .65rem 1.25rem .75rem;
    flex-wrap: wrap;
}
.collab-toolbar-search {
    position: relative;
    /* Don't grow to fill the row - a filter field doesn't need the full content
       width. Cap at a comfortable 360px and let Sort sit right beside it; still
       shrinks (flex-shrink:1 + min-width:0) to fit a narrow container like the
       +N collaborators modal. */
    flex: 0 1 360px;
    min-width: 0;
    display: inline-flex;
    align-items: center;
}
.collab-toolbar-search > svg {
    position: absolute;
    left: .65rem;
    color: var(--text-muted);
    pointer-events: none;
}
.collab-toolbar-input {
    width: 100%;
    padding: .35rem .65rem .35rem 2rem;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--surface);
    color: var(--text);
    font-size: 12px;
    transition: border-color .12s, box-shadow .12s;
}
.collab-toolbar-input:focus {
    border-color: var(--primary);
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent);
}
.collab-toolbar-sort {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    flex: 0 0 auto;
}
.collab-toolbar-sort-label {
    font-size: 11px;
    color: var(--text-muted);
}
.collab-toolbar-empty {
    width: 100%;
    text-align: center;
    color: var(--text-muted);
    font-size: 12px;
    padding: .5rem 0;
}

/* Whole-row clickable variant. Resets <button> chrome so it looks
   like a card row, not a control. The chevron telegraphs that the
   row leads somewhere; primary tint on hover signals the affordance. */
button.collab-row-clickable {
    width: 100%;
    background: transparent;
    border: 0;
    text-align: left;
    cursor: pointer;
    color: inherit;
    font: inherit;
    padding: 0; /* the .coworker-row inside owns padding */
    display: flex;
    align-items: center;
    gap: 0;
}
button.collab-row-clickable .coworker-row {
    width: 100%;
    /* The button itself is the focus target; the inner row absorbs
       the row-specific padding + hover. */
}
button.collab-row-clickable:hover .coworker-row,
button.collab-row-clickable:focus-visible .coworker-row {
    background: color-mix(in srgb, var(--text) 5%, var(--surface));
}
button.collab-row-clickable:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: -2px;
}
.collab-row-chevron {
    flex: 0 0 auto;
    margin-right: .9rem;
    color: var(--text-muted);
    opacity: .5;
    transition: opacity .12s ease, transform .12s ease, color .12s ease;
    display: inline-flex;
    align-items: center;
}
button.collab-row-clickable:hover .collab-row-chevron,
button.collab-row-clickable:focus-visible .collab-row-chevron {
    opacity: 1;
    color: var(--text);
    transform: translateX(2px);
}

/* Meta line (3rd line under name + email). Used by /Collaborators for
   "1 workspace shared · 2 teams in common" relationship summary. */
.coworker-meta {
    font-size: 11px;
    color: var(--text-muted);
    margin-top: .1rem;
    line-height: 1.35;
}
.coworker-meta strong {
    color: var(--text);
    font-weight: 600;
}

/* Right-aligned subtle role indicator. Word + color, not a capsule. */
.collab-row-role {
    flex: 0 0 auto;
    font-size: 11px;
    font-weight: 600;
    line-height: 1.4;
    color: var(--text-muted);
    white-space: nowrap;
}
.collab-row-role-owner {
    color: var(--text);
    font-weight: 700;
}
.collab-row-role-admin {
    color: var(--text);
    font-weight: 600;
}
.collab-row-role-viewer {
    color: var(--text-subtle);
    font-weight: 500;
}
/* Pending invitation: warm amber tone with an inline clock glyph so
   the unfinished state reads at a glance against the cool
   primary / muted palette of accepted-member rows. Inline-flex aligns
   the icon's optical center with the text baseline. */
.collab-row-role-pending {
    color: #b45309;
    font-weight: 600;
    display: inline-flex;
    align-items: center;
    gap: .3rem;
}
html[data-theme="dark"] .collab-row-role-pending { color: var(--warning); }

/* Row-level pending treatment. Mute the avatar + email so the row
   reads as "in flight" without losing scannability. The role tag in
   the gutter carries the primary signal; this is the reinforcement
   so the reader's eye finds pending rows even before the gutter. */
.coworker-row.is-pending .coworker-avatar {
    opacity: .55;
    border: 1px dashed var(--border, #e2e8f0);
    box-sizing: border-box;
}
.coworker-row.is-pending .coworker-name { color: var(--text-muted); }
.coworker-row.is-pending .coworker-email { color: var(--text-subtle); }

/* Optional inline per-row action (passed through CollaboratorRow.ActionHtml).
   Currently powers the Teams page remove-from-team X button. Sized to match
   the role indicator's vertical rhythm so the gutter stays balanced. */
.collab-row-action-form {
    display: inline-flex;
    margin: 0;
    padding: 0;
}
.collab-row-action {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px; height: 22px;
    padding: 0;
    border: 0;
    border-radius: 4px;
    background: transparent;
    color: var(--text-subtle);
    cursor: pointer;
    opacity: 0;
    transition: opacity .12s ease, background .12s ease, color .12s ease;
}
.collab-row-action svg {
    width: 12px; height: 12px;
    display: block;
}
/* Reveal on row hover/focus so the X doesn't add visual noise to every
   resting row, but stays discoverable. Keyboard users get it via the
   focus-within fallback. */
.coworker-row:hover .collab-row-action,
.coworker-row:focus-within .collab-row-action {
    opacity: .8;
}
.collab-row-action:hover,
.collab-row-action:focus-visible {
    opacity: 1;
    background: color-mix(in srgb, currentColor 12%, transparent);
}
.collab-row-action-remove:hover,
.collab-row-action-remove:focus-visible {
    color: var(--danger, #dc2626);
    background: color-mix(in srgb, var(--danger, #dc2626) 12%, transparent);
}

/* Text-label row action (Teams "Make manager" / "Remove manager"). The
   icon-sized .collab-row-action is a fixed 22px square - a TEXT label inside
   it overflows the box and lands on top of the role indicator beside it. This
   variant sizes to its label, sharing the icon button's hover-reveal rhythm so
   the gutter reads as one set of actions. Word + tint on hover, no pill. */
.collab-row-action-text {
    display: inline-flex;
    align-items: center;
    width: auto;
    height: 22px;
    padding: 0 .5rem;
    border: 0;
    border-radius: 6px;
    background: transparent;
    color: var(--text-muted);
    font-size: 10px;
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
    cursor: pointer;
    opacity: 0;
    transition: opacity .12s ease, background .12s ease, color .12s ease;
}
.coworker-row:hover .collab-row-action-text,
.coworker-row:focus-within .collab-row-action-text {
    opacity: .85;
}
.collab-row-action-text:hover,
.collab-row-action-text:focus-visible {
    opacity: 1;
    color: var(--text);
    background: color-mix(in srgb, var(--text) 5%, transparent);
}

.collab-list-caption {
    margin-top: .15rem;
    margin-bottom: .65rem;
}
/* Modal body: edge-to-edge grid for a premium "list inside a card"
   feel. Caption gets explicit horizontal padding since it's a sibling
   of the edge-to-edge grid. */
.collab-modal-body {
    padding: .75rem 0 0 0;
}
.collab-modal-body .collab-list-caption {
    padding: 0 1.25rem;
}
.collab-modal-body .collab-grid .coworker-row {
    padding-left: 1.25rem;
    padding-right: 1.25rem;
}

/* Collaborators modal header: gives the workspace title room to
   breathe (Bootstrap's default modal-header packs the title hard
   against the dialog edge, which read as cramped here). The two
   stacked lines - title + "N people have access." - share a
   left column; the close button stays right-aligned. */
.collab-modal .collab-modal-header {
    padding: 1.25rem 1.5rem 1rem;
    align-items: flex-start;
    gap: 1rem;
}
.collab-modal .collab-modal-titles {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: .25rem;
}
.collab-modal .modal-title {
    font-family: "Plus Jakarta Sans", "Inter", -apple-system, system-ui, sans-serif;
    font-weight: 700;
    font-size: 15px;
    letter-spacing: -.01em;
    line-height: 1.3;
    color: var(--text);
    margin: 0;
}
.collab-modal .collab-modal-subtitle {
    margin: 0;
    font-size: 12px;
    color: var(--text-muted);
    line-height: 1.4;
}
.collab-modal .collab-modal-subtitle:empty {
    display: none;
}
.collab-modal .btn-close {
    margin: .25rem 0 0 0;
    flex: 0 0 auto;
}

/* Settings grid. */
.ws-detail-settings {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: .65rem;
}
.ws-detail-setting {
    display: grid;
    grid-template-columns: auto 1fr;
    grid-template-rows: auto auto;
    gap: .15rem .8rem;
    padding: .85rem 1rem;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 10px;
    text-align: left;
    cursor: pointer;
    transition: border-color .15s ease, background-color .15s ease, transform .12s ease;
}
.ws-detail-setting:hover {
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
    background: var(--surface);
    transform: translateY(-1px);
}
.ws-detail-setting > svg {
    grid-row: 1 / span 2;
    grid-column: 1;
    width: 18px; height: 18px;
    color: var(--primary);
    align-self: center;
}
.ws-detail-setting > .ws-accent-dot {
    grid-row: 1 / span 2;
    grid-column: 1;
    align-self: center;
    width: 18px; height: 18px;
    background: var(--bf-color, var(--primary));
    border-radius: 50%;
    box-shadow: 0 0 0 1px color-mix(in srgb, var(--bf-color, var(--primary)) 50%, transparent);
}
.ws-detail-setting-label {
    grid-column: 2;
    font-size: 13px;
    font-weight: 600;
    color: var(--text);
}
.ws-detail-setting-sub {
    grid-column: 2;
    font-size: 11px;
    color: var(--text-muted);
}

/* Gap warning. */
.ws-detail-gap p {
    margin: 0 0 .65rem;
    font-size: 12px;
    color: var(--text);
    line-height: 1.5;
}
.ws-detail-gap p:last-child { margin-bottom: 0; }

/* Danger zone. */
.ws-detail-danger {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
}
.ws-detail-danger-text { flex: 1 1 auto; min-width: 280px; }
/* Action button stays right-aligned even when the row wraps to a
   second line. Without `margin-left: auto`, a wrapped button
   reflows to the row's left edge, producing the visual
   misalignment the user caught (Convert button on the left,
   Remove button on the right). With this rule, both buttons
   land on the right edge regardless of how the description
   text fills the row. */
.ws-detail-danger > .btn { margin-left: auto; flex-shrink: 0; }
.ws-detail-danger-text strong {
    display: block;
    font-size: 13px;
    color: var(--text);
    margin-bottom: .25rem;
}
.ws-detail-danger-text p {
    margin: 0;
    font-size: 12px;
    color: var(--text-muted);
    line-height: 1.5;
}

/* Thin separator between the two danger-zone actions (Convert /
   Remove) so they read as distinct rows rather than a wall of
   destructive copy. Color matches the section's existing border
   tint so it doesn't compete for attention. */
.ws-detail-danger-divider {
    margin: 1rem 0;
    border: 0;
    border-top: 1px solid color-mix(in srgb, var(--danger) 18%, var(--border));
    opacity: .6;
}

/* Convert-to-archive consequences list inside the modal. Each
   item leads with a strong label so a user scanning quickly can
   see "Sync stops / Credentials are removed / This is reversible"
   without reading the full sentence. Tight line-height, no
   bullets - the bold lead-in does the visual job. */
.ws-convert-consequences {
    margin: 1rem 0 .5rem;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: .55rem;
}
.ws-convert-consequences li {
    padding-left: 1.2rem;
    position: relative;
    font-size: 12px;
    line-height: 1.5;
    color: var(--text-muted);
}
.ws-convert-consequences li::before {
    content: "·";
    position: absolute;
    left: .35rem;
    top: -.05rem;
    color: var(--text-subtle);
    font-weight: 700;
}
.ws-convert-consequences li strong {
    color: var(--text);
    font-weight: 600;
}

/* Read-only state on read-only mail workspaces. Quiet inline
   italic note alongside the workspace title - no chip, no pill,
   no badge, no border, no background, no button. Reads as a
   piece of editorial copy that names the state ("read-only ·
   only notes can be added") with a tiny lock glyph for scan-
   ability. The full tooltip on hover spells out exactly what
   works vs what doesn't. */
.readonly-note {
    display: inline-flex;
    align-items: baseline;
    gap: .35rem;
    margin-left: .8rem;
    color: var(--text-muted);
    font-size: 12px;
    font-style: italic;
    font-weight: 400;
    line-height: 1.2;
    vertical-align: baseline;
    white-space: nowrap;
    cursor: default;
}
.readonly-note svg {
    flex: 0 0 auto;
    width: 12px;
    height: 12px;
    color: var(--text-subtle);
    opacity: .85;
    transform: translateY(2px);
}

/* ==================================================================
   Connect-inbox shared form styles. Used by:
     /Workspaces/New/Inbox/Gmail   (Add.cshtml - Connect mailbox)
   The page uses a hero / two-column layout / form card / help aside
   vocabulary; keeping these in site.css avoids duplicating the same
   CSS in the page's local <style> block. Add.cshtml retains its own
   page-specific rules (ci-grid two-column layout for the Add page,
   ci-label-row, ci-info-btn, ci-guide-link and ci-guide-modal modal
   styles).
   ================================================================== */

.ci-page {
    max-width: 1080px;
    margin: 0 auto;
    padding: 2rem 1.5rem 2.5rem;
}

.ci-back {
    display: inline-flex; align-items: center; gap: .35rem;
    margin-bottom: 1rem;
    padding: .25rem .5rem .25rem .15rem;
    border-radius: 8px;
    color: var(--text-muted);
    text-decoration: none;
    font-size: 12px;
}
.ci-back:hover { color: var(--text); background: var(--surface-2); }

.ci-hero { margin-bottom: 1.4rem; }
.ci-hero h1 {
    font-family: "Plus Jakarta Sans", "Inter", -apple-system, system-ui, sans-serif;
    font-weight: 700;
    font-size: clamp(1.45rem, 2.2vw, 1.75rem);
    letter-spacing: -.025em;
    line-height: 1.2;
    margin: 0 0 .25rem;
    color: var(--text);
}
.ci-hero p {
    margin: 0;
    color: var(--text-muted);
    font-size: 13px;
}

.ci-error {
    margin-bottom: 1rem;
    padding: .65rem .9rem;
    border-radius: 10px;
    border: 1px solid color-mix(in srgb, var(--danger) 30%, var(--border));
    background: color-mix(in srgb, var(--danger) 7%, var(--surface));
    color: color-mix(in srgb, var(--danger) 80%, var(--text));
    font-size: 12px; line-height: 1.5;
}
.ci-error a { color: inherit; text-decoration: underline; font-weight: 600; }
.ci-error code {
    font-family: var(--bs-font-monospace, ui-monospace, SFMono-Regular, monospace);
    font-size: .85em;
    padding: .05em .3em;
    border-radius: 4px;
    background: color-mix(in srgb, var(--text) 8%, transparent);
}

.ci-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 1.5rem;
}
.ci-form { display: flex; flex-direction: column; gap: 1rem; }

.ci-field { display: flex; flex-direction: column; gap: .35rem; }
.ci-field label {
    font-size: 12px;
    font-weight: 600;
    color: var(--text);
}

.ci-input {
    display: flex; align-items: center;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 0 .9rem;
    transition: border-color .15s ease, box-shadow .15s ease;
}
.ci-input:focus-within {
    border-color: var(--primary);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 15%, transparent);
}
.ci-input svg {
    flex: 0 0 auto;
    width: 16px; height: 16px;
    color: var(--text-subtle);
    margin-right: .65rem;
}
.ci-input input {
    flex: 1 1 auto; min-width: 0;
    padding: .7rem 0;
    background: transparent;
    border: 0;
    outline: none;
    font-size: 13px;
    color: var(--text);
    font-family: inherit;
}
.ci-input input::placeholder { color: var(--text-subtle); }

.ci-hint {
    font-size: 11px;
    color: var(--text-muted);
    line-height: 1.5;
}
.ci-hint[data-app-password-state="valid"]   { color: var(--success); }
.ci-hint[data-app-password-state="valid"]::before   { content: "✓ "; font-weight: 700; }
.ci-hint[data-app-password-state="wrong"]   { color: var(--warning); }
.ci-hint[data-app-password-state="wrong"]::before   { content: "⚠ "; }
.ci-hint a { color: inherit; text-decoration: underline; font-weight: 600; }

.ci-submit {
    width: 100%;
    margin-top: .15rem;
    padding: .75rem 1.2rem;
    background: var(--text);
    color: var(--surface, #fff);
    border: 0;
    border-radius: 10px;
    font-weight: 600; font-size: 13px;
    cursor: pointer;
    transition: background .15s ease, opacity .15s ease, transform .15s ease;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: .55rem;
    min-height: 46px;
}
.ci-submit:hover { background: color-mix(in srgb, var(--text) 84%, var(--surface)); }
.ci-submit.is-busy,
.ci-submit:disabled {
    background: color-mix(in srgb, var(--text) 70%, var(--surface));
    cursor: wait;
    opacity: .92;
    transform: none;
}
.ci-submit.is-busy:hover { background: color-mix(in srgb, var(--text) 5%, transparent); }
.ci-submit-spinner {
    display: inline-block;
    width: 16px; height: 16px;
    border-radius: 50%;
    border: 2px solid rgba(255,255,255,.4);
    border-top-color: #fff;
    animation: ci-submit-spin .7s linear infinite;
    flex: 0 0 auto;
}
@keyframes ci-submit-spin { to { transform: rotate(360deg); } }

.ci-trust {
    margin: .35rem 0 0;
    font-size: 11px;
    color: var(--text-muted);
    text-align: center;
    line-height: 1.5;
}

.ci-help {
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 1.25rem 1.35rem;
}
.ci-help h2 {
    font-family: "Plus Jakarta Sans", "Inter", -apple-system, system-ui, sans-serif;
    font-weight: 700; font-size: 14px;
    letter-spacing: -.01em;
    margin: 0 0 .25rem;
    color: var(--text);
}
.ci-help-lead {
    margin: 0 0 1rem;
    color: var(--text-muted);
    font-size: 12px; line-height: 1.5;
}

.ci-checklist {
    list-style: none; padding: 0; margin: 0 0 .75rem;
    counter-reset: ci-step;
}
.ci-checklist li {
    position: relative;
    padding: .55rem 0 .85rem 2rem;
    border-bottom: 1px solid var(--border);
    counter-increment: ci-step;
}
.ci-checklist li:last-child { border-bottom: 0; padding-bottom: .25rem; }
.ci-checklist li::before {
    content: counter(ci-step);
    position: absolute; left: 0; top: .55rem;
    width: 22px; height: 22px;
    border-radius: 50%;
    background: var(--surface);
    border: 1px solid var(--border-strong);
    color: var(--text);
    font-size: 10px; font-weight: 700;
    display: inline-flex; align-items: center; justify-content: center;
}
.ci-checklist li strong {
    display: block;
    font-weight: 600; font-size: 12px;
    color: var(--text);
    margin-bottom: 2px;
}
.ci-checklist li span {
    display: block;
    font-size: 11px;
    color: var(--text-muted);
    line-height: 1.5;
}
.ci-checklist li code {
    font-family: var(--bs-font-monospace, ui-monospace, SFMono-Regular, monospace);
    font-size: .82em;
    padding: 0 .25em;
    border-radius: 4px;
    background: color-mix(in srgb, var(--text) 6%, transparent);
}
.ci-step-link {
    display: inline-flex; align-items: center; gap: .25rem;
    margin-top: .5rem;
    padding: 4px 10px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    font-size: 11px; font-weight: 500;
    text-decoration: none;
    transition: border-color .15s ease;
}
.ci-step-link:hover { border-color: color-mix(in srgb, var(--text) 25%, transparent); color: var(--text); }
.ci-step-link-primary {
    background: var(--primary-bg);
    border-color: color-mix(in srgb, var(--primary) 30%, var(--border));
    color: var(--primary);
    font-weight: 600;
}

.ci-faq {
    margin-top: .35rem;
    padding-top: .65rem;
    border-top: 1px solid var(--border);
}
.ci-faq + .ci-faq { padding-top: .35rem; border-top: 0; }
.ci-faq summary {
    cursor: pointer;
    list-style: none;
    font-size: 11px; font-weight: 500;
    color: var(--text);
    padding: .35rem 0;
}
.ci-faq summary::-webkit-details-marker { display: none; }
.ci-faq summary::before { content: "▸ "; color: var(--text-muted); transition: transform .15s ease; display: inline-block; }
.ci-faq[open] summary::before { content: "▾ "; }
.ci-faq p {
    margin: .25rem 0 .35rem;
    font-size: 11px;
    color: var(--text-muted);
    line-height: 1.6;
}
.ci-faq strong { color: var(--text); }

/* Read-only intro: shown at the top of the detail page when the
   workspace has been converted to read-only. Calmer styling than
   the warn/danger sections - this is informational, not a problem. */
.ws-detail-readonly-note p {
    margin: 0 0 .5rem;
    font-size: 13px;
    color: var(--text-muted);
    line-height: 1.55;
}
.ws-detail-readonly-note p:last-child { margin-bottom: 0; }

/* Connect-to-live-mailbox panel for read-only workspaces. Inline
   form, owner-only. Visually neutral so the user reads it as
   "you can do this" rather than "you must do this". */
.ws-detail-reconnect-lede {
    margin: 0 0 .9rem;
    font-size: 13px;
    color: var(--text-muted);
    line-height: 1.55;
}
.ws-detail-reconnect-form {
    display: grid;
    grid-template-columns: 1fr 1fr auto;
    gap: .75rem .9rem;
    align-items: end;
}
.ws-detail-reconnect-field {
    display: flex;
    flex-direction: column;
    gap: .3rem;
    min-width: 0;
}
.ws-detail-reconnect-field span {
    font-size: 11px;
    font-weight: 600;
    color: var(--text);
    letter-spacing: .01em;
}
.ws-detail-reconnect-field input {
    width: 100%;
    padding: .55rem .7rem;
    font-size: 13px;
    background: var(--surface);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 8px;
}
.ws-detail-reconnect-field input:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent);
}
.ws-detail-reconnect-error {
    margin: 0 0 .75rem;
    padding: .55rem .75rem;
    border-radius: 8px;
    background: color-mix(in srgb, var(--danger) 8%, var(--surface));
    border: 1px solid color-mix(in srgb, var(--danger) 35%, var(--border));
    color: var(--text);
    font-size: 12px;
}
.ws-detail-reconnect-help {
    margin: .8rem 0 0;
    font-size: 11px;
    color: var(--text-muted);
}
@media (max-width: 720px) {
    .ws-detail-reconnect-form { grid-template-columns: 1fr; }
}

/* Type-to-confirm gate inside the Remove modal. The submit
   button is disabled until the typed value matches the
   workspace email exactly; the gate is the last guard before
   an irreversible local-archive deletion. */
.ws-remove-confirm {
    margin-top: 1rem;
    padding: .75rem .9rem;
    background: color-mix(in srgb, #b91c1c 5%, var(--surface));
    border: 1px solid color-mix(in srgb, #b91c1c 25%, var(--border));
    border-radius: 8px;
}
.ws-remove-confirm .form-label {
    font-size: 12px;
    color: var(--text);
    margin-bottom: .35rem;
}
.ws-remove-confirm .form-label strong {
    color: #b91c1c;
    font-weight: 700;
    user-select: all;
}
.ws-remove-confirm .form-control {
    font-family: var(--bs-font-monospace, ui-monospace, SFMono-Regular, monospace);
    font-size: 12px;
}
.ws-remove-confirm .form-control.is-valid {
    border-color: #16a34a;
    background-image: none;
}
.ws-remove-confirm .form-control.is-invalid {
    border-color: #b91c1c;
    background-image: none;
}
.ws-remove-confirm .form-text {
    font-size: 10px;
    margin-top: .35rem;
}

/* Mobile adjustments. */
@media (max-width: 720px) {
    .ws-detail-section { padding: 1rem; }
    /* Group cards zero the shell padding on every width; only the
       head/body paddings shrink on small screens. */
    .ws-cat { padding: 0; }
    .ws-cat > .ws-cat-head { padding: 10px 14px 2px; }
    .ws-cat-body { padding: 4px 14px 10px; }
    .ws-chapter:not(:has(> section ~ section)) > .ws-cat > .ws-cat-head,
    .ws-chapter-body:not(:has(> section ~ section)) > .ws-cat > .ws-cat-head { padding: 6px 2px 0; }
}

/* ── Global feedback widget ───────────────────────────────────
   Floating launcher button + modal that lets any authenticated
   user submit a problem report or feature request from any page.
   Submissions land in master.db's FeedbackReports table; the
   super-admin reads them on /Admin/Feedback. */
.feedback-widget-launcher {
    position: fixed;
    right: 1rem;
    bottom: 1rem;
    z-index: 1080;
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    padding: .5rem .9rem;
    background: var(--text);
    color: var(--surface, #fff);
    border: 1px solid var(--primary);
    border-radius: 999px;
    box-shadow: 0 4px 12px rgba(0,0,0,.15);
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
    transition: transform .12s, box-shadow .12s, background .12s;
}
.feedback-widget-launcher:hover {
    transform: translateY(-1px);
    box-shadow: 0 6px 16px rgba(0,0,0,.18);
    background: color-mix(in srgb, var(--text) 5%, transparent);
}
.feedback-widget-modal {
    position: fixed;
    inset: 0;
    z-index: 1090;
    display: flex;
    align-items: center;
    justify-content: center;
}
.feedback-widget-modal[hidden] { display: none; }
.feedback-widget-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(0,0,0,.45);
    backdrop-filter: blur(2px);
}
.feedback-widget-panel {
    position: relative;
    background: var(--surface, #fff);
    color: var(--text, #0f172a);
    border-radius: 10px;
    box-shadow: 0 20px 50px rgba(0,0,0,.25);
    width: min(640px, 95vw);
    height: min(720px, 90vh);
    min-width: 420px;
    min-height: 360px;
    max-width: 95vw;
    max-height: 95vh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    resize: both;
}
.feedback-widget-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
    padding: .9rem 1.1rem;
    background: linear-gradient(135deg,
        color-mix(in srgb, var(--primary) 8%, var(--surface, #fff)),
        var(--surface, #fff));
    border-bottom: 1px solid var(--border);
    border-top-left-radius: 10px;
    border-top-right-radius: 10px;
}
.feedback-widget-head-title {
    display: flex;
    align-items: center;
    gap: .65rem;
    min-width: 0;
}
.feedback-widget-head-icon {
    flex: 0 0 32px;
    width: 32px;
    height: 32px;
    border-radius: 8px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--text);
    color: var(--surface, #fff);
    box-shadow: 0 4px 10px rgba(0, 0, 0, .15);
}
.feedback-widget-head h3 {
    margin: 0;
    font-size: 15px;
    font-weight: 600;
    line-height: 1.2;
    color: var(--text);
}
.feedback-widget-head-sub {
    margin: 0;
    font-size: 11px;
    color: var(--text-muted);
    line-height: 1.3;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.feedback-widget-x {
    flex: 0 0 auto;
    background: transparent;
    border: 1px solid transparent;
    width: 32px;
    height: 32px;
    border-radius: 6px;
    line-height: 1;
    cursor: pointer;
    color: var(--text-muted);
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: background .12s, color .12s, border-color .12s;
}
.feedback-widget-x:hover {
    color: var(--text);
    background: var(--surface-2, #f1f5f9);
    border-color: var(--border);
}
.feedback-widget-panel form {
    display: flex;
    flex-direction: column;
    gap: .75rem;
    padding: 1rem;
    overflow-y: auto;
    flex: 1 1 auto;
    min-height: 0;
}
.feedback-widget-panel textarea[name="body"] {
    flex: 1 1 auto;
    min-height: 7rem;
}
.feedback-kind-row {
    display: flex;
    gap: .4rem;
    flex-wrap: wrap;
    align-items: center;
}
.feedback-kind {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    padding: .35rem .7rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    cursor: pointer;
    user-select: none;
    font-size: 12px;
    line-height: 1.2;
    background: var(--surface, #fff);
    transition: border-color .12s, background .12s, color .12s;
}
.feedback-kind input[type="radio"] {
    /* Hide the OS radio - the pill is the indicator */
    appearance: none;
    -webkit-appearance: none;
    width: 0;
    height: 0;
    margin: 0;
    padding: 0;
}
.feedback-kind:hover {
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
    color: var(--text);
}
.feedback-kind:has(input:checked) {
    border-color: var(--primary);
    color: var(--primary);
    background: var(--primary-bg, color-mix(in srgb, var(--primary) 12%, transparent));
}
.feedback-widget-panel textarea {
    width: 100%;
    padding: .6rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    font: inherit;
    color: inherit;
    background: var(--surface);
    resize: none;
}
.feedback-widget-panel textarea:focus {
    outline: 2px solid var(--primary);
    outline-offset: -2px;
    border-color: var(--primary);
}
.feedback-attach-row {
    display: flex;
    gap: .5rem;
    flex-wrap: wrap;
}
.feedback-attach-btn {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    padding: .35rem .65rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: transparent;
    cursor: pointer;
    font-size: .85em;
    color: var(--text);
}
.feedback-attach-btn:hover {
    background: var(--surface-2, #f8fafc);
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
    color: var(--text);
}
.feedback-attach-btn[disabled] { opacity: .5; cursor: progress; }
.feedback-attach-list {
    display: flex;
    flex-wrap: wrap;
    gap: .5rem;
    min-height: 0;
}
.feedback-attach-list:empty { display: none; }
.feedback-attach-chip {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    padding: .25rem .55rem;
    background: var(--surface-2, #f1f5f9);
    border: 1px solid var(--border);
    border-radius: 999px;
    font-size: .8em;
    max-width: 100%;
}
.feedback-attach-chip > span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.feedback-attach-chip button {
    border: none;
    background: none;
    cursor: pointer;
    color: var(--text-muted);
    line-height: 1;
    font-size: 1.1em;
    padding: 0 .15rem;
}
.feedback-attach-chip button:hover { color: var(--danger, #dc2626); }
.feedback-attach-thumb {
    position: relative;
    display: inline-block;
    border: 1px solid var(--border);
    border-radius: 6px;
    overflow: hidden;
    background: var(--surface-2, #f1f5f9);
    line-height: 0;
}
.feedback-attach-thumb img {
    display: block;
    width: 84px;
    height: 84px;
    object-fit: cover;
    cursor: zoom-in;
}
.feedback-attach-thumb-x {
    position: absolute;
    top: 2px;
    right: 2px;
    width: 18px;
    height: 18px;
    border: none;
    border-radius: 50%;
    background: rgba(0,0,0,.6);
    color: #fff;
    font-size: 12px;
    line-height: 1;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
}
.feedback-attach-thumb-x:hover { background: var(--danger, #dc2626); }
.feedback-shot-modal {
    position: fixed;
    inset: 0;
    z-index: 1095; /* above the feedback panel (1090) */
    display: flex;
    align-items: center;
    justify-content: center;
}
.feedback-shot-modal[hidden] { display: none; }
.feedback-shot-panel {
    position: relative;
    background: var(--surface, #fff);
    color: var(--text, #0f172a);
    border-radius: 10px;
    box-shadow: 0 24px 60px rgba(0,0,0,.35);
    max-width: 95vw;
    max-height: 95vh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}
.feedback-shot-body {
    flex: 0 0 auto;
    padding: .85rem;
    background: var(--surface-2, #f8fafc);
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
}
.feedback-shot-img-wrap {
    /* Width follows the captured aspect ratio (set as a CSS var by
       the JS at capture time). The wrap uses `aspect-ratio` so the
       panel hugs the image instead of stretching to a fixed box. */
    --feedback-shot-aspect: 16 / 10;
    background: repeating-conic-gradient(#e5e7eb 0 25%, #f8fafc 0 50%) 50% / 18px 18px;
    border: 1px solid var(--border);
    border-radius: 6px;
    aspect-ratio: var(--feedback-shot-aspect);
    /* Cap the rendered size so a tall page doesn't blow past the
       viewport and a wide page stays under the panel max-width.
       Width: viewport minus padding/borders/scrollbar slack.
       Height: viewport minus header (~58px) + footer (~58px) + body
       padding (1.7rem) + extra slack for the dialog chrome. */
    width:  min(calc(95vw - 2rem),
                calc((95vh - 9rem) * var(--feedback-shot-aspect)));
    max-width: 100%;
    height: auto;
    overflow: hidden;
    display: flex;
    align-items: stretch;
    justify-content: center;
    padding: 4px;
}
.feedback-shot-img-wrap img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
    border-radius: 4px;
    cursor: zoom-in;
}
.feedback-shot-fullscreen {
    position: fixed;
    inset: 0;
    z-index: 1100;
    background: rgba(0,0,0,.85);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 2rem;
    cursor: zoom-out;
}
.feedback-shot-fullscreen[hidden] { display: none; }
.feedback-shot-fullscreen img {
    max-width: 100%;
    max-height: 100%;
    box-shadow: 0 10px 40px rgba(0,0,0,.5);
    border-radius: 4px;
}
.feedback-widget-foot {
    display: flex;
    align-items: center;
    gap: .5rem;
    margin-top: .25rem;
}
.feedback-shot-panel > .feedback-widget-foot {
    margin-top: 0;
    padding: .75rem 1.1rem;
    border-top: 1px solid var(--border);
    background: var(--surface, #fff);
}
.feedback-status {
    flex: 1 1 auto;
    font-size: .85em;
    color: var(--text-muted);
}
.feedback-kind-praise {
    background: linear-gradient(120deg,
        color-mix(in srgb, #f59e0b 12%, transparent),
        color-mix(in srgb, #ec4899 12%, transparent),
        color-mix(in srgb, #6366f1 12%, transparent));
    border-style: dashed;
}
.feedback-kind-praise:has(input:checked) {
    border-style: solid;
    border-color: #ec4899;
    background: linear-gradient(120deg,
        color-mix(in srgb, #f59e0b 25%, transparent),
        color-mix(in srgb, #ec4899 25%, transparent),
        color-mix(in srgb, #6366f1 25%, transparent));
}
.feedback-kind-sparkle {
    display: inline-block;
    transform-origin: center;
}
.feedback-kind-praise:has(input:checked) .feedback-kind-sparkle {
    animation: feedback-sparkle 1.4s ease-in-out infinite;
}
@keyframes feedback-sparkle {
    0%, 100% { transform: rotate(-12deg) scale(1); }
    50%      { transform: rotate(12deg)  scale(1.25); }
}
.feedback-praise {
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 1rem;
    background:
        radial-gradient(circle at 0% 0%,   color-mix(in srgb, #f59e0b 14%, transparent), transparent 60%),
        radial-gradient(circle at 100% 0%, color-mix(in srgb, #ec4899 14%, transparent), transparent 60%),
        radial-gradient(circle at 50% 100%,color-mix(in srgb, #6366f1 14%, transparent), transparent 60%),
        var(--surface, #fff);
    display: flex;
    flex-direction: column;
    gap: .85rem;
    flex: 1 1 auto;
    min-height: 0;
    justify-content: center;
}
.feedback-praise[hidden] { display: none; }
.feedback-praise-prompt {
    text-align: center;
    font-size: 14px;
    color: var(--text);
    display: flex;
    align-items: center;
    justify-content: center;
    gap: .4rem;
}
.feedback-praise-emoji {
    display: inline-block;
    font-size: 1.4rem;
    animation: feedback-praise-bounce 1.6s ease-in-out infinite;
}
@keyframes feedback-praise-bounce {
    0%, 100% { transform: translateY(0) rotate(-3deg); }
    50%      { transform: translateY(-4px) rotate(3deg); }
}
.feedback-praise-options {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: .55rem;
}
.feedback-praise-opt {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: .35rem;
    padding: .85rem .5rem;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--surface, #fff);
    color: var(--text);
    cursor: pointer;
    font-size: 13px;
    font-weight: 500;
    transition: transform .15s, box-shadow .15s, border-color .15s, background .15s;
}
.feedback-praise-opt:hover {
    transform: translateY(-2px);
    box-shadow: 0 6px 14px rgba(0,0,0,.08);
    border-color: #ec4899;
}
.feedback-praise-opt-emoji {
    font-size: 1.6rem;
    transition: transform .25s;
}
.feedback-praise-opt:hover .feedback-praise-opt-emoji {
    transform: scale(1.2) rotate(-6deg);
}
.feedback-praise-opt.is-selected {
    border-color: transparent;
    color: #fff;
    background: linear-gradient(120deg, #f59e0b, #ec4899, #6366f1);
    background-size: 200% 200%;
    animation: feedback-praise-shimmer 3s ease-in-out infinite;
    box-shadow: 0 6px 20px rgba(236,72,153,.35);
}
.feedback-praise-opt.is-selected .feedback-praise-opt-emoji {
    animation: feedback-praise-bounce 1.2s ease-in-out infinite;
}
@keyframes feedback-praise-shimmer {
    0%, 100% { background-position:   0% 50%; }
    50%      { background-position: 100% 50%; }
}

/* Signature editor inside the Preferences panel. Three stacked
   pieces - toolbar / contenteditable body / footer with save +
   status. The body is left contenteditable so the user gets the
   browser's native rich-text editing (selection, IME, paste,
   image drop) without us shipping a wysiwyg engine. The Save
   button POSTs to /Users/Signature; live status text reflects
   the result. */
.signature-editor {
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--surface, #fff);
    overflow: hidden;
}
.signature-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .15rem;
    padding: .35rem .4rem;
    background: var(--surface-2, #f8fafc);
    border-bottom: 1px solid var(--border);
}
.signature-toolbar button {
    width: 28px;
    height: 28px;
    border: 1px solid transparent;
    background: transparent;
    border-radius: 6px;
    color: var(--text);
    font-size: 12px;
    line-height: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    padding: 0;
}
.signature-toolbar button:hover {
    background: var(--surface, #fff);
    border-color: var(--border);
    color: var(--text);
}
.signature-toolbar button.is-active {
    background: color-mix(in srgb, var(--text) 7%, transparent);
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
    color: var(--text);
}
.signature-toolbar-sep {
    width: 1px;
    height: 18px;
    margin: 0 .25rem;
    background: var(--border);
}
.signature-body {
    min-height: 7rem;
    max-height: 18rem;
    overflow-y: auto;
    padding: .7rem .85rem;
    font: inherit;
    color: inherit;
    background: var(--surface, #fff);
    line-height: 1.45;
    cursor: text;
}
.signature-body:focus {
    outline: 2px solid var(--primary);
    outline-offset: -2px;
}
.signature-body:empty::before {
    content: attr(data-placeholder);
    color: var(--text-muted);
    white-space: pre-line;
    pointer-events: none;
}
.signature-body img {
    max-width: 100%;
    height: auto;
    border-radius: 4px;
    vertical-align: middle;
}
.signature-body a {
    color: var(--primary);
    text-decoration: underline;
}
/* The editor draws the spacing the mail is sent with (MailSignatureLines,
   issue #594): the signature's ONE spacing setting, single (Outlook's
   look, the default), relaxed or spaced. A margin or line height the
   author set on a line wins, as it does at send; a blank line stays one
   blank line. The compose preview reads the same choice from the
   signature's wrapper. What is typed is what the recipient sees. */
.signature-body p,
.compose-signature-preview .bf-sig-lines-relaxed p,
.compose-signature-preview .bf-sig-lines-spaced p {
    margin: 0;
}
.signature-body[data-lines="relaxed"],
.signature-body[data-lines="relaxed"] :is(p, div),
.compose-signature-preview .bf-sig-lines-relaxed,
.compose-signature-preview .bf-sig-lines-relaxed :is(p, div) {
    line-height: 1.5;
}
.signature-body[data-lines="spaced"] :is(p, div:not(:has(> :is(p, div, table, ul, ol, blockquote)))),
.compose-signature-preview .bf-sig-lines-spaced :is(p, div:not(:has(> :is(p, div, table, ul, ol, blockquote)))) {
    margin: 0 0 10px 0;
}
.signature-body[data-lines="spaced"] :is(p, div):has(> br:only-child),
.compose-signature-preview .bf-sig-lines-spaced :is(p, div):has(> br:only-child) {
    margin: 0;
}
/* The spacing choice: three drawn glyphs in one group, the chosen one
   wearing the toolbar's active wash; 6px keeps them apart. */
.signature-lines {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
.signature-foot {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .55rem .65rem;
    background: var(--surface-2, #f8fafc);
    border-top: 1px solid var(--border);
}
.signature-status {
    flex: 1 1 auto;
    font-size: .8em;
    color: var(--text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.signature-status.is-error  { color: var(--danger, #b91c1c); }
.signature-status.is-saved  { color: var(--success, #059669); }

/* Non-editable signature preview under the compose contenteditable,
   the body of the composer's FOLDED signature unit (its toggle wears
   the word and the hint from the catalogs). Muted, read-only; the
   actual stitch happens server-side - this is purely a preview. */
.compose-signature-preview {
    position: relative;
    margin-top: .35rem;
    padding: .35rem .85rem .35rem 1.7rem;
    color: var(--text-muted);
    font-size: 12px;
    line-height: 1.45;
    background: transparent;
    cursor: default;
    user-select: text;
}
.compose-signature-preview[hidden] { display: none; }
.compose-signature-preview img {
    max-width: 100%;
    height: auto;
    border-radius: 4px;
    vertical-align: middle;
}
.compose-signature-preview a {
    color: var(--primary);
    text-decoration: underline;
    pointer-events: none;     /* preview is read-only */
}
.compose-signature-preview p,
.compose-signature-preview div {
    margin: 0;
}

/* ── Issues workspace ─────────────────────────────────────────────── */
/* standard-shape layout for the issues surface. List page uses an
   edge-to-edge container (with comfortable side gutters) so wide
   monitors get the breathing room they paid for. Compose + detail
   stay narrower (~1080px) because long-form prose reads better with
   a constrained measure.
   Palette mirrors Burofolk primer-light tokens for a familiar feel:
   page bg #f6f8fa, card #ffffff, border #d0d7de, accent green/purple
   for issue states. We keep everything tied back to var(--text) /
   var(--text-muted) so dark mode still works. */

:root {
    --gh-fg-default: #1f2328;
    --gh-fg-muted: #59636e;
    --gh-fg-subtle: #6e7781;
    --gh-canvas: #ffffff;
    --gh-canvas-default: var(--gh-canvas);
    --gh-canvas-subtle: #f6f8fa;
    --gh-canvas-inset: #f6f8fa;
    --gh-border: #d0d7de;
    --gh-border-default: var(--gh-border);
    --gh-border-muted: #d8dee4;
    --gh-accent-fg: #0969da;
    --gh-accent-emphasis: #0969da;
    --gh-accent-subtle: #ddf4ff;
    /* Brand-color rebind: the GitHub-issues palette ships with
       green for "open / success / primary action" everywhere
       (state badge, New issue button, vote arrows). Burofolk is
       not a green brand - the wordmark + primary CTA are indigo
       (--primary). Re-pointing these two tokens is the single
       lever that swaps every green accent in the issues workspace
       to the brand color in lockstep, keeping the page styled
       consistently with the rest of the app. */
    --gh-success-fg: var(--primary);
    --gh-success-emphasis: var(--primary);
    --gh-done-fg: var(--text-muted);
    --gh-attention-fg: #9a6700;
    --gh-danger-fg: #cf222e;
    --gh-attention-emphasis: #bf8700;
    --gh-danger-emphasis: #cf222e;
    --success-fg: #1a7f37;
    --gh-btn-bg: #f6f8fa;
    --gh-btn-hover: #f3f4f6;
    --gh-btn-border: rgba(31, 35, 40, 0.15);
    --gh-shadow: 0 1px 0 rgba(31, 35, 40, 0.04);
}
/* Dark palette keys EXCLUSIVELY on html[data-theme="dark"] - the
   attribute the in-app theme toggle stamps (the head script maps
   the OS preference onto the same attribute when no explicit
   choice is saved). Never key these tokens on
   @media (prefers-color-scheme): that query follows the BROWSER
   setting and overrides an explicit in-app Light choice, shipping
   mixed light/dark surfaces. Pinned by
   ThemePaintHonorsInAppThemeTests. */
html[data-theme="dark"] {
    --gh-fg-default: #e6edf3;
    --gh-fg-muted: #9198a1;
    --gh-fg-subtle: #6e7681;
    --gh-canvas: #0d1117;
    --gh-canvas-default: var(--gh-canvas);
    --gh-canvas-subtle: #151b23;
    --gh-canvas-inset: #010409;
    --gh-border: #3d444d;
    --gh-border-default: var(--gh-border);
    --gh-border-muted: #3d444d;
    --gh-accent-fg: #4493f8;
    --gh-accent-emphasis: #1f6feb;
    --gh-accent-subtle: #121d2f;
    --gh-success-fg: var(--primary);
    --gh-success-emphasis: var(--primary);
    --gh-attention-emphasis: #9e6a03;
    --gh-danger-emphasis: #da3633;
    --success-fg: #3fb950;
    --gh-done-fg: var(--text-muted);
    --gh-btn-bg: #212830;
    --gh-btn-hover: #2a313c;
    --gh-btn-border: rgba(240, 246, 252, 0.10);
    --gh-shadow: 0 0 transparent;
}

.issues-page {
    /* The parent .app-main is overflow:hidden flex column at fixed
       viewport height. The page itself is a flex column that DOES
       NOT scroll - the breadcrumbs + title row stay anchored at the
       top, the right sidebar stays anchored too, and only the
       thread column scrolls. This matches the convention every
       serious issue tracker follows.

       Falls back to a normal page-level scroll on narrow viewports
       (the sidebar stacks above the thread there - see media query
       below). */
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    width: 100%;
    max-width: 1500px;
    margin: 0 auto;
    padding: 32px 32px 0;
    box-sizing: border-box;
    color: var(--gh-fg-default);
    font-feature-settings: 'liga', 'kern';
    overflow: hidden;
}
/* Document-flow variant: the issue LIST page opts out of the
   height-bounded layout so the rows push the page taller and
   the parent .app-page scrollbar takes over (GitHub-style),
   instead of an internal nested rail on the rows region.
   The detail page keeps its anchored layout via the unmodified
   .issues-page rules above. */
.issues-page.issues-page-flow {
    flex: 0 0 auto;
    min-height: 0;
    display: block;
    overflow: visible;
}
/* Issue LIST page: drop the 1500px cap inherited from
   .issues-page so the rows fill the full viewport width.
   Detail + New still cap at 1280–1500px (the prose-focused
   reading width); the list is dense data - wider is better
   so we can show updated / created columns + title +
   labels + assignees on one row without truncation. */
.issues-page.issues-list-page {
    max-width: none;
}
/* ── Issues flow pages: INTERNAL scroll between pinned chrome ─────
   `is-issues-list-flow` (issue list / milestones / labels /
   configuration) and `is-issues-detail-flow` (issue detail / new)
   turn .app-main into THE scroll container for the page.

   THE RULE (explicit, 2026-07-08): the top nav and the app footer
   are ALWAYS visible, and the scrollbar spans ONLY the scrollable
   content region between them - it ends at the footer's TOP edge.
   It must NEVER run the full screen height over the footer, which
   is exactly what a browser-edge document scrollbar does. So these
   body modes must NEVER unlock html/body document scrolling (the
   retired `height: auto; overflow: visible` flow model) - the
   locked shell stays locked and only .app-main scrolls. Pinned by
   AppFooterAlwaysVisibleTests.

   .app-main already spans the full width between nav and footer
   (body flex column) and is position:relative, so absolute
   descendants keep anchoring to the scroller and travel with the
   content. scrollbar-gutter: stable is the same zero-CLS contract
   as .app-page: short → tall page navigation must not shift the
   layout by a scrollbar's width. */
body.is-issues-list-flow .app-main,
body.is-issues-detail-flow .app-main {
    display: block;
    overflow-y: auto;
    scrollbar-gutter: stable;
}

/* ── Issue DETAIL extras in flow mode: the grid un-bounds (the
   whole page scrolls in .app-main via the shared rule above) and
   the right sidebar goes sticky inside the scroller so it stays
   visible while the thread scrolls. The internal-scroll rules on
   .issues-detail-main + the height-bounded grid below are
   nullified so the page scrolls as one. */
body.is-issues-detail-flow .issues-detail-grid {
    overflow: visible;
    flex: none;
    min-height: 0;
    align-items: start;
}
body.is-issues-detail-flow .issues-detail-main {
    overflow: visible;
    padding-right: 0;
    /* Drop the auto-hide scrollbar styling - there's no scroll
       container here anymore. Leave the bottom padding for the
       last action row's breathing room from the page footer. */
    scrollbar-width: auto;
}
body.is-issues-detail-flow .issues-detail-main::-webkit-scrollbar { display: none; }
/* The sidebar stays PINNED while the thread scrolls: position:sticky
   inside the .app-main scroller. When the panel is TALLER than the
   viewport, issues-ui.js drives its `top` so it scrolls WITH the page,
   revealing its bottom on scroll-down and its top on scroll-up, and
   pins at whichever edge the reader reaches - no inner scrollbar,
   nothing clipped. (The earlier static-flow shape was chosen only
   because a naive sticky clipped its own bottom - the watcher roster
   fell off, unreachable - and overflow-y brought back a double
   scrollbar; the smart-sticky solves both.) A panel that FITS just
   pins at the top - pure CSS, no JS. align-self:start gives it a
   full-height grid cell to travel in without stretching its own box.
   On collapse (<=960px) it goes static below the thread (media block). */
body.is-issues-detail-flow .issues-detail-sidebar {
    position: sticky;
    top: 16px;
    align-self: start;
    /* A sticky box is a stacking context of its own; with z-index auto it paints
       in markup order, and the column now PRECEDES the thread (2026-09-12) - its
       popovers went under the comment editor. One level up keeps them above. */
    z-index: 1;
}

/* GitHub-style font stack on the issue page. Body prose uses the
   system-UI stack (matches GH's app chrome); issue + comment
   markdown bodies pick this up so the rendered text reads like
   GH's issue body. Code blocks keep monospace via the existing
   .markdown-body pre/code rules elsewhere. */
.issues-detail-page,
.issues-detail-page .issues-thread-bubble-body,
.issues-detail-page .issues-thread-bubble-meta {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans",
                 Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
}
/* Shared "via MCP" origin marker (Burofolk.Services.UI.McpOriginMarker):
   the quiet bot glyph beside a comment / reply an AI client posted on the
   user's behalf. Purely informational - no link, no hover accent - it just
   sits inline with the header meta as muted metadata. */
.bf-mcp-marker {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--gh-fg-muted, var(--text-subtle));
    vertical-align: -1px;
}
.bf-mcp-marker svg { display: block; }
/* The monospace stack for code/pre lives in the canonical
   `.markdown-body` block; kbd is a non-markdown element that may
   appear in issue prose, so it keeps its own surface rule. */
.issues-detail-page .issues-thread-bubble-body kbd {
    font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
                 "Liberation Mono", monospace;
}
.issues-page.issues-page-flow {
    flex: none;
    min-height: 0;
    display: block;
    overflow: visible;
}
/* Header rows stay fixed - no shrink, no scroll. */
.issues-page > .issues-page-head,
.issues-page > .issues-detail-title-row,
.issues-page > .issues-flash,
.issues-page > .ws-new-error { flex-shrink: 0; }
.issues-page-head {
    padding-bottom: 16px;
    margin-bottom: 16px;
    border-bottom: 1px solid var(--gh-border);
    display: flex; align-items: center; justify-content: space-between;
    gap: 16px;
}
/* Shared breadcrumb (used by every page that wants the standard
   "Workspaces / <name> / <subpage>" trail). The .app-breadcrumb-*
   classes are the canonical names; .issues-page-crumbs / .issues-
   crumb-* are kept as aliases so existing pages render identically
   while the migration to the shared partial proceeds. */
.app-breadcrumb,
.issues-page-crumbs {
    font-size: 14px;
    color: var(--gh-fg-muted);
    display: flex; align-items: center; gap: 8px;
    min-width: 0;
    flex-wrap: wrap;
}
.app-breadcrumb-link,
.issues-crumb-link { color: var(--gh-accent-fg); text-decoration: none; font-weight: 400; }
.app-breadcrumb-link:hover,
.issues-crumb-link:hover { text-decoration: underline; }
.app-breadcrumb-sep,
.issues-crumb-sep { color: var(--gh-fg-subtle); }
.app-breadcrumb-current,
.issues-crumb-current {
    color: var(--gh-fg-default);
    font-weight: 600;
    font-size: 16px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* When the current (leaf) crumb is itself a link - a page opting its leaf
   into "reset to the default view" - it keeps the bold current-page look
   above; the pointer + hover underline are the only signals it's clickable. */
a.app-breadcrumb-current {
    text-decoration: none;
    cursor: pointer;
}
a.app-breadcrumb-current:hover { text-decoration: underline; }

/* Head locator group: the breadcrumb plus the affordances that act on the
   LOCATION it names (today: the page permalink button). Grouped so those
   controls sit beside the crumb they belong to instead of being flung to
   the far edge by the header's space-between - a copy-link button an inch
   from the page title reads as belonging to the title, one at the opposite
   end of the header reads as belonging to nothing. */
.app-head-locator {
    display: flex; align-items: center; gap: 8px;
    min-width: 0;
}
.app-head-locator > .app-breadcrumb { min-width: 0; }

/* Page permalink button: "copy a link to THIS page", in the header of the
   issue detail page and the mail message locator rail. Quiet action -
   hairline border, no fill, no accent tint - sized on the same 28px icon
   grid as the thread bubble's twin. Both glyphs (chain, success check)
   stack in ONE reserved 14px box and swap by opacity, so the flash cannot
   shift the header. */
/* Very subtle at rest (owner, 2026-08-15: the always-on hairline plate
   read as aggressive next to the locator): the glyph alone in subtle
   ink, full 28px hit target kept invisible, the plate appearing only
   under the pointer - the same rest-quiet grammar as the issue
   bubble's action icons (.issues-bubble-edit). */
.page-permalink-btn {
    flex: 0 0 auto;
    position: relative;
    display: inline-flex; align-items: center; justify-content: center;
    width: 28px; height: 28px;
    padding: 0;
    border: 1px solid transparent;
    border-radius: 8px;
    background: transparent;
    color: var(--text-subtle);
    cursor: pointer;
    transition: color .12s ease, border-color .12s ease, background-color .12s ease;
}
.page-permalink-btn:hover {
    color: var(--text);
    border-color: var(--border);
    background: var(--surface-2);
}
.page-permalink-btn:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}
.page-permalink-btn .bf-permalink-icon { transition: opacity .12s ease; }
.page-permalink-check {
    position: absolute;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    opacity: 0;
    transition: opacity .12s ease;
}
/* Success: the canonical .is-copied flash copy-text.js sets for ~1.4s. */
.page-permalink-btn.is-copied {
    color: var(--success);
    border-color: color-mix(in srgb, var(--success) 45%, var(--border));
}
.page-permalink-btn.is-copied .bf-permalink-icon { opacity: 0; }
.page-permalink-btn.is-copied .page-permalink-check { opacity: 1; }

/* Top toolbar - search + secondary buttons + New issue. Wraps so the
   saved-filters strip (its last child, flex-basis 100%) drops onto its own
   full-width row beneath the search + action buttons rather than wedging in
   between them. The 8px gap doubles as the row gap to that strip. */
/* (The list page's own search bar retired on 2026-09-06: the field rides
   the strip - Services/Shell/StripSearch, app-shell.css ".app-search". The
   chips, the input and the suggestions panel below keep their paint.) */
.issues-bar-pill {
    display: inline-flex; align-items: center;
    padding: 0 6px;
    height: 20px;
    font-size: 12px;
    color: var(--gh-fg-default);
    background: var(--gh-canvas);
    border: 1px solid var(--gh-border);
    border-radius: 4px;
    white-space: nowrap;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
/* The Mentions view's All | Unread well, seated among the filter pills at
   their height (the app-segments family, sized to the row). */
/* The Mentions facet's unread figure, right-aligned in its option row. */
.issues-fdrop-opt-figure { font-size: 12px; font-weight: 600; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.issues-bar-pill strong {
    color: var(--gh-accent-fg);
    font-weight: 600;
    margin-left: 2px;
}
/* Removable variant: small × button on the right of the chip,
   wired as an anchor so a click clears the corresponding filter
   without JS. */
.issues-bar-pill-removable {
    text-decoration: none;
    color: var(--gh-fg-default);
    cursor: pointer;
    transition: background-color .12s, border-color .12s;
}
.issues-bar-pill-removable:hover {
    background: var(--gh-canvas-subtle);
    border-color: color-mix(in srgb, var(--gh-danger-fg) 60%, var(--gh-border));
    color: var(--gh-danger-fg);
}
.issues-bar-pill-removable:hover .issues-bar-pill-x { color: var(--gh-danger-fg); }
.issues-bar-pill-x {
    display: inline-flex; align-items: center; justify-content: center;
    width: 14px; height: 14px;
    margin-left: 4px;
    font-size: 14px;
    font-weight: 600;
    color: var(--gh-fg-muted);
    line-height: 1;
}
.issues-bar-input {
    flex: 1 1 auto;
    border: none; outline: none;
    background: transparent;
    color: var(--gh-fg-default);
    font-size: 14px;
    height: 100%;
    min-width: 0;
}
.issues-bar-input::placeholder { color: var(--gh-fg-muted); }
.issues-bar-input:focus { outline: none; box-shadow: none; }

/* Issues filtering + saved filters live in wwwroot/css/issues-filter.css
   (own stylesheet per feedback_scale_ready_architecture). */

/* Smart-search autocomplete dropdown. Anchored under the input
   via .issues-bar-input-wrap; rendered on focus / keystroke by
   the smart-search JS in issues-ui.js. Items show the value /
   key name as primary text + a small hint as secondary text. */
.issues-bar-input-wrap {
    position: relative;
    flex: 1 1 auto;
    display: flex;
    min-width: 0;
}
.issues-search-suggest {
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    right: 0;
    z-index: 30;
    max-height: 320px;
    overflow-y: auto;
    background: var(--gh-canvas);
    border: 1px solid var(--gh-border);
    border-radius: 6px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
    padding: 4px 0;
}
.issues-search-suggest[hidden] { display: none; }
.issues-search-suggest-item {
    display: flex; align-items: center; justify-content: space-between;
    gap: 12px;
    width: 100%;
    padding: 6px 12px;
    background: transparent;
    border: none;
    text-align: left;
    cursor: pointer;
    font-size: 13px;
    color: var(--gh-fg-default);
}
.issues-search-suggest-item:hover,
.issues-search-suggest-item.is-active {
    background: var(--gh-canvas-subtle);
}
/* The "Go to #123" jump row renders as a real <a> so new-tab
   affordances work; keep it visually identical to the buttons. */
a.issues-search-suggest-item,
a.issues-search-suggest-item:hover {
    text-decoration: none;
    color: var(--gh-fg-default);
}
.issues-search-suggest-primary {
    font-weight: 500;
    color: var(--gh-fg-default);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.issues-search-suggest-secondary {
    color: var(--gh-fg-muted);
    font-size: 12px;
    white-space: nowrap;
    flex-shrink: 0;
}

/* ── Inline-edit popover on the Issues list row cells ───────
   Singleton dialog rendered above the list. Clones the visual
   language of the bulk-toolbar / detail-sidebar pickers so
   typing "what is this thing?" anywhere on the page surfaces
   the same answer: a filterable option list with a check icon
   indicating the currently-applied state. */
.issues-inline-edit-popover {
    position: absolute;
    z-index: 80;
    min-width: 280px;
    max-width: 340px;
    max-height: 360px;
    overflow: hidden;
    background: var(--gh-canvas);
    border: 1px solid var(--gh-border);
    border-radius: 8px;
    box-shadow: 0 10px 28px rgba(0, 0, 0, 0.14);
    display: flex;
    flex-direction: column;
}
.issues-inline-edit-popover[hidden] { display: none; }
.issues-inline-edit-pop-head {
    padding: 6px 12px 4px;
    color: var(--gh-fg-muted);
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    border-bottom: 1px solid var(--gh-border);
}
.issues-inline-edit-pop-search {
    display: flex; align-items: center; gap: 6px;
    padding: 8px 12px;
    border-bottom: 1px solid var(--gh-border);
    color: var(--gh-fg-muted);
}
.issues-inline-edit-pop-filter {
    flex: 1 1 auto;
    border: none; outline: none; background: transparent;
    color: var(--gh-fg-default);
    font-size: 13px;
}
.issues-inline-edit-pop-filter::placeholder { color: var(--gh-fg-muted); }
.issues-inline-edit-pop-list {
    list-style: none;
    margin: 0; padding: 4px 0;
    /* overflow-y + min-height:0 come from the shared .issues-pop-scroll
       primitive this element also carries; only layout lives here. */
    flex: 1 1 auto;
}
.issues-inline-edit-pop-opt {
    display: flex; align-items: center; gap: 8px;
    width: 100%;
    padding: 6px 12px;
    background: transparent;
    border: none;
    text-align: left;
    cursor: pointer;
    font-size: 13px;
    color: var(--gh-fg-default);
}
.issues-inline-edit-pop-opt:hover,
.issues-inline-edit-pop-opt.is-keyboard-active {
    background: var(--gh-canvas-subtle);
}
.issues-inline-edit-pop-opt .issues-inline-edit-pop-check {
    visibility: hidden;
    color: var(--gh-accent-fg);
}
.issues-inline-edit-pop-opt.is-on .issues-inline-edit-pop-check {
    visibility: visible;
}
.issues-inline-edit-pop-opt.is-on {
    background: var(--gh-canvas-subtle);
}
.issues-inline-edit-pop-name {
    flex: 1 1 auto;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.issues-inline-edit-pop-meta {
    color: var(--gh-fg-muted);
    font-size: 11px;
    margin-left: auto;
    flex-shrink: 0;
    max-width: 140px;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.issues-inline-edit-avatar {
    display: inline-flex; align-items: center; justify-content: center;
    width: 20px; height: 20px;
    border-radius: 50%;
    background: var(--gh-canvas-subtle);
    color: var(--gh-fg-default);
    font-size: 11px;
    flex-shrink: 0;
}
.issues-inline-edit-pop-empty {
    padding: 12px;
    color: var(--gh-fg-muted);
    font-size: 12px;
    text-align: center;
    margin: 0;
}

/* The discrete affordance icon at the end of an inline-
   editable cell. Two variants:
     - .is-add  -> `+` glyph for empty cells ("set a value").
     - .is-edit -> small chevron SVG for populated cells
       ("open the picker to modify").
   Both hidden at rest, revealed on cell hover / focus so the
   row stays clean when the user isn't actively touching it.
   Pointer events disabled so the cell click reaches the
   wrapper, not the affordance. */
.issues-inline-edit-affordance {
    display: inline-flex; align-items: center; justify-content: center;
    width: 18px; height: 18px;
    margin-left: 4px;
    border-radius: 4px;
    color: var(--gh-fg-muted);
    background: var(--gh-canvas-subtle);
    font-size: 14px;
    line-height: 1;
    opacity: 0;
    transition: opacity 90ms ease-in-out;
    pointer-events: none;
}
.issues-inline-edit-affordance.is-edit {
    /* Chevron variant - slightly lighter background so the
       SVG glyph reads as a marker, not a button. */
    background: transparent;
    color: var(--gh-fg-muted);
}
.issues-inline-edit-affordance.is-edit svg {
    display: block;
}
[data-inline-edit-cell]:hover .issues-inline-edit-affordance,
[data-inline-edit-cell]:focus-visible .issues-inline-edit-affordance {
    opacity: 1;
}
[data-inline-edit-cell] {
    cursor: pointer;
    border-radius: 4px;
    position: relative; /* anchor for inline-edit text input overlay */
}
[data-inline-edit-cell]:hover {
    background: var(--gh-canvas-subtle);
}

/* Inline text editor - text / number / date custom-field
   column cells. Positioned absolute on the cell itself,
   covering the rendered chip / text while the user types.
   Esc cancels, Enter / blur commits via the inline-edit JS. */
.issues-inline-edit-input {
    box-sizing: border-box;
    border: 1px solid var(--gh-accent-fg);
    border-radius: 4px;
    padding: 0 6px;
    font-size: inherit;
    font-family: inherit;
    color: var(--gh-fg-default);
    background: var(--gh-canvas);
    outline: none;
    box-shadow: 0 0 0 2px rgba(9, 105, 218, 0.18);
}
.issues-inline-edit-input::placeholder { color: var(--gh-fg-muted); }

/* Stage indicator - filled coloured disc that stands in for
   the icon-shaped Type partial. Every stage paints a SOLID
   fill (no transparent / hollow / dashed variants): a ring-
   only dot disappears against a white surface and reads as
   noise rather than as a status signal. Differentiation rides
   on hue alone (slate / amber / emerald / muted-gray) via the
   inline `--stage-color` variable. */
.issues-stage-dot {
    display: inline-block;
    width: 12px; height: 12px;
    border-radius: 50%;
    background: var(--stage-color, var(--text-subtle));
    border: 2px solid color-mix(in srgb, var(--stage-color, var(--text-subtle)) 60%, transparent);
    box-sizing: content-box;
}
.issues-list-row-meta-stage {
    display: inline-flex; align-items: center; gap: 4px;
    color: var(--gh-fg-muted);
    font-size: 12px;
    /* Two-word stage labels ("In Progress", "In Review") sit
       in a flex parent that otherwise allows children to
       shrink. Nowrap + flex:0 0 auto keep the indicator on a
       single line, preserving the row's vertical rhythm. */
    flex: 0 0 auto;
    white-space: nowrap;
}
.issues-list-row-meta-stage .issues-stage-dot {
    width: 8px; height: 8px;
    border-width: 0;
}

.issues-bar-actions {
    display: inline-flex; align-items: stretch; gap: 0;
    flex-shrink: 0;
}
/* standard-shape button group: the secondary buttons (Labels, Milestones)
   share borders so they feel like one segmented control, with the New-
   issue CTA breaking out as the primary call-to-action. */
.issues-bar-btn {
    display: inline-flex; align-items: center; gap: 6px;
    height: 32px;
    padding: 0 14px;
    font-size: 14px;
    font-weight: 500;
    line-height: 20px;
    color: var(--gh-fg-default);
    background: var(--gh-btn-bg);
    border: 1px solid var(--gh-btn-border);
    border-radius: 0;
    cursor: pointer;
    text-decoration: none;
    white-space: nowrap;
    transition: background-color .12s, border-color .12s;
}
.issues-bar-btn:hover { background: var(--gh-btn-hover); }
.issues-bar-btn:first-child { border-radius: 6px 0 0 6px; }
.issues-bar-btn:last-of-type { border-radius: 0 6px 6px 0; border-left: 0; }
.issues-bar-btn + .issues-bar-btn { border-left: 0; }
.issues-bar-btn[disabled] { opacity: .6; cursor: not-allowed; }
.issues-bar-btn-cta {
    margin-left: 8px;
    color: var(--surface, #fff);
    background: var(--text);
    border-color: rgba(31, 35, 40, 0.15);
    border-radius: 6px !important;
    border-left: 1px solid rgba(31, 35, 40, 0.15) !important;
    box-shadow: 0 1px 0 rgba(31, 35, 40, 0.10), inset 0 1px 0 rgba(255, 255, 255, 0.03);
    font-weight: 500;
}
.issues-bar-btn-cta:hover {
    background: color-mix(in srgb, var(--text) 84%, var(--surface));
    color: #ffffff;
}

/* List card */
.issues-list-card {
    /* The shell's card grammar (owner, 2026-09-06: "follow what we have
       done for the new ticket, rounded stuff"): surface, hairline, the pane
       radius and the pane lift. */
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--app-pane-radius, 10px);
    box-shadow: var(--app-pane-shadow);
    /* The card itself isn't a scroll container. Horizontal
       scroll is delegated to the `.issues-list-rows-scroll`
       child below; the column-header clip (`.issues-list-rows-head-clip`)
       sits OUTSIDE that scroller and tracks scrollLeft via JS
       so the header is page-sticky (sticky-y binds to the
       document) while the rows pan horizontally inside the
       card. This separation prevents page-chrome (title bar,
       filters, toolbar) from sliding horizontally with the
       table on narrow viewports. */
    overflow: visible;
    display: flex;
    flex-direction: column;
}
.issues-list-card-head,
.issues-list-toolbar {
    /* THE filter bar (owner, 2026-09-07: "the top deserves strong design
       improvements… no margin… pin this to the top, only the content
       scrolls"): one 32px row above the list or the board, pinned - the
       page's flex head, never part of the scroller. Two groups: the
       FILTER chips (what is in the set) at the start, the VIEW tools (how
       it reads) at the end. ONE line, always: the row never wraps; when
       the chips' words would not fit, bf-fit.js takes the bar to
       is-compact (glyph chips, the words in the tooltips) - decided in the
       parse stream, before the first paint - and below 721px
       the filters fold behind the Filters toggle (issues-responsive.css). */
    display: flex; align-items: center; justify-content: space-between;
    gap: 12px;
    min-height: 32px;
    padding: 0;
    margin: 0 0 16px;
    background: transparent;
    border: 0;
    flex-wrap: nowrap;
    flex-shrink: 0;
}
/* The state scope (Open · Closed · All) is painted ONCE as the head's
   segmented well - .app-segments in app-shell.css (owner, 2026-09-08). */

.issues-list-card-filters,
.issues-list-card-tools {
    display: flex; align-items: center; gap: 6px;
    flex-wrap: nowrap;
    min-width: 0;
}
/* The filters' box is what the fit ladder measures: it clips SIDEWAYS instead
   of wrapping, so a first frame before the measure is never a second line -
   and sideways only. Its chips open absolute cards below the bar; a box that
   clipped both axes cut every card to the bar's 32px and each chip read as a
   dead button (owner, production, 2026-09-07: "almost all buttons are not
   working"). The 4px of room keeps a chip's hairline and focus ring whole at
   the box's edges; the negative margin gives the room back to the row. */
.issues-list-card-filters { flex: 1 1 auto; overflow-x: clip; overflow-y: visible; padding: 0 4px; margin: 0 -4px; }
/* Toolbar cards hang from their chip's LEFT edge: the bar reads left to right,
   and a right-hung card under the first chips opened past the box's left edge
   and sat cut until the engine's frame-late flip. The engine still flips a card
   that would run past the box's right edge (the last chips at narrow widths). */
.issues-list-card-filters .issues-fdrop-pop { left: 0; right: auto; }
.issues-list-card-tools { flex: 0 0 auto; margin-left: auto; }
/* A measured bar is unpainted until its fit lands (bf-fit.js: the inline call
   right after the markup, or the swap that brought it in) - a frame before the
   decision shows an empty band, never chips that then fold. The delayed
   keyframe is the failsafe should the script never run. */
[data-bf-fit]:not(.is-fit) { visibility: hidden; animation: bf-fit-failsafe 0s 1.5s forwards; }
/* A unit whose geometry follows a bar's decision (the /Work row's marks strip:
   the progress mark sits where the facts mark's fold leaves it) hides with the
   bar - a hidden bar still has its footprint, and siblings painted around it
   would move when the decision lands (the tripwire's span.wq-marks at 900). */
[data-bf-fit-unit]:has([data-bf-fit]:not(.is-fit)) { visibility: hidden; animation: bf-fit-failsafe 0s 1.5s forwards; }
@keyframes bf-fit-failsafe { to { visibility: visible; } }
/* The chips: hairline-bordered quiet controls (the charter's quiet action),
   glyph · word · caret, 30px, the surface under them. */
.issues-list-toolbar .issues-fdrop > summary,
.issues-fdrop-clear {
    display: inline-flex; align-items: center; gap: 6px;
    height: 30px;
    padding: 0 9px 0 8px;
    font-size: 13px;
    font-weight: 500;
    line-height: 1;
    color: var(--text-muted);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
    white-space: nowrap;
    text-decoration: none;
    cursor: pointer;
    transition: background .12s, color .12s, border-color .12s;
}
.issues-list-toolbar .issues-fdrop > summary:hover,
.issues-fdrop-clear:hover {
    color: var(--text);
    border-color: var(--border-strong, color-mix(in srgb, var(--text) 24%, transparent));
    text-decoration: none;
}
.issues-list-toolbar .issues-fdrop[open] > summary {
    color: var(--text);
    background: color-mix(in srgb, var(--text) 5%, var(--surface));
    border-color: var(--border-strong, color-mix(in srgb, var(--text) 24%, transparent));
}
.issues-fdrop-glyph { width: 14px; height: 14px; flex: 0 0 auto; color: var(--text-subtle); transition: color .12s; }
.issues-fdrop-chev { width: 11px; height: 11px; flex: 0 0 auto; color: var(--text-subtle); }
.issues-list-toolbar .issues-fdrop > summary:hover .issues-fdrop-glyph,
.issues-list-toolbar .issues-fdrop[open] > summary .issues-fdrop-glyph,
.issues-fdrop-clear:hover .issues-fdrop-glyph { color: var(--text); }
/* An ENGAGED filter reads from the chip itself - ink word, ink hairline,
   ink glyph - never a filled pill; the count tag stays a hidden hook. */
.issues-list-toolbar .issues-fdrop > summary:has(.issues-fdrop-active-tag) {
    color: var(--text);
    font-weight: 600;
    border-color: var(--text);
}
.issues-list-toolbar .issues-fdrop > summary:has(.issues-fdrop-active-tag) .issues-fdrop-glyph { color: var(--text); }
.issues-list-toolbar .issues-fdrop-active-tag { display: none; }
.issues-fdrop-clear { color: var(--text); }
/* The compact regime: glyph squares, the words wait in the tooltips - an
   ENGAGED filter keeps its word (the viewer must read what narrows the
   set) until even that overflows (is-tight). */
.issues-list-toolbar.is-compact .issues-fdrop > summary,
.issues-list-toolbar.is-compact .issues-fdrop-clear { padding: 0 7px; gap: 4px; }
.issues-list-toolbar.is-compact .issues-fdrop-word { display: none; }
.issues-list-toolbar.is-compact:not(.is-tight) .issues-fdrop > summary:has(.issues-fdrop-active-tag) { padding: 0 9px 0 8px; gap: 6px; }
.issues-list-toolbar.is-compact:not(.is-tight) .issues-fdrop > summary:has(.issues-fdrop-active-tag) .issues-fdrop-word { display: inline; }
.issues-fdrop {
    position: relative;
    display: inline-block;
}
.issues-fdrop summary {
    list-style: none;
    cursor: pointer;
    display: inline-flex; align-items: center; gap: 4px;
    padding: 5px 10px;
    font-size: 14px;
    font-weight: 500;
    color: var(--gh-fg-muted);
    background: transparent;
    border: 1px solid transparent;
    border-radius: 6px;
    transition: background .12s, color .12s, border-color .12s;
}
.issues-fdrop summary::-webkit-details-marker { display: none; }
.issues-fdrop summary:hover { color: var(--gh-fg-default); }
.issues-fdrop[open] summary {
    background: var(--gh-canvas);
    color: var(--gh-fg-default);
    border-color: var(--gh-border);
}
.issues-fdrop-pop {
    position: absolute;
    top: calc(100% + 6px);
    right: 0;
    z-index: 10;
    min-width: 240px;
    background: var(--gh-canvas);
    /* Two-layer elevation: a hairline ring keeps the card edge legible
       on light page backgrounds (where a pure shadow alone reads as
       "floating without anchor"), and a deeper soft shadow lifts the
       popover off the surface. Combined, the popover always reads as
       a discrete card no matter where it lands. */
    border: 1px solid var(--gh-border);
    border-radius: 10px;
    box-shadow:
        0 0 0 1px rgba(31, 35, 40, .04),
        0 12px 28px -8px rgba(31, 35, 40, .22),
        0 4px 10px -4px rgba(31, 35, 40, .14);
    padding: 4px 0;
    /* Auto-scroll if the list ever gets very tall - some pickers
       (label list) can have many entries. Default behavior shows
       every option without clipping. */
    overflow-y: auto;
    overflow-x: hidden;
    max-height: 60vh;
}
.issues-fdrop-opt {
    display: block;
    padding: 8px 14px;
    font-size: 14px;
    color: var(--gh-fg-default);
    text-decoration: none;
    border-top: 1px solid transparent;
}
.issues-fdrop-opt:hover { background: var(--gh-canvas-subtle); }
.issues-fdrop-opt.is-active {
    background: var(--gh-accent-subtle);
    color: var(--gh-fg-default);
    font-weight: 500;
}
.issues-fdrop-opt-row {
    display: flex; align-items: center; justify-content: space-between;
    gap: 8px;
}

/* Assignees filter pop-out (GH-style). Wider than the default
   so user names + handles fit on one line; rows show a small
   24x24 avatar + the resolved display name. The "No assignees"
   row uses a generic person silhouette in a muted circle so
   the empty-state still looks like an option, not a glitch. */
.issues-fdrop-pop-assignees { min-width: 280px; max-width: 340px; }
.issues-fdrop-pop-assignees .issues-fdrop-opt-row {
    justify-content: flex-start;
    padding: 6px 10px;
}
.issues-fdrop-assignee-avatar {
    flex-shrink: 0;
    width: 22px; height: 22px;
    border-radius: 50%;
}
.issues-fdrop-assignee-avatar-empty {
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--surface-2);
    color: var(--text-muted);
    border: 1px solid var(--border);
}
.issues-fdrop-assignee-text {
    flex: 1; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    color: var(--text);
}
.issues-fdrop-meta {
    font-size: 12px;
    color: var(--gh-fg-muted);
    font-variant-numeric: tabular-nums;
}
.issues-fdrop-active-tag {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 18px; height: 18px;
    padding: 0 4px;
    margin-left: 4px;
    font-size: 11px;
    font-weight: 600;
    color: #ffffff;
    background: var(--gh-accent-emphasis);
    border-radius: 999px;
    line-height: 1;
}
.issues-fdrop-empty {
    display: block;
    padding: 10px 14px;
    color: var(--gh-fg-muted);
    font-size: 13px;
    font-style: italic;
}

/* GitHub-style "Filter by issue type" dropdown.
   Shape:
     - sticky header label at the top
     - one row per type with: icon · name + description (two lines)
     - hover row gets the canvas-subtle background
     - active row gets a left-edge accent stripe
     - thin border between rows so the list reads as a grouped menu
   Mirrors the standard tracker's affordance so users from there
   recognise it instantly. */
.issues-fdrop-pop-types {
    min-width: 280px;
    padding: 0;
}
.issues-fdrop-head {
    padding: 10px 14px 8px;
    font-size: 12px;
    font-weight: 600;
    color: var(--gh-fg-default);
    border-bottom: 1px solid var(--gh-border-muted, var(--gh-border));
    background: var(--gh-canvas);
    /* Sticky so the header label stays visible if the menu ever
       grows tall enough to scroll. */
    position: sticky;
    top: 0;
    z-index: 1;
}
.issues-fdrop-type-opt {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: 8px 14px 8px 12px;
    color: var(--gh-fg-default);
    text-decoration: none;
    border-left: 2px solid transparent;
    border-bottom: 1px solid var(--gh-border-muted, var(--gh-border));
    transition: background-color .1s, border-left-color .1s;
}
.issues-fdrop-type-opt:last-child {
    border-bottom: 0;
}
.issues-fdrop-type-opt:hover {
    background: var(--gh-canvas-subtle);
    color: var(--gh-fg-default);
    text-decoration: none;
}
.issues-fdrop-type-opt.is-active {
    background: var(--gh-canvas-subtle);
    border-left-color: var(--gh-accent-emphasis, #0969da);
}
.issues-fdrop-type-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 22px;
    height: 22px;
    margin-top: 1px;
    /* The _TypeIcon partial renders a small inline SVG with its own
       color; keep the wrapper transparent so the icon's own palette
       (red bug / blue feature / amber task) reads at a glance. */
}
.issues-fdrop-type-icon svg {
    width: 18px;
    height: 18px;
    display: block;
}
.issues-fdrop-type-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
}
.issues-fdrop-type-text strong {
    font-size: 13px;
    font-weight: 600;
    line-height: 1.3;
    color: var(--gh-fg-default);
}
.issues-fdrop-type-desc {
    font-size: 12px;
    color: var(--gh-fg-muted);
    line-height: 1.35;
    margin-top: 1px;
}

/* Rows */
.issues-list-rows {
    list-style: none; padding: 0; margin: 0;
    /* No internal scroll. The page (document) handles scrolling
       like GitHub, so the rail-on-the-right artefact is gone
       and the list flows naturally with the rest of the page. */
}
/* Issue list row.
   Only the title link is clickable / hoverable. The row itself
   has no cursor, no hover background, and no overlay. Avatar
   pips, label chips, comment-count link and author chip keep
   their own click destinations - none of them is hijacked by a
   row-wide overlay anymore. */
.issues-list-row {
    position: relative;
    /* Real grid: every row + the header share the same
       --issues-grid-cols template (set inline on the
       .issues-list-card wrapper from the Razor handler).
       Cells are direct children; their order in the DOM
       maps to grid columns by source order. Vertical
       gridlines come from each cell's border-right,
       horizontal gridlines from the row's border-bottom.
       Translucent black so hairlines feel like glass edges
       and adapt to dark mode via the override below. */
    display: grid;
    grid-template-columns: var(--issues-grid-cols);
    align-items: stretch;
    border-bottom: 1px solid rgba(0, 0, 0, .08);
    transition: background 0.08s ease;
    min-height: 40px;
    font-size: 13px;
}
html[data-theme="dark"] .issues-list-row { border-bottom-color: rgba(255, 255, 255, .08); }
/* Low width with the tier inline (1281-1599px; below 1281 the tier is a
   drawer and the rows are cards): ONE date column, the sorted one, on the
   template with one date track less (owner, 2026-09-06: "doesn't make much
   sense to display both Created and Updated columns on low screen space"). */
@media (min-width: 1281px) and (max-width: 1599.98px) {
    .issues-list-card .issues-list-rows-head,
    .issues-list-card .issues-list-row { grid-template-columns: var(--issues-grid-cols-compact, var(--issues-grid-cols)); }
    .issues-list-card:not([data-issues-dates="created"]) .issues-list-col-head-created,
    .issues-list-card:not([data-issues-dates="created"]) .issues-list-row-date-created,
    .issues-list-card[data-issues-dates="created"] .issues-list-col-head-updated,
    .issues-list-card[data-issues-dates="created"] .issues-list-row-date-updated { display: none; }
}
/* Small-viewport / touch disposition for this table lives in
   issues-responsive.css (row -> card, popovers -> bottom sheets) -
   ONE responsive layer, never a second one here. The desktop-width
   complaint ("text stops at half width") is solved upstream: the
   sizer keeps the TITLE as the row's only flex track. */
/* Zebra stripes for vertical scannability. Very faint slate
   (~2% black) so the rows still feel like one continuous list;
   on hover the row picks up a brand-violet wash to confirm
   interactivity. The previous values mixed --gh-canvas-subtle
   which gave a heavier banded look against the new restrained
   canvas. */
.issues-list-row:nth-child(even) {
    background: rgba(15, 23, 42, .02);
}
/* Row-level :hover background was REMOVED. The previous
   primary-tinted wash signalled "this row is clickable" - but
   only the title text is. Hovering the row chrome (state
   icon, dates, type label, author, comment count, etc.) now
   leaves the row visually neutral; only the title text picks
   up its own color + underline on hover (see
   `.issues-list-row-title:hover` below). The zebra striping
   from `:nth-child(even)` stays as the row-tracking signal.
   Dark-mode zebra rule preserved; dark-mode hover rule
   removed alongside the light-mode one. */
html[data-theme="dark"] .issues-list-row:nth-child(even) {
    background: rgba(255, 255, 255, .025);
}

/* Every cell in the row + header carries a right divider so
   columns read as a real table. `!important` because several
   cell-specific overrides below (checkbox, heat strip, title
   two-line cell) reset other cell properties, and we lost
   the divider once before to specificity drift. The whole
   table-readability promise lives on this one line - it must
   not lose to a later rule. Last cell explicitly drops its
   divider so we don't paint a phantom edge past the
   rightmost column. */
.issues-list-row > *,
.issues-list-rows-head > * {
    /* Translucent column dividers - same rationale as the row
       hairlines above. Glass-edge feel, adapts to dark mode via
       the override below. `!important` retained because several
       cell-specific overrides reset other cell properties and
       have historically clobbered this divider. */
    border-right: 1px solid rgba(0, 0, 0, .08) !important;
    padding: 6px 10px;
    overflow: hidden;
    min-width: 0;
    display: flex;
    align-items: center;
    box-sizing: border-box;
}
html[data-theme="dark"] .issues-list-row > *,
html[data-theme="dark"] .issues-list-rows-head > * {
    border-right-color: rgba(255, 255, 255, .08) !important;
}
.issues-list-row > *:last-child,
.issues-list-rows-head > *:last-child {
    border-right: 0 !important;
}
/* Heat strip is the FIRST cell - it intentionally drops the
   right border so the 4px colour bar reads as one continuous
   strip against the checkbox cell to its right. */
.issues-list-row > .issues-list-row-heat,
.issues-list-rows-head > .issues-list-col-head-heat {
    border-right: 0 !important;
}
/* The legacy flex-era rules pinned widths on individual cell
   classes (.issues-list-col-head: 68px, .issues-list-row-date:
   68px, .issues-list-row-checkbox: 52px, .issues-list-row-cf-col:
   130px, etc.). Grid mode IGNORES those widths - cell widths
   come from the track template (--issues-grid-cols) - so we
   neutralise them here to stop them fighting the layout. */
.issues-list-row > .issues-list-row-heat,
.issues-list-row > .issues-list-row-checkbox,
.issues-list-row > .issues-list-row-title-cell,
.issues-list-row > .issues-list-row-type-cell,
.issues-list-row > .issues-list-row-labels-cell,
.issues-list-row > .issues-list-row-date,
.issues-list-row > .issues-list-row-cf-col,
.issues-list-row > .issues-list-row-assignees,
.issues-list-rows-head > .issues-list-col-head,
.issues-list-rows-head > .issues-list-col-head-heat,
.issues-list-rows-head > .issues-list-col-head-cf-wrap,
.issues-list-rows-head > .bulk-chooser {
    width: auto;
    flex: initial;
    margin-left: 0;
    margin-right: 0;
}

/* ── Issues bulk-action selection ───────────────────────────────
   Visual language is SHARED with Mail's bulk bar: the .bulk-bar
   / .bulk-chooser / .bulk-chooser-cb / .bulk-chooser-box /
   .bulk-cancel / .bulk-count / .bulk-btn rules above are the
   single source of truth for paint. The Issues page reuses every
   one of them; this block adds ONLY the Issues-specific layout
   wiring (the swap between column-header and toolbar based on
   selection state, and the row-tint rule). Pinned by
   IssuesBulkToolbarTests. */
/* Row-level select checkbox - sits in its own 44px grid
   cell, centred BOTH horizontally and vertically.

   Why this looks fiddly: the label carries `.bulk-chooser-cb`
   (a shared Mail/Issues class) which hard-pins
   `width: 26px; height: 100%`. As a direct grid child that
   `width: 26px` shrinks the item to 26px and anchors it at
   the start of the 44px track (default `justify-self: start`).
   Visually that puts the box hard against the cell's left
   edge while the row gets taller (the row is now ~52px tall
   with the two-line title), so the box also appears
   pinned to the top. The fix has TWO parts:
     - `justify-self: stretch` to make the label fill the
       cell's horizontal track (overriding `width: 26px`).
     - `align-self: stretch` to fill the cell's vertical
       track (so flex centering inside has room to work).
     - `width: 100%; height: 100%` re-asserts the fill
       against `.bulk-chooser-cb`'s hard-pinned dimensions.
     - `display: flex; align-items: center; justify-content: center`
       then centres the painted box inside the now-filled
       label. */
.issues-list-row > .issues-list-row-checkbox {
    padding: 0;
    margin: 0;
    width: 100%;
    height: 100%;
    justify-self: stretch;
    align-self: stretch;
    display: flex;
    align-items: center;
    justify-content: center;
}
/* Header chooser - de-chromed.

   `.bulk-chooser` is shared with Mail's bulk bar where it
   sits as a discrete toolbar pill (border + radius + surface
   bg + fixed 32px height). Inside the table column header
   that pill paint reads as a floating widget next to flat
   header cells - jarring. Strip every piece of standalone
   chrome here so the chooser cell reads as bare metadata,
   identical visual weight to "CREATED" / "UPDATED" beside
   it. The caret button stays in the DOM for keyboard /
   programmatic access but fades to barely-visible at rest;
   hovering the cell brings it forward. */
.issues-list-rows-head > .bulk-chooser {
    padding: 0;
    margin: 0;
    border: 0;
    border-radius: 0;
    background: transparent;
    height: auto;
    justify-self: stretch;
    align-self: stretch;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 2px;
}
.issues-list-rows-head > .bulk-chooser > .bulk-chooser-cb {
    width: auto;
    height: auto;
}
.issues-list-rows-head > .bulk-chooser > .bulk-chooser-caret {
    width: 14px;
    height: 14px;
    opacity: 0;
    transition: opacity .12s ease, color .12s ease, background .12s ease;
}
.issues-list-rows-head:hover > .bulk-chooser > .bulk-chooser-caret,
.issues-list-rows-head > .bulk-chooser:focus-within > .bulk-chooser-caret,
.issues-list-rows-head > .bulk-chooser > .bulk-chooser-caret[aria-expanded="true"] {
    opacity: 1;
}
.issues-list-row.is-selected {
    background: color-mix(in srgb, var(--primary) 8%, transparent);
}

/* The bulk-region wrapper holds the toolbar AND the column
   header in the same vertical slot; .is-bulk-mode flips which
   one is visible. The toolbar is hidden by default so the page
   shows the standard column header until the user picks at
   least one row. */
.issues-list-bulk-region {
    border-bottom: 1px solid var(--gh-border-muted, var(--border));
}
.issues-list-bulk-region + .issues-list-rows-head-clip .issues-list-rows-head {
    /* Rows-head sits inside the head-clip wrapper, which is a
       sibling of bulk-region. Bulk-region owns the shared
       bottom border; the head drops its own to avoid doubling. */
    border-bottom: 0;
}

/* Header clip + scroll architecture.
   ---------------------------------
   The card splits into three regions stacked vertically:
     1. .issues-list-bulk-region  - bulk-action toolbar
     2. .issues-list-rows-head-clip - viewport-width wrapper that
                                      CLIPS the wide grid header
                                      so it never widens the page.
                                      `overflow: clip` does NOT
                                      establish a scroll container
                                      per spec, so the inner head's
                                      sticky-y binds further up
                                      (to the document) instead of
                                      to this wrapper.
     3. .issues-list-rows-scroll  - horizontal scroll container
                                    holding the rows ul. The user
                                    scrolls this container to pan
                                    the columns sideways; the
                                    page chrome stays put.

   issues-ui.js syncs the header's translateX with the rows-
   scroll's scrollLeft so the header pans visually in lockstep
   with the rows. */
.issues-list-rows-head-clip {
    overflow: clip;
    /* Sticky lives on the CLIP, not the inner head. The clip's
       containing block is the card (its parent), giving sticky
       the full table height to engage in. If sticky were on the
       inner head, its containing block would be this clip
       wrapper itself (~42px tall) and the head could only stick
       for ~0px before its bottom hit the containing-block
       bottom edge.
       top is 0: the page scrolls inside .app-main, whose
       scrollport starts BELOW the global navbar, so the column
       header strip pins flush at the scrollport's top edge. */
    position: sticky;
    top: 0;
    /* Above every row-level lift. The rows' piles raise a face to
       z-index 2 (the owner's mark in front, a hovered pip lifted), and
       at the same level the LATER element wins: a pile scrolling under
       the header painted its face over the column words (owner,
       2026-09-13: "the pinned header must be on front of other UI
       elements"). The header stands one notch above any lift a row
       may take; the piles also isolate their own stacking below. */
    z-index: 3;
    /* Subtle slate tint - much lighter than the previous GitHub
       grey (#f6f8fa) but enough to set the header apart from the
       white data rows. ~3.5% slate reads as "section header" on
       a white canvas without painting a banded slab.
       The tint is composited OVER an opaque canvas fill (gradient
       layer + solid layer) so the sticky strip is fully opaque:
       rows scrolling underneath must never bleed through the
       header. A bare translucent rgba let the data rows show
       through the header on scroll - it read as see-through. */
    background:
        linear-gradient(rgba(15, 23, 42, .035), rgba(15, 23, 42, .035)),
        var(--gh-canvas);
}
/* Row hover: the row under the pointer reads as one interactive
   band (neutral hover surface, the sanctioned list grammar), and
   the title underlines so the primary link is unmistakable. */
.issues-list-row:hover {
    background: var(--surface-2, #f1f5f9);
}
html[data-theme="dark"] .issues-list-row:hover {
    background: rgba(255, 255, 255, .045);
}
.issues-list-row:hover .issues-list-row-title {
    text-decoration: underline;
    text-underline-offset: 3px;
}
/* Date cells read as a NUMERIC column: tabular digits so "il y a
   30 j" / "2026-06-19" stack into tidy verticals instead of a
   ragged prose column. */
.issues-list-row-date {
    font-variant-numeric: tabular-nums;
}
/* List page rhythm (user 2026-07-29: the top gap read
   disproportionate): the page keeps the codified 32px top pad
   (CONVENTIONS.md 'Spacing rules', pinned by
   PageTopPaddingNeverBelowMinimumTests) and the FIRST block pulls
   up instead - same 18px visual, no page-level floor violation.
   The About panel's own padding carries the breathing room.
   Detail/New keep the roomier top (prose-focused pages). */
.issues-page.issues-list-page {
    padding-top: 32px;
}
/* Extra .issues-page-flow class: outranks the about-card's own
   `margin-top: 0` (same 3-class specificity, later in file) so the
   pull wins order-independently for WHATEVER renders first. */
.issues-page.issues-page-flow.issues-list-page > .issues-about-card:first-child {
    margin-top: -14px;
}
.issues-page.issues-list-page .issues-about-card {
    margin-top: 0;
}
html[data-theme="dark"] .issues-list-rows-head-clip {
    background:
        linear-gradient(rgba(255, 255, 255, .04), rgba(255, 255, 255, .04)),
        var(--gh-canvas);
}
/* In-flight sort / filter swap (issues-list-softnav.js): opacity-only
   dim on the ROWS while the fresh list is fetched - the header strip
   the user just clicked stays crisp, and nothing may move (zero CLS).
   Pinned by IssuesListSoftSortTests. */
.issues-list-page.is-list-refreshing .issues-list-rows-scroll {
    opacity: .55;
    transition: opacity .12s ease;
}
.issues-list-page.is-list-refreshing {
    cursor: progress;
}
.issues-list-rows-scroll {
    overflow-x: auto;
    overflow-y: visible;
}
.issues-list-rows-scroll > .issues-list-rows {
    /* The ul itself doesn't constrain its rows; each row's
       grid renders at the natural sum of its track minimums,
       and the scroll container handles overflow. min-content
       sums minmax-min values (the per-track floor); max-content
       would expand flex tracks unboundedly. */
    min-width: min-content;
}
.issues-list-rows-head-clip > .issues-list-rows-head {
    /* The head's grid matches the rows' natural width so the
       column headers stay aligned with the columns below. */
    min-width: min-content;
}
.issues-bulk-bar {
    display: none;
    /* Issues toolbar swaps in for the column header when at
       least one row is selected; the column header is full-width
       inside the card, so the toolbar's left padding stays at
       16px to align the chooser checkbox with the per-row
       checkboxes below. Mail's .bulk-bar default is 1rem
       horizontal padding which already matches. */
    height: auto !important;  /* override .bulk-bar's fixed 52px */
    min-height: 44px;
    padding: .4rem .85rem;
}
/* Hide the always-visible column header (which carries its own
   master chooser) while the bulk toolbar is up - otherwise the
   user sees TWO master checkboxes stacked vertically, one in the
   toolbar and one in the head right below it. The column header
   lives inside `.issues-list-rows-head-clip` (a sticky-positioned
   wrapper added later for horizontal-scroll syncing); the older
   selector targeted `.issues-list-rows-head` as a direct sibling
   of `.issues-list-bulk-region`, which the wrapper broke - the
   `+` combinator only matches direct DOM siblings. Target the
   clip wrapper instead so the swap actually fires. */
.issues-list-bulk-region.is-bulk-mode + .issues-list-rows-head-clip {
    display: none;
}
/* When rows are selected the toolbar REPLACES the (sticky) column
   header, so it must inherit the header's sticky behaviour: pinned
   at the .app-main scrollport's top edge (same top:0 as the
   rows-head-clip) so it stays on-screen no matter how far the list
   is scrolled. Without this the toolbar sits in normal flow at the
   top of the card and scrolls out of view, so selecting a row far
   down the list appears to do nothing.
   z-index sits one above the rows-head-clip (z:2); the background is
   forced opaque (the bar's own fill is translucent) so rows panning
   underneath never bleed through the pinned toolbar. */
.issues-list-bulk-region.is-bulk-mode {
    position: sticky;
    top: 0;
    z-index: 3;
    background: var(--gh-canvas);
}
html[data-theme="dark"] .issues-list-bulk-region.is-bulk-mode {
    background: var(--gh-canvas);
}
.issues-list-bulk-region.is-bulk-mode .issues-bulk-bar {
    display: flex;
}
/* Issues toolbar paints with the SAME muted gray the column
   header uses (canvas-subtle 50%) so it visually reads as a
   header swap, not a selection callout. The selected ROWS get
   the brand wash; reusing the same wash on the toolbar made
   the selected count + the rows blend into one purple block,
   confusing the user about which rows are selected. Header
   stays gray, rows stay purple - clean distinction. */
.issues-list-bulk-region.is-bulk-mode .issues-bulk-bar {
    background: color-mix(in srgb, var(--gh-canvas-subtle, var(--surface-2)) 50%, transparent);
    border-bottom: 1px solid var(--gh-border-muted);
}
.issues-list-bulk-region.is-bulk-mode .issues-bulk-bar .bulk-bar-actions {
    display: flex;
}

/* Bulk-toolbar picker overrides. Detail's .issues-detail-side-picker
   targets a tiny gear-icon trigger (24×24) inside the right-side
   sidebar; the bulk toolbar instead wraps its existing .bulk-btn
   button shape inside the <details>. The summary needs to keep
   the .bulk-btn paint (icon + text + padding) - Detail's
   `.issues-detail-side-picker summary { width: 24px; height: 24px }`
   collapses my buttons to the gear-icon shape and stacks them on
   top of each other. The override below restores the .bulk-btn
   shape AND positions the popover directly below the trigger
   inside the toolbar (no need for Detail's `position: fixed` JS
   hack since the toolbar isn't inside a clipped overflow). */
.issues-bulk-bar .bulk-btn-picker {
    position: relative;
    display: inline-flex;
}
.issues-bulk-bar .bulk-btn-picker > summary.bulk-btn {
    /* Reset Detail's tight 24×24 sidebar sizing - keep the
       full .bulk-btn shape (icon + text + 30px-tall pill).
       Explicit height pin matches Mail's other bulk buttons
       so the toolbar's row height stays consistent across
       picker / direct-action buttons. */
    width: auto;
    height: 30px;
    list-style: none;
    cursor: pointer;
}
.issues-bulk-bar .bulk-btn-picker > summary.bulk-btn::-webkit-details-marker {
    display: none;
}
.issues-bulk-bar .bulk-btn-picker[open] > summary.bulk-btn {
    background: var(--surface-2);
    border-color: var(--border-strong);
}
.issues-bulk-bar .bulk-btn-picker .issues-detail-side-pop {
    /* Override Detail's `position: fixed` + JS positioning -
       the toolbar lives at the top of the page where there's
       no overflow clip, so a plain absolute positioning works
       and we avoid the JS dependency entirely. Anchored to the
       LEFT edge of the trigger button so the dropdown opens
       under the icon (Label / Assign / Type). The JS clamp
       below flips to right-anchor when the default would
       overflow the right edge of the viewport. */
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    right: auto;
    width: 320px;
    max-height: 420px;
    z-index: 50;
}

/* Column-header row above the issue list. Pure visual labels
   - sorting is owned by the Sort dropdown above so there's
   exactly ONE place to change the order. Tight, all-caps micro-
   label; the active sort lights up the matching date header in
   brand color so the user can correlate the column with the
   dropdown selection at a glance. Header columns + data
   columns share the same fixed widths so the list reads as a
   table. */
.issues-list-rows-head {
    /* Mirror the row's grid so header columns land directly
       over their data cells - one less alignment trap to
       worry about. The same --issues-grid-cols variable on
       the .issues-list-card wrapper drives both.

       Visually distinct from the rows below: tinted
       background, taller cell padding, heavier bottom
       border. The user reads this row as the table head,
       not as another data row. */
    display: grid;
    grid-template-columns: var(--issues-grid-cols);
    align-items: stretch;
    /* Header row sits on the slate-tinted wrapper above. Background
       transparent here so the wrapper's tint shows through; the
       bottom hairline carries the separation from the first data
       row. Slightly heavier (rgba .12) so the boundary is clearly
       visible against the tinted header. */
    border-bottom: 1px solid rgba(0, 0, 0, .12);
    background: transparent;
    min-height: 42px;
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--gh-fg-muted);
    /* Sticky lives on the clip wrapper above (its containing
       block is the card, giving sticky the full table height
       to engage in). The head itself stays in normal flow so
       its translateX (JS-synced to rows-scroll.scrollLeft) can
       pan the columns horizontally without sticky-x mishaps. */
}
html[data-theme="dark"] .issues-list-rows-head {
    border-bottom-color: rgba(255, 255, 255, .14);
}
.issues-list-rows-head > * {
    /* Header cells get extra vertical padding so the head
       reads taller / more distinct from the data rows
       (which use 6px padding). */
    padding-top: 10px;
    padding-bottom: 10px;
}
.issues-list-col-head {
    /* Grid cell - actual width comes from the parent's
       --issues-grid-cols track template. No flex-basis or
       width here, otherwise it fights the grid layout. */
    display: inline-flex; align-items: center; gap: 4px;
    color: var(--text-muted);
    text-decoration: none;
    cursor: pointer;
    transition: color .12s ease;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.issues-list-col-head:hover { color: var(--text); }
.issues-list-col-head.is-active { color: var(--text); }
/* Sort discoverability: an INACTIVE sortable header reveals a
   ghost caret on hover, so "these columns sort" needs no guessing.
   The active column's caret is real markup; this is paint only.
   Every labeled column sorts (user 2026-07-29), so every labeled
   header carries the hint. */
.issues-list-col-head-created:not(.is-active):hover::after,
.issues-list-col-head-updated:not(.is-active):hover::after,
.issues-list-col-head-title:not(.is-active):hover::after,
.issues-list-col-head-labels:not(.is-active):hover::after,
.issues-list-col-head-roadmap:not(.is-active):hover::after,
.issues-list-col-head-assignees:not(.is-active):hover::after {
    content: "";
    width: 9px;
    height: 9px;
    flex: 0 0 auto;
    opacity: .45;
    background: currentColor;
    -webkit-mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>') center / contain no-repeat;
    mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>') center / contain no-repeat;
}
.issues-list-col-head svg { flex-shrink: 0; opacity: .9; }
.issues-list-col-head-num,
.issues-list-col-head-type,
.issues-list-col-head-author,
.issues-list-col-head-comments {
    /* Static (non-sortable) headers - same paint as the
       sortable links but no cursor / hover affordance.
       Title / Labels / Roadmap / Assignees left this group
       when every labeled column became a sort toggle. */
    cursor: default;
}
.issues-list-col-head-num:hover,
.issues-list-col-head-type:hover,
.issues-list-col-head-author:hover,
.issues-list-col-head-comments:hover { color: var(--text-muted); }
.issues-list-col-head-comments { justify-content: center; }
.issues-list-col-head-state { justify-content: center; }
.issues-list-col-head-spacer { display: none; }

/* Custom-field columns: header + per-row cell. Each column
   reserves a fixed pixel slot so values line up vertically
   across rows (the whole point of a column view). The
   header is clickable to toggle sort; the cells are filter
   links when they have a value, blank when not.

   Width 130px is a sweet spot for short option names like
   "In Progress" or "High" without crowding. Date / number
   cells comfortably fit in the same width. Long text values
   truncate with an ellipsis - the full value is in the
   title attribute for hover. */
.issues-list-col-head-cf {
    /* Width comes from the parent grid track (130px slot in
       --issues-grid-cols). The flex inside the wrap arranges
       the sort link + filter affordances within the cell. */
    flex: 1 1 auto;
    min-width: 0;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    color: var(--text-muted);
    text-transform: uppercase;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.04em;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    text-decoration: none;
    cursor: pointer;
}
.issues-list-col-head-cf:hover { color: var(--text); text-decoration: none; }
.issues-list-col-head-cf.is-active { color: var(--text); }
.issues-list-col-head-cf-label {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Column-header wrapper that contains the sort link + the
   optional filter popover + the active-filter clear pill.
   The wrapper takes the full 130px slot; its children share
   the row inside. */
.issues-list-col-head-cf-wrap {
    /* Width comes from the parent grid track. The shared
       `.issues-list-rows-head > *` rule already provides
       padding + border-right + overflow:hidden, so we only
       need the inline-flex layout here. */
    display: inline-flex;
    align-items: center;
    gap: 4px;
    min-width: 0;
}
.issues-list-col-head-cf-wrap > .issues-list-col-head-cf {
    flex: 1 1 auto;
    width: auto;
    border-left: 0;
    padding: 0;
    min-width: 0;
}
.issues-list-col-head-cf-wrap.is-filtered > .issues-list-col-head-cf {
    color: var(--primary);
}
/* Filter popover trigger (funnel icon). */
.issues-list-col-head-cf-filter {
    position: relative;
    flex: 0 0 auto;
}
.issues-list-col-head-cf-filter > summary {
    list-style: none;
    cursor: pointer;
    width: 18px;
    height: 18px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--text-muted);
    border-radius: 4px;
}
.issues-list-col-head-cf-filter > summary::-webkit-details-marker { display: none; }
.issues-list-col-head-cf-filter > summary:hover {
    background: var(--surface-2);
    color: var(--text);
}
.issues-list-col-head-cf-wrap.is-filtered .issues-list-col-head-cf-filter > summary {
    color: var(--primary);
}

/* Active-filter chip used to render an inline pill in the
   column header showing the filtered value + an x to clear -
   visual noise inside a tight column header. Removed: the
   column header now signals an active filter through the
   funnel icon colour (.is-filtered on the wrap), and the
   filter popover offers a "Clear" affordance. */
.issues-list-col-head-cf-filter-clear { display: none; }

.issues-list-row-cf-col {
    /* Width comes from the parent grid track (130px slot).
       Shared `.issues-list-row > *` provides padding +
       border-right + overflow + display:flex. */
    white-space: nowrap;
    font-size: 12px;
    color: var(--text);
}
.issues-list-row-cf-col-text {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--text);
    text-decoration: none;
    max-width: 100%;
}
.issues-list-row-cf-col-text:hover {
    text-decoration: none;
    color: var(--text);
}
.issues-list-row-cf-col-chip {
    display: inline-flex;
    align-items: center;
    padding: 2px 8px;
    border-radius: 4px;
    font-size: 11px;
    font-weight: 600;
    line-height: 1.4;
    color: var(--text);
    background: color-mix(in srgb, var(--cf-color, var(--text-muted)) 14%, transparent);
    border: 1px solid color-mix(in srgb, var(--cf-color, var(--text-muted)) 36%, transparent);
    max-width: 100%;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    text-decoration: none;
}
.issues-list-row-cf-col-chip:hover {
    text-decoration: none;
    filter: brightness(0.96);
}

/* Phase 2: when custom-field columns push the row past
   viewport width, the row container scrolls horizontally
   while the title column stays anchored. The trick is to
   set overflow-x on the row container, then position:sticky
   on the title body so it pins to the left edge during
   scroll. Activates at viewport widths where the row would
   otherwise truncate. */
@media (max-width: 1600px) {
    .issues-list .issues-list-row,
    .issues-list-bulk-region .bulk-bar,
    .issues-list-card > .issues-list-rows-head {
        /* Don't allow the columns to wrap. */
        flex-wrap: nowrap;
    }
}
/* Three-state list-display picker: the fieldset frame. The choice
   cards + the styled ring/dot mark (which hides the native radio)
   live with the rest of the custom-fields catalog styles further
   down - one grammar, one place. */
.issues-custom-fields-display-mode {
    border: 0;
    padding: 0;
    margin: 0 0 10px;
    display: flex;
    flex-direction: column;
}
.issues-custom-fields-display-mode > legend {
    padding: 0;
    margin: 0 0 6px;
    font-size: 12px;
    font-weight: 600;
    color: var(--text);
}
.issues-custom-fields-display-mode .form-check .form-hint {
    margin: 0;
    font-size: 11px;
    color: var(--text-muted);
    line-height: 1.35;
    font-weight: 400;
}

/* Issue-number prefix that sits inline before the title link.
   Tabular monospace so a column of #1, #12, #123 stays
   visually aligned at the left of the title cluster. Subtle
   muted color so the number reads as metadata, not as the
   row's primary content. */
/* .issues-list-row-num is no longer a grid cell - it renders
   inline at the end of the title cell (GitHub-Projects style).
   The cell-local rule for the inline #N lives under
   .issues-list-row-title-cell .issues-list-row-num above. */

/* Dedicated metadata cells - one grid track each. The shared
   `.issues-list-row > *` rule provides padding + border-right
   + overflow + display:flex + min-width:0, so each rule below
   only adds the per-cell typography / alignment / chip layout.
   Width comes from --issues-grid-cols. */
/* `.issues-list-row-type-cell` and `.issues-list-row-title-*`
   classes from earlier iterations no longer exist in markup -
   the Type column was dropped and the type icon + author
   avatar now render INSIDE the title cell's meta row. */
.issues-list-row-labels-cell {
    /* Chip wrap - if the row has many labels we let them flow
       onto a second line rather than clipping, since this is
       the only cell where the user explicitly opted into a
       variable-height payload. The rest of the row sets its
       own height by the title cell. */
    flex-wrap: wrap;
    gap: 4px;
    justify-content: flex-start;
    align-content: center;
    overflow: hidden;
}
.issues-list-row-labels-cell .issues-label-chip,
.issues-list-row-labels-cell .issues-list-row-cf-chip {
    flex-shrink: 0;
}
.issues-list-row-votes {
    font-size: 11px;
    font-weight: 600;
    color: var(--gh-fg-muted);
    font-variant-numeric: tabular-nums;
    margin-left: 4px;
}

/* Two compact date columns at the left of each row, prefixed
   by tiny calendar / pencil icons for visual texture. Width
   matches the column headers above to the pixel so the list
   reads as a table. The Updated cell carries a 1px left
   divider so it visibly partitions from Created. Updated-
   equal-to-created (no edit since creation) is rendered in a
   softer muted color via .is-stale so the column still shows
   data yet de-emphasises rows where nothing has changed. */
.issues-list-row-date {
    /* Grid cell - width from --issues-grid-cols (76px). */
    display: inline-flex; align-items: center; gap: 5px;
    font-size: 11px;
    line-height: 1.3;
    font-variant-numeric: tabular-nums;
    cursor: default;
    user-select: text;
    white-space: nowrap;
    color: var(--text-muted);
}
.issues-list-row-date-text {
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
}
.issues-list-row-date-icon {
    flex-shrink: 0;
    opacity: .55;
    color: currentColor;
}
.issues-list-row-date-created { font-weight: 500; }
.issues-list-row-date-updated {
    /* Updated cell sits in its own grid track - no negative
       margin gymnastics needed since grid handles column
       widths. */
}
.issues-list-row-date-updated.is-stale {
    /* "stale" = no activity since creation. Slightly muted so
       it visually de-emphasises rows where nothing has
       changed, but still readable - the previous near-border
       grey was effectively invisible. */
    color: color-mix(in srgb, var(--text-muted) 80%, transparent);
}
.issues-list-row-date-updated.is-stale .issues-list-row-date-icon { opacity: .55; }

/* Author cluster on the row meta line: avatar + "#N by name".
   The avatar comes from the canonical Html.UserAvatar helper
   (single source of truth for avatar paint - see
   feedback_single_avatar_renderer). Tight 16px bubble that
   sits inline with the meta text. */
.issues-list-row-meta-author {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
.issues-list-row-meta-avatar {
    width: 16px; height: 16px;
    border-radius: 50%;
    flex-shrink: 0;
}
/* GitHub-import author fallback on a list row: paints the
   octocat mark inside a muted pill so the row visibly reads
   as imported (same paint as the detail page's
   `.issues-thread-avatar-imported`) and the author chip
   links externally to the source. */
.issues-list-row-meta-author-github {
    text-decoration: none;
    color: inherit;
}
.issues-list-row-meta-author-github:hover .issues-list-row-author {
    text-decoration: underline;
    text-underline-offset: 2px;
}
.issues-list-row-meta-avatar-github {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 16px; height: 16px;
    border-radius: 50%;
    background: var(--gh-canvas-subtle);
    border: 1px solid var(--gh-border);
    color: var(--gh-fg-default);
    flex-shrink: 0;
}
.issues-list-row-meta-avatar-github svg {
    width: 11px; height: 11px;
    color: var(--gh-fg-default);
    fill: var(--gh-fg-default);
}

/* Activity heat bar - 3px-wide vertical strip at the very
   left of every row. Color bucket reflects how recent the
   issue's last activity is so the user can spot fresh
   threads at a glance:
     hot  (today)        - brand primary, fully saturated
     warm (this week)    - amber
     cool (this month)   - muted blue/grey
     cold (older)        - barely-there grey
   Tooltip on hover repeats the relative time for context.
   Top-aligned (matches the rest of the left-side cells) and
   sized to span the title's first line so the rail keeps a
   consistent presence regardless of how tall the row gets. */
/* Activity heat strip - 4px-wide vertical column at the very
   left of every row. Colour bucket reflects how recent the
   issue's last activity is so the user can spot fresh threads
   at a glance:
     hot   (≤ 24h)   - saturated primary
     warm  (≤ 1w)    - amber
     cool  (≤ 1mo)   - muted blue/grey
     cold  (older)   - barely-visible grey
   The strip fills the cell - no padding, no border-right, no
   gap; the colour IS the cell. The matching header cell is an
   empty placeholder so the grid alignment is preserved. */
.issues-list-row > .issues-list-row-heat,
.issues-list-rows-head > .issues-list-col-head-heat {
    padding: 0;
    align-self: stretch;
}
.issues-list-row-heat[data-heat="hot"]  { background: var(--primary); }
.issues-list-row-heat[data-heat="warm"] { background: color-mix(in srgb, #f59e0b 90%, transparent); }
.issues-list-row-heat[data-heat="cool"] { background: color-mix(in srgb, var(--gh-fg-muted) 35%, transparent); }
.issues-list-row-heat[data-heat="cold"] { background: color-mix(in srgb, var(--gh-fg-muted) 12%, transparent); }
.issues-list-col-head-heat { background: transparent; }
/* Open / closed state glyph, rendered inline at the left of
   the title cell (GitHub-Projects style). The icon picks up
   the issue's state colour via the row-level
   data-issue-state selectors below. */
.issues-list-row-state {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 16px; height: 16px;
}
.issues-list-row[data-issue-state="open"] .issues-list-row-state { color: var(--gh-success-emphasis); }
.issues-list-row[data-issue-state="closed"] .issues-list-row-state { color: var(--gh-done-fg); }
/* Legacy row-body / titlerow / meta wrappers are no longer
   emitted - each piece of metadata is now its own grid cell.
   Hide them defensively in case any cached partial markup
   still references them. */
.issues-list-row-body,
.issues-list-row-titlerow,
.issues-list-row-meta { display: contents; }

/* Title cell - two-line layout:
     Row 1: [state glyph] [title…]
     Row 2: meta strip (#N · type · avatar+name · 💬 N)
   The shared `.issues-list-row > *` rule sets `display:
   flex; align-items: center`. Override here to a column
   layout so the two rows stack. Vertical padding bumped so
   the cell has breathing room around both lines. Min-width:0
   lets the title row ellipsis-clip correctly. */
.issues-list-row-title-cell {
    flex-direction: column;
    align-items: stretch;
    justify-content: center;
    gap: 3px;
    min-width: 0;
    padding-top: 8px !important;
    padding-bottom: 8px !important;
}
.issues-list-row-title-row {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
    line-height: 1.3;
}
.issues-list-row-title {
    /* `flex: 0 1 auto` (not `1 1 auto`) so the anchor's
       intrinsic width = the rendered title text. Hovering the
       row OUTSIDE the title text no longer falls on the anchor,
       so the issue-preview popover (bound to anchor mouseover)
       only fires when the cursor is actually on the title -
       matching the user's mental model "I'm interacting with
       the title, not the row". Long titles still truncate via
       `min-width: 0` + flex-shrink (1) + the ellipsis pair
       below; short titles simply leave horizontal whitespace
       to their right inside the title-row cell. */
    flex: 0 1 auto;
    min-width: 0;
    /* An unbreakable token (a setting, a URL) breaks inside the title where
       the card regimes wrap it - never past the row (owner, 2026-09-13,
       "text going out of the limit"); the table's nowrap/ellipsis pair is
       untouched, nothing wraps there. */
    overflow-wrap: anywhere;
    font-size: 16px;
    font-weight: 600;
    color: var(--gh-fg-default);
    text-decoration: none;
    line-height: 1.35;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.issues-list-row-title:hover { color: var(--gh-accent-fg); text-decoration: underline; text-underline-offset: 2px; }
/* Translated-title indicator - a quiet translate glyph next to a row
   title shown in the viewer's language. Only rendered when the title
   actually differs; muted so it reads as a state marker, not a control,
   and never shifts the title (flex: 0 0 auto). Its tooltip names the
   original. Deliberately NOT a pill/chip - a single icon per the
   no-badge rule. */
.issues-list-row-title-i18n {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    color: var(--gh-fg-muted);
    opacity: .7;
}
.issues-list-row-title-i18n svg { display: block; }
/* Meta strip on row 2 - tight inline-flex of the secondary
   attributes. Muted typography so it reads as metadata,
   not as competing content with the title above. */
.issues-list-row-meta-row {
    display: flex;
    align-items: center;
    gap: 6px;
    min-width: 0;
    font-size: 12px;
    line-height: 1.3;
    color: var(--gh-fg-muted);
    overflow: hidden;
}
/* Card-mode companion of the UPDATED column (see Index.cshtml):
   hidden while the table renders its real date cells; the card arm
   in issues-responsive.css reveals it when the table collapses. */
.issues-list-row-meta-updated,
.issues-list-row-meta-updated-sep { display: none; }
.issues-list-row-meta-updated { white-space: nowrap; }
.issues-list-row-meta-sep {
    flex: 0 0 auto;
    color: color-mix(in srgb, var(--gh-fg-muted) 60%, transparent);
    user-select: none;
}
.issues-list-row-title-cell .issues-list-row-num {
    flex: 0 0 auto;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 12px;
    font-variant-numeric: tabular-nums;
    color: var(--gh-fg-muted);
    cursor: default;
    user-select: text;
    padding: 0;
}
.issues-list-row-meta-type {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: 4px;
}
.issues-list-row-meta-type svg { display: block; }
.issues-list-row-meta-type-label {
    font-size: 12px;
    color: var(--gh-fg-muted);
}
/* THE unread-mention ROW (owner, 2026-09-18: "different appearance of the
   entire row, not a text added in the row"). The rows the pane's Mentions
   figure counts read UNREAD the way a mail row does - the whole row on the
   accent's faint wash, the title at full ink, the meta strip in ink instead
   of muted - and return to rest once the mention is read. No word, no mark,
   no dot, no edge bar (the heat bar keeps the row's left edge). The wash
   outranks the zebra and composes with the hover wash; never the ink wash
   of a SELECTED row (that one is the selection's alone). */
.issues-list-row.has-unread-mention,
.issues-list-row.has-unread-mention:nth-child(even) {
    background: color-mix(in srgb, var(--gh-accent-fg) 8%, transparent);
}
.issues-list-row.has-unread-mention:hover {
    background: color-mix(in srgb, var(--gh-accent-fg) 11%, transparent);
}
html[data-theme="dark"] .issues-list-row.has-unread-mention,
html[data-theme="dark"] .issues-list-row.has-unread-mention:nth-child(even) {
    background: color-mix(in srgb, var(--gh-accent-fg) 10%, transparent);
}
html[data-theme="dark"] .issues-list-row.has-unread-mention:hover {
    background: color-mix(in srgb, var(--gh-accent-fg) 14%, transparent);
}
.issues-list-row.has-unread-mention .issues-list-row-title {
    font-weight: 700;
    color: var(--text);
}
.issues-list-row.has-unread-mention .issues-list-row-meta-row,
.issues-list-row.has-unread-mention .issues-list-row-num,
.issues-list-row.has-unread-mention .issues-list-row-meta-author {
    color: var(--text);
}
/* The row's ONE glyph, no word: the pane's @ in the accent at the title
   row's right end (margin-left:auto aligns the column by construction),
   16px like the state glyph at the row's other end. */
.issues-list-row-mention-glyph {
    flex: 0 0 auto;
    margin-left: auto;
    padding-left: 12px;
    display: inline-flex;
    align-items: center;
    color: var(--gh-accent-fg);
}
.issues-list-row-mention-glyph svg {
    display: block;
    width: 16px;
    height: 16px;
    stroke-width: 2;
}
.issues-list-row-meta-author {
    flex: 0 1 auto;
    min-width: 0;
    display: inline-flex;
    align-items: center;
    gap: 5px;
    color: var(--gh-fg-muted);
    text-decoration: none;
    cursor: pointer;
    overflow: hidden;
}
.issues-list-row-meta-author:hover { color: var(--gh-fg-default); }
.issues-list-row-meta-author:hover .issues-list-row-meta-author-name {
    text-decoration: underline;
    text-underline-offset: 2px;
}
.issues-list-row-meta-author-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}
.issues-list-row-meta-avatar {
    width: 16px; height: 16px;
    border-radius: 50%;
    flex-shrink: 0;
}
.issues-list-row-meta-author-github {
    cursor: pointer;
}
.issues-list-row-meta-avatar-github {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 16px; height: 16px;
    border-radius: 50%;
    background: var(--gh-canvas-subtle);
    border: 1px solid var(--gh-border);
    color: var(--gh-fg-default);
    flex-shrink: 0;
}
.issues-list-row-meta-avatar-github svg {
    color: var(--gh-fg-default);
    fill: var(--gh-fg-default);
}
.issues-list-row-meta-type {
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
}
.issues-list-row-meta-type svg { display: block; }
.issues-list-row-author {
    color: var(--gh-fg-muted);
}
.issues-list-row-author:hover { color: var(--gh-accent-fg); cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
/* Comments cell - always emitted (the wrapper span exists
   even when the issue has zero comments) so the column
   reserves its 56px pixel width and the assignee column at
   the row's right edge always lines up vertically. */
/* Comments badge is now rendered INLINE inside the title cell
   (GitHub-Projects style), no longer a dedicated grid column.
   Tight inline-flex pill: speech-bubble icon + count, muted
   colour so it sits behind the title visually. */
.issues-list-row-comments {
    flex: 0 0 auto;
    display: inline-flex; align-items: center; gap: 3px;
    padding: 1px 0;
    color: var(--gh-fg-muted);
    text-decoration: none;
    font-size: 12px;
    font-variant-numeric: tabular-nums;
    border-radius: 4px;
}
.issues-list-row-comments:hover { color: var(--gh-fg-default); }
.issues-list-row-comments svg { color: var(--gh-fg-muted); flex-shrink: 0; }

/* Vote chips ride in the same meta strip as the comments chip
   so the title row gains the participation signal without
   reshuffling the layout. Two modifiers:
     .is-up   - upvote tally, neutral muted by default; greens
                slightly on hover so the directional intent is
                obvious without screaming on the row.
     .is-down - downvote tally, same muted baseline; reds on
                hover with the same restraint.
   Numeric count uses tabular-nums so a row stack with mixed
   counts (3 / 11 / 132) keeps the digits in vertical column. */
.issues-list-row-votes {
    flex: 0 0 auto;
    display: inline-flex; align-items: center; gap: 3px;
    padding: 1px 0;
    color: var(--gh-fg-muted);
    text-decoration: none;
    font-size: 12px;
    font-variant-numeric: tabular-nums;
    border-radius: 4px;
}
.issues-list-row-votes svg { color: var(--gh-fg-muted); flex-shrink: 0; }
.issues-list-row-votes:hover { color: var(--gh-fg-default); }
.issues-list-row-votes.is-up:hover { color: var(--gh-success-fg, #1a7f37); }
.issues-list-row-votes.is-up:hover svg { color: var(--gh-success-fg, #1a7f37); }
.issues-list-row-votes.is-down:hover { color: var(--gh-danger-fg, #cf222e); }
.issues-list-row-votes.is-down:hover svg { color: var(--gh-danger-fg, #cf222e); }

/* Label chips on the row - flow INLINE right after the title
   text, GH-style. A long title may push labels onto the next
   line via flex-wrap, but the chip(s) always sit immediately
   adjacent to the last word of the title rather than being
   pinned to the row's right edge. */
.issues-list-row-labels {
    /* Legacy inline-labels container - retained for old
       partial markup; replaced by .issues-list-row-labels-cell
       in the grid view. */
    display: inline-flex; flex-wrap: wrap; gap: 4px;
    align-items: center;
}

/* Assignees cell - LAST cell of every row, always emitted
   even when the issue has no assignees so the column
   reserves its pixel width and reads as a clean vertical
   strip of avatars (or a stable empty space) down the list. */
.issues-list-row-assignees {
    /* Grid cell - width from --issues-grid-cols (140px).
       Right-aligned so the overlap pile hugs the cell's
       right edge, matching the column header. */
    justify-content: flex-end;
}
/* Larger 28px avatars in the row-pile, vs 32px in the
   workspace card. The pile reuses .ws-members-pile /
   .ws-members-pip from the workspace member preview;
   we just size the avatars one notch smaller here so the
   row stays dense without the pile dominating. The 2px
   surface ring around each avatar makes the overlap read
   as discrete circles instead of a smear. */
.issues-list-row-assignee-avatar {
    width: 28px !important;
    height: 28px !important;
    box-shadow: 0 0 0 2px var(--gh-canvas);
    background: var(--avatar-bg);
    color: var(--avatar-color);
    font-size: 11px !important;
    font-weight: 600 !important;
}
/* THE owner mark on a pile face (issue #450, P2): the corner flag the People
   block wears, seated bottom-right on a canvas ring so it reads on any photo -
   the tile corner-flag family, never a dot. */
.ws-members-pip.is-owner,
.bf-alllist-owner.is-owner,
.ms-avatar-owner { position: relative; display: inline-flex; }
/* In a pile the owner stands FIRST and in front: the next face never covers
   the mark (the pile's overlap paints later faces above earlier ones). */
.issues-list-row-assignee-pile .ws-members-pip.is-owner { z-index: 2; }
/* A pile's stacking stays INSIDE the pile: the owner's front face and a
   hovered pip lift against their neighbours only, never against the
   pinned column header the row scrolls under (the row itself keeps
   z-index auto, so nothing in it can climb over the header). */
.issues-list-row-assignee-pile { position: relative; isolation: isolate; }
.issues-list-owner-mark {
    position: absolute; right: -4px; bottom: -3px;
    display: inline-grid; place-items: center;
    width: 14px; height: 14px; border-radius: 50%;
    background: var(--gh-canvas); color: var(--text);
    box-shadow: 0 0 0 1px var(--border);
    pointer-events: none;
}
.issues-list-owner-mark svg { display: block; }
/* Overflow "+N" pip - matches the avatar size, painted
   muted so it reads as a meta indicator, not as another
   person. */
.issues-list-row-assignee-more {
    width: 28px !important;
    height: 28px !important;
    font-size: 10px !important;
    box-shadow: 0 0 0 2px var(--gh-canvas);
    background: var(--gh-canvas-subtle);
    color: var(--gh-fg-muted);
    border: 1px dashed var(--gh-border-muted);
}

.issues-list-empty {
    padding: 80px 24px;
    text-align: center;
    background: var(--gh-canvas);
}
/* Compact variant - rendered when a filter is active and produced
   zero matches. Smaller padding so the card doesn't dwarf the
   page; the "No matches" hint reads as a result-of-filter, not
   an onboarding moment. */
.issues-list-empty.is-compact {
    padding: 36px 24px;
}
/* A personal slice with nothing in it (owner, 2026-09-08): the slice's own
   glyph, bare - never inside a plate - over ONE line; no verb, the pane's
   List row is the way back. */
.issues-list-empty-glyph { display: inline-flex; margin: 0 0 10px; color: var(--text-subtle); }
.issues-list-empty-glyph svg { width: 28px; height: 28px; }
.issues-list-empty h3.issues-list-empty-line { margin: 0; font-size: 15px; font-weight: 600; color: var(--text); }
/* When the list card holds the empty-state, don't stretch to fill
   remaining viewport height (which the flex:1 1 auto rule above
   does to give the rows-region room to scroll). The empty state
   should size to its own content - a tall blank rectangle is
   noise. :has() lets the parent react to the child's presence. */
.issues-list-card:has(> .issues-list-empty) {
    flex: 0 0 auto;
}
.issues-list-empty h3 {
    margin: 0 0 8px;
    font-size: 20px;
    font-weight: 600;
    color: var(--gh-fg-default);
}
.issues-list-empty p {
    margin: 0 auto 24px;
    max-width: 540px;
    color: var(--gh-fg-muted);
    font-size: 14px;
    line-height: 1.6;
}
.issues-list-empty .btn-primary,
.issues-list-empty .btn {
    background: var(--text);
    border-color: var(--text);
    color: var(--surface, #fff);
}
.issues-list-empty .btn-primary:hover { background: color-mix(in srgb, var(--text) 84%, var(--surface)); color: #fff; }
/* The matches stand in the other tab (#669): its door in ink, Clear filters the quiet
   hairline beside it - 8px apart, the pair wrapping as a whole, never touching. */
.issues-list-empty-actions { display: inline-flex; flex-wrap: wrap; justify-content: center; gap: 8px; }
.issues-list-empty-actions .btn-outline-secondary {
    background: transparent;
    border-color: var(--border);
    color: var(--text);
}
.issues-list-empty-actions .btn-outline-secondary:hover { background: color-mix(in srgb, var(--text) 6%, transparent); color: var(--text); }

/* ─── Issues LIST page responsive ───────────────────────────
   Three breakpoints, keyed to common device widths:
     ≤ 1080 px : tablet (iPad portrait, smaller laptops)
     ≤ 720 px  : tablet portrait + small tablets
     ≤ 540 px  : phone
   The detail page has its own layout chain (.issues-detail-page)
   and isn't affected. */

/* Tablet - narrow the gutters and stack the toolbar vertically
   so the search field gets full width and the action buttons
   (Labels / Import / New issue) keep their full labels on the
   row below. */
@media (max-width: 1080px) {
    .issues-page { padding: 20px 20px 0; }
    .issues-bar-actions { justify-content: flex-end; flex-wrap: wrap; gap: 0; }
    .issues-list-card-head { flex-direction: column; align-items: flex-start; gap: 10px; }
    /* About card: trim padding so it doesn't dominate when the
       page is narrower. */
    .issues-about-card { padding: 14px 16px; }
}

/* Small tablet / large phone - drop the date columns entirely
   and let the title own the row. The dates are still available
   in the per-row meta line below the title ("#42 opened 5d ago
   by NAME") and as a hover tooltip on the row, so we lose no
   information by hiding the duplicate date cells. The previous
   approach (just shrinking the date columns to 56 px each) left
   ~140 px of date chrome before the title even started, and on
   phones titles were breaking mid-word ("Deliver / y Stat…")
   the user reported - the symptom that triggered this change. */
@media (max-width: 720px) {
    .issues-page { padding: 16px 14px 0; }
    .issues-bar-actions { width: 100%; }
    .issues-bar-actions .issues-bar-btn:not(.issues-new-submit) { flex: 1 1 0; min-width: 0; justify-content: center; }
    .issues-bar-actions .issues-new-submit { flex: 1 1 auto; }
    /* Hide the date cells (both head + body) - see comment
       above. The CREATED/UPDATED columns disappear from the
       column header AND from each row, so the title cluster
       gets the full remaining width. */
    .issues-list-col-head-created,
    .issues-list-col-head-updated,
    .issues-list-col-head-spacer,
    .issues-list-row-date-created,
    .issues-list-row-date-updated { display: none; }
    .issues-list-col-head-ticket { margin-left: 4px; }
    .issues-list-rows-head { padding: 6px 12px; gap: 10px; }
    .issues-list-row { padding: 12px 12px; gap: 10px; }
    /* Right-edge slots collapse to content width on tablet too -
       see the comment in the ≤ 540 px block. Reserved widths
       made sense on desktop where vertical column alignment
       across rows reads as a table; on tablet the title cluster
       needs every horizontal pixel it can claim. */
    .issues-list-row-comments-slot,
    .issues-list-row-assignees { width: auto; }
    .issues-list-row-assignees:empty,
    .issues-list-row-comments-slot:empty { display: none; }
    /* Pager: chevron buttons only - drop the "Previous" /
       "Next" word labels to save row width. */
    .issues-pager-step span { display: none; }
}

/* Phone - even tighter chrome + collapse the now-empty
   column header row entirely. */
@media (max-width: 540px) {
    .issues-page { padding: 12px 12px 0; }
    .issues-page-head { padding-bottom: 12px; margin-bottom: 12px; }
    .issues-list-row { padding: 10px 10px; gap: 8px; }
    .issues-list-col-head-ticket { margin-left: 0; }
    /* Right-edge reserved slots (comments + assignees) collapse
       to content width on phones - on desktop they reserve 56 +
       70 px so the columns line up vertically across rows, but
       on phones that 126 px reservation EATS HALF the title's
       width even when most rows have 0 comments and 0 assignees.
       The user's symptom: "horrible text is using half width" -
       caused entirely by these two empty reservations. Drop the
       fixed widths so an empty slot takes 0 px, populated slots
       still render at their natural size. */
    .issues-list-row-comments-slot,
    .issues-list-row-assignees {
        width: auto;
    }
    .issues-list-row-assignees:empty,
    .issues-list-row-comments-slot:empty { display: none; }
    /* About card: even smaller padding + no min-height growth
       so it stays as a tight summary above the list. */
    .issues-about-card { padding: 12px 14px; margin: 12px 0 6px; font-size: 14px; line-height: 1.5; }
    /* Action buttons in the bar wrap to two rows when needed
       (e.g. Labels + Import + New issue). The New issue CTA
       breaks onto its own row by claiming 100 % width once
       it's the lone child. */
    .issues-bar-actions {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 6px;
    }
    .issues-bar-actions > * {
        border-radius: 6px !important;
        border-left: 1px solid var(--gh-btn-border) !important;
    }
    .issues-bar-actions .issues-new-submit {
        grid-column: 1 / -1;
    }
    /* Column-header row is empty after hiding both date cells
       above; collapse it visually so the top border of the first
       row doubles as the table head separator. */
    .issues-list-rows-head { display: none; }
    /* Pager rolls onto two lines on a phone - center the chevrons
       so the layout reads cleanly. */
    .issues-pager { gap: 2px; }
    .issues-pager-num { min-width: 28px; height: 28px; padding: 0 .35rem; font-size: 13px; }
    .issues-pager-step { padding: 0 .5rem; }
    /* Title can wrap aggressively at word boundaries on phones,
       but should NEVER hyphen-break mid-word ("Deliver / y
       Stat…"). overflow-wrap: anywhere preserves whole words
       across line breaks; word-break stays normal. */
    .issues-list-row-title { overflow-wrap: anywhere; word-break: normal; }
}

/* ── Issue detail page (two-column layout) ───────────────── */

.issues-detail-page { max-width: 1280px; }

.issues-detail-title-row {
    margin: 0 0 24px;
    padding-bottom: 16px;
    border-bottom: 1px solid var(--gh-border-muted);
}
.issues-detail-title {
    /* Grow to fill the row, but never shrink below a readable measure:
       the toolbar wraps below rather than squeezing the title. min() so
       a narrow viewport still caps at 100% (never overflows). */
    flex: 1 1 auto;
    min-width: min(100%, 34rem);
    /* Also a flex CONTAINER for its own children (the title text + the
       inline vote pill): baseline-align them with a small gap, wrapping
       the pill under a long title. This is the SINGLE source for the
       detail title - the later Detail-page duplicate that overrode it to
       21px/700 (and inverted the responsive ramp below) was removed. */
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: .5rem;
    margin: 0;
    font-size: 32px;
    font-weight: 400;
    line-height: 1.25;
    color: var(--gh-fg-default);
    letter-spacing: -.01em;
    /* Break only genuinely over-long words (URLs, long tokens);
       ordinary words wrap at their boundaries rather than snapping
       mid-word ("sugges-tion") the way the legacy word-break did. */
    overflow-wrap: break-word;
    word-break: normal;
}
.issues-detail-title-text { font-weight: 400; min-width: 0; }
/* Header action buttons: the Edit ghost (the New ticket door lives in
   the module head now - one primary for the container). */
.btn-ghost-action {
    display: inline-flex; align-items: center; justify-content: center;
    height: 32px;
    padding: 0 14px;
    font-size: 14px;
    font-weight: 500;
    line-height: 20px;
    border-radius: 6px;
    text-decoration: none;
    cursor: pointer;
    white-space: nowrap;
    transition: background-color .12s, border-color .12s;
}
.btn-ghost-action {
    color: var(--gh-fg-default);
    background: var(--gh-btn-bg);
    border: 1px solid var(--gh-btn-border);
}
.btn-ghost-action:hover { background: var(--gh-btn-hover); color: var(--gh-fg-default); }

.issues-detail-meta {
    margin-top: 12px;
    display: flex; align-items: center; gap: 8px;
    flex-wrap: wrap;
    font-size: 14px;
    color: var(--gh-fg-muted);
    /* Zero-CLS: the page-presence stack reveals its 26px avatar
       bubbles in this row after the roster poll - reserve the
       bubble envelope so the reveal never grows the row. */
    min-height: 26px;
}
.issues-detail-meta-text strong {
    color: var(--gh-fg-default);
    font-weight: 600;
}

/* Inline edit form on the issue post: the title field, then the canonical
   markdown editor (its own toolbar, floor and verbs - issue #494). */
.issues-detail-edit-form {
    display: flex; flex-direction: column; gap: 12px;
    background: var(--gh-canvas);
    border: 1px solid var(--gh-border);
    border-radius: 6px;
    padding: 16px;
}
.issues-detail-edit-title { font-size: 18px; font-weight: 600; }

/* Two-column grid: thread + sidebar */
.issues-detail-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 296px;
    gap: 32px;
    align-items: stretch;
    /* Grid takes the remaining vertical space inside .issues-page
       (which is a flex column that doesn't scroll). The grid itself
       must not scroll - its children handle scrolling so the header
       bar stays pinned. */
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;
    padding-bottom: 0;
}
.issues-detail-main {
    min-width: 0;
    /* Thread + composer column scrolls. Internal bottom padding is
       intentionally small - just enough breathing room beneath the
       last action row. The PAGE-level bottom buffer (.issues-page
       padding-bottom below) handles the gap from the scrollbar end
       to the page footer; we don't double up here, otherwise the
       scrollbar would extend hundreds of pixels below the visible
       content.

       Right-side breathing room: 14px keeps the scrollbar lane
       away from the comment chrome (edit / copy icons) so the
       scrollbar doesn't kiss the rightmost button. */
    overflow-y: auto;
    padding-bottom: 16px;
    padding-right: 14px;
    /* Auto-hiding overlay scrollbars. Same idle-hide behavior macOS /
       MAUI ship by default: thin transparent track, thumb only
       appears on hover/scroll, fades out when idle. Cross-browser
       via WebKit pseudo-elements + the standard scrollbar-* props. */
    transition: scrollbar-color .25s ease;
}
.issues-detail-main:hover,
.issues-detail-main:focus-within {
}
.issues-detail-main::-webkit-scrollbar { width: 10px; height: 10px; }
.issues-detail-main::-webkit-scrollbar-track { background: transparent; }
.issues-detail-main::-webkit-scrollbar-thumb {
    border-radius: 999px;
    border: 2px solid transparent;
    transition: background-color .25s ease;
}
.issues-detail-main:hover::-webkit-scrollbar-thumb,
.issues-detail-main:focus-within::-webkit-scrollbar-thumb {
}
.issues-detail-main::-webkit-scrollbar-thumb:hover {
    background-color: rgba(140, 149, 159, .85);
}
.issues-detail-sidebar-wrap {
    /* Wrapper used by the right-rail sidebar so it can have its own
       scroll if the section list ever grows past viewport height
       (Type + Assignees + Labels currently fit easily). */
    overflow-y: auto;
    padding-bottom: 16px;
}
/* Page-level bottom buffer so the scrollbar end sits comfortably
   above the footer instead of touching the edge. The grid takes
   the remaining height MINUS this buffer so the scroll region is
   bounded; tuned to feel like a finished page, not a window pane. */
.issues-page { padding-bottom: 8px; }

/* About-this-board panel - optional markdown blurb rendered above
   the issue list. Renders as bare prose on the page canvas, not a
   tinted card: chrome competed visually with the issue rows below
   and read as "from a different app" against the rest of the new
   restrained surface system. The prose itself + the H1 hierarchy
   carry the section without a fill or border. The pencil edit
   affordance still anchors to this element's top-right; we keep
   `position: relative` for that, just nothing painted around it.
   Edit mode (textarea form) gets its own bordered surface via the
   `.is-editing` modifier below so the input still reads as an
   input. */
/* A LABELED quiet panel in read mode too (user 2026-07-29: the
   unpainted prose burned as raw text above the board). Same
   translucent hairline + restrained shadow grammar as every card,
   no tinted fill; the kicker head says what the prose IS. */
.issues-about-card {
    position: relative;
    margin: 18px 0 14px;
    padding: 14px 16px;
    background: var(--surface);
    border: 1px solid rgba(0, 0, 0, .06);
    border-radius: 10px;
    box-shadow:
        0 1px 2px rgba(15, 23, 42, .04),
        0 8px 24px -8px rgba(15, 23, 42, .06);
    color: var(--gh-fg-default);
    font-size: 14px;
    line-height: 1.55;
}
html[data-theme="dark"] .issues-about-card {
    border-color: rgba(255, 255, 255, .08);
    box-shadow:
        0 1px 2px rgba(0, 0, 0, .35),
        0 8px 24px -8px rgba(0, 0, 0, .45);
}
.issues-about-head {
    display: block;
    margin-bottom: 6px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--text-subtle);
}
/* THE "About this board" panel (issue #450; owner, 2026-09-13: the head's popover was "a big
   block of text thrown to the user"): reading matter on THE slide-over. The guidelines read as
   one block (14px on a 1.6 line, paragraphs a breath apart), then the facts under their head,
   then the health figures; each section stands 22px under a hairline. The head's pencil is the
   guidelines' editor door, a 30px hairline square beside the close. */
.issues-about-panel-glyph { display: inline-grid; place-items: center; width: 30px; height: 30px; flex: 0 0 auto; border-radius: 8px; color: var(--text-muted); background: color-mix(in srgb, var(--text) 5%, transparent); }
.issues-about-panel-edit { margin-inline-start: auto; display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 30px; flex: 0 0 auto; border: 1px solid var(--border); border-radius: 8px; color: var(--text-muted); text-decoration: none; transition: color .12s ease-out, border-color .12s ease-out; }
.issues-about-panel-edit:hover { color: var(--text); border-color: color-mix(in srgb, var(--text) 25%, transparent); text-decoration: none; }
.issues-about-panel-edit:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
.issues-about-panel-edit + .bf-slideover-close { margin-inline-start: 0; }
.issues-about-panel-body { padding-top: 0.25rem; }
.issues-about-panel .issues-about-panel-prose { font-size: 14px; line-height: 1.6; color: var(--text); }
.issues-about-panel .issues-about-panel-prose :is(h1, h2, h3, h4) { margin: 0 0 8px; padding: 0; border: 0; font-size: 15px; font-weight: 600; line-height: 1.35; letter-spacing: -0.005em; color: var(--text); }
.issues-about-panel .issues-about-panel-prose :is(ul, ol) { margin: 0 0 10px; padding-left: 20px; }
.issues-about-panel .issues-about-panel-prose > :last-child { margin-bottom: 0; }
.issues-about-panel-section { margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--border); }
.issues-about-panel-body > .issues-about-panel-section:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.issues-about-panel-section .issues-about-head { margin-bottom: 8px; }
.issues-about-panel .bf-discipline { margin: 0; }
.issues-about-panel .issues-health { margin: 0; }
/* The board's ownership health (issue #450, P3): three hairline units, glyph +
   figure + word, 8px apart, the doors washing under the pointer. */
.issues-health { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 10px; }
.issues-health-unit { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 10px; border: 1px solid var(--border); border-radius: 999px; font-size: 12px; color: var(--text-muted); text-decoration: none; white-space: nowrap; }
.issues-health-unit strong { font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; }
.issues-health-unit svg { flex: none; color: var(--text-muted); }
a.issues-health-unit:hover { background: color-mix(in srgb, var(--text) 4%, transparent); color: var(--text); text-decoration: none; }
.issues-about {
    overflow-wrap: anywhere;
    word-break: break-word;
}
.issues-about > :first-child { margin-top: 0; }
.issues-about > :last-child { margin-bottom: 0; }
/* Discrete pencil icon in the top-right corner of the rendered
   About panel. Reveals on hover so the prose stays clean. */
/* Phone-only chrome toggles (revealed by issues-responsive.css
   below 720px): one button folds the filter row, one reopens the
   two-line-clamped board description. Desktop never renders them. */
.issues-filters-toggle { display: none; }
/* Keyboard rings for the fold toggles + the sub-issues triggers -
   quiet buttons still owe a visible focus state. */
.issues-filters-toggle:focus-visible,
.issues-subissues-add > summary:focus-visible,
.issues-subissues-new:focus-visible,
.issues-rel-remove:focus-visible {
    outline: 2px solid var(--gh-accent-emphasis);
    outline-offset: 2px;
}
.issues-about-edit-icon {
    position: absolute;
    top: 8px;
    right: 10px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px; height: 28px;
    color: var(--gh-fg-muted);
    background: transparent;
    border-radius: 6px;
    text-decoration: none;
    opacity: 0;
    transition: opacity .12s, background .12s, color .12s;
}
.issues-about-card:hover .issues-about-edit-icon,
.issues-about-edit-icon:focus-visible {
    opacity: 1;
}
.issues-about-edit-icon:hover {
    background: var(--gh-canvas);
    color: var(--gh-fg-default);
}
/* Empty-state CTA: discrete inline link, NOT a card. Single line,
   muted color, tiny + icon. Becomes a card only after the user
   types something. */
.issues-about-cta {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    margin: 12px 0 0;
    padding: 0;
    color: var(--gh-fg-muted);
    text-decoration: none;
    font-size: 13px;
    line-height: 1;
}
.issues-about-cta:hover {
    color: var(--gh-accent-fg);
    text-decoration: underline;
    text-underline-offset: 2px;
}
.issues-about-cta svg { opacity: .8; }
.issues-about-form {
    display: flex;
    flex-direction: column;
    gap: 14px;
    padding-right: 0;
    padding-top: 6px;
}
/* Edit mode hides the entire floating top-right tool cluster
   (pencil + close X). Both are redundant while editing: the
   pencil flips a state we're already in, and the close X is
   a destructive escape that bypasses the form's explicit
   Cancel button. The form's footer owns Save / Cancel. This
   keeps the textarea's borders clean to its top-right corner
   and forces the user to consciously cancel or save. */
.issues-about-dialog.is-editing .issues-about-dialog-tools {
    display: none;
}
.issues-about-textarea {
    width: 100%;
    min-height: 160px;
    padding: 12px 14px;
    font-family: inherit;
    font-size: 15px;
    line-height: 1.6;
    color: var(--gh-fg-default);
    background: var(--gh-canvas);
    border: 1px solid var(--gh-border);
    border-radius: 10px;
    resize: vertical;
    overflow-wrap: anywhere;
    word-break: break-word;
    transition: border-color .15s ease, box-shadow .15s ease;
}
.issues-about-textarea:focus {
    outline: none;
    border-color: var(--gh-accent-emphasis, #1f6feb);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--gh-accent-emphasis, #1f6feb) 22%, transparent);
}
.issues-about-actions {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    padding-top: 4px;
    border-top: 1px solid var(--gh-border-muted);
    margin-top: 2px;
    padding-top: 14px;
}
.issues-about-hint { color: var(--gh-fg-muted); font-style: normal; font-size: 13px; }
.issues-about-buttons { display: inline-flex; gap: 8px; align-items: center; }

/* About textarea inside the workspace permissions modal. Block-row
   variant of .perm-row so the textarea spans the full width. */
.perm-row.perm-row-block {
    display: flex;
    flex-direction: column;
    gap: 6px;
    align-items: stretch;
    cursor: default;
}
.perm-row.perm-row-block .perm-textarea {
    width: 100%;
    min-height: 96px;
    padding: 10px 12px;
    font-family: inherit;
    font-size: 14px;
    line-height: 1.5;
    color: var(--gh-fg-default);
    background: var(--gh-canvas);
    border: 1px solid var(--gh-border);
    border-radius: 6px;
    resize: vertical;
}
.perm-row.perm-row-block .perm-textarea:focus {
    outline: none;
    border-color: var(--gh-accent-emphasis, #1f6feb);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--gh-accent-emphasis, #1f6feb) 25%, transparent);
}
.perm-row.perm-row-block .perm-hint {
    color: var(--gh-fg-muted);
    font-size: 13px;
    font-style: normal;
}

/* Thread - one item = avatar + bubble. Wider gap between sibling
   replies than the standard list gap so each reply gets visible
   breathing room and the thread reads as a sequence of distinct
   messages, not a wall of stacked cards. */
.issues-thread {
    position: relative;
    display: flex; flex-direction: column;
    gap: 28px;
}

.issues-thread-item {
    position: relative;
    display: flex; gap: 16px;
    align-items: flex-start;
    z-index: 1;
}
/* Deep-link target: a result (or an edit / resolve redirect) that points at #comment-{id} scrolls the
   comment into view (issue-comment-anchor.js) and pulses an accent ring on its bubble, so the user sees
   WHICH answer matched. scroll-margin clears the sticky topbar; the pulse fades fully to transparent, so
   there is no persistent fill. */
.issues-thread-item, .issues-thread-reply { scroll-margin-top: 90px; }
.issues-thread-item.is-deep-link-target .issues-thread-bubble,
.issues-thread-reply.is-deep-link-target .issues-thread-reply-bubble {
    animation: bfCommentTargetPulse 2s ease-out;
}
@keyframes bfCommentTargetPulse {
    0%   { box-shadow: 0 0 0 3px color-mix(in srgb, var(--gh-accent-emphasis, #0969da) 70%, transparent); }
    55%  { box-shadow: 0 0 0 3px color-mix(in srgb, var(--gh-accent-emphasis, #0969da) 32%, transparent); }
    100% { box-shadow: 0 0 0 3px transparent; }
}
.issues-thread-avatar {
    flex-shrink: 0;
    width: 40px; height: 40px;
    display: inline-flex; align-items: center; justify-content: center;
    border-radius: 50%;
    overflow: hidden;
    /* Reads `var(--avatar-bg)` so the per-user satin gradient
       written by `--avatar-hue` (set inline by the canonical
       AvatarHelper) shows through. The earlier neutral
       `var(--gh-canvas)` painted every thread bubble plain
       white regardless of user. */
    background: var(--avatar-bg);
    text-decoration: none;
}
.issues-avatar-img,
.issues-thread-avatar img,
.issues-thread-avatar > * {
    width: 40px; height: 40px;
    border-radius: 50%;
    object-fit: cover;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 16px;
    font-weight: 600;
    /* Same canonical avatar background variable - the inline
       `--avatar-hue` from the helper resolves to a per-user hue. */
    background: var(--avatar-bg);
    color: var(--avatar-color);
}
/* Imported-from-GitHub avatar: a generic GitHub-mark bubble
   that stands in for the original GH author whose Burofolk
   identity we don't have. The mark sits in a neutral muted
   circle so it reads as "external source" instead of "user
   X" - the per-user hue gradient would mis-attribute the
   row to the importer. */
.issues-thread-avatar-imported {
    background: var(--gh-canvas-subtle);
    border: 1px solid var(--gh-border);
    color: var(--gh-fg-muted);
}
/* The cascade trap: `.issues-thread-avatar > *` paints every
   child with the per-user avatar gradient (purple by default
   when --avatar-hue isn't set inline) AND sets color to
   var(--avatar-color) (white). Both clobber our GH-mark
   intent: white path on purple SVG on light-gray wrapper =
   invisible mark.
   Override BOTH on the SVG: bare `transparent` (no !important
   so the AvatarSingleSourceOfTruthTests pin allows it; same
   specificity as the parent rule + later in source = wins) for
   the background, and the muted GH foreground for the path
   fill. The wrapper keeps its --gh-canvas-subtle pill colour. */
.issues-thread-avatar-imported svg {
    width: 22px; height: 22px;
    background: transparent;
    color: var(--gh-fg-default) !important;
    fill: var(--gh-fg-default) !important;
}
.issues-thread-avatar-imported svg path {
    fill: var(--gh-fg-default) !important;
}
/* Imported-author label: subtly italic + muted weight so it
   reads as a GitHub login (e.g. "@octocat") rather than a
   Burofolk display name. Keeps the same size as the regular
   author strong so the bubble header doesn't shift. */
.issues-thread-bubble-author-imported {
    font-style: italic;
    color: var(--gh-fg-default);
    font-weight: 600;
}
.issues-thread-bubble {
    flex: 1; min-width: 0;
    background: var(--gh-canvas);
    border: 1px solid var(--gh-border);
    border-radius: 6px;
    overflow: hidden;
    position: relative;
}

.issues-thread-bubble-head {
    display: flex; align-items: center; justify-content: space-between;
    padding: 10px 16px;
    background: var(--gh-canvas-subtle);
    border-bottom: 1px solid var(--gh-border);
    font-size: 14px;
    color: var(--gh-fg-muted);
    gap: 8px;
}
.issues-thread-bubble-author { display: inline-flex; align-items: baseline; gap: 6px; flex-wrap: wrap; }
.issues-thread-bubble-author strong { color: var(--gh-fg-default); font-weight: 600; }
/* Author name carrying data-user-profile-id behaves like a link
   (cursor + hover underline) so the profile-card affordance is
   visible without changing layout. */
.issues-thread-bubble-author strong[data-user-profile-id],
.issues-detail-meta-text strong[data-user-profile-id] { cursor: pointer; }
.issues-thread-bubble-author strong[data-user-profile-id]:hover,
.issues-detail-meta-text strong[data-user-profile-id]:hover { text-decoration: underline; text-underline-offset: 2px; }
.issues-thread-bubble-meta { color: var(--gh-fg-muted); font-size: 14px; }
.issues-thread-bubble-actions {
    display: inline-flex; align-items: center; gap: 4px;
    flex-shrink: 0;
}
/* Language chips row - a dedicated full-width strip between the
   bubble head and the body, right-aligned like the Mail message
   card. Reserves the one-chip-row height (28px) from the first
   frame so ai-lang.js paints the chips into this strip in place
   after the async status fetch - the body below NEVER moves
   (zero-CLS rule; same contract as .wiki-article-lang-row /
   .msg-lang-row in ai-lang.css). Spacing is margin, not padding,
   so min-height equals the chip height exactly. */
.issues-bubble-lang-row {
    display: flex;
    justify-content: flex-end;
    margin: 12px 16px 0;
    /* Reserve the segmented control's height (28px chips) from the first
       frame so the async chip paint never shifts the thread (zero-CLS).
       Moves in lockstep with .bf-lang-chip height in ai-lang.css. */
    min-height: 28px;
}
/* Collapse the strip ONLY when the Translation feature is off and
   no chip marker is rendered at all - never while the status fetch
   is merely in flight (an in-flight collapse would un-hide after
   the fetch and shift the whole thread). */
.issues-bubble-lang-row:not(:has(.bf-lang)) {
    display: none;
}

/* Replies are indented under the root so the thread structure is
   immediately legible. The vertical guide rail mimics the GitHub
   review-thread pattern - one rule from the avatar's column down
   to the reply composer. */
.issues-thread-replies {
    margin-left: 56px;        /* aligns with the right edge of the root avatar column */
    padding-left: 20px;
    border-left: 2px solid var(--gh-border-muted, var(--gh-border));
    margin-top: 8px;
    margin-bottom: 16px;
    display: flex;
    flex-direction: column;
    gap: 12px;
}
.issues-thread-reply {
    display: grid;
    grid-template-columns: 28px 1fr;
    gap: 8px;
    align-items: flex-start;
}
.issues-thread-avatar-sm,
.issues-avatar-sm { width: 28px; height: 28px; }
.issues-thread-reply-bubble {
    background: var(--gh-canvas);
    border: 1px solid var(--gh-border);
    border-radius: 6px;
    padding: 8px 12px;
    min-width: 0;
}
.issues-thread-reply-head {
    display: flex; align-items: baseline; gap: 6px;
    font-size: 13px;
    margin-bottom: 4px;
}
.issues-thread-reply-head strong {
    color: var(--gh-fg-default);
    font-weight: 600;
}
/* Edit / delete affordances on a reply sit at the right edge of the slim
   reply header, vertically centered against the baseline-aligned name + meta. */
.issues-thread-reply-head .issues-thread-reply-actions { margin-left: auto; align-self: center; }
.issues-thread-reply-body {
    font-size: 14px;
    line-height: 1.5;
}
.issues-thread-reply-body > *:last-child { margin-bottom: 0; }
/* Inline "Reply" disclosure under each thread. <details> is the
   no-JS toggle; the form is hidden until the user opens it. */
.issues-thread-reply-composer {
    margin-top: 4px;
}
.issues-thread-reply-toggle {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 4px 10px;
    font-size: 13px;
    color: var(--gh-fg-muted);
    background: transparent;
    border: 1px solid var(--gh-border);
    border-radius: 6px;
    cursor: pointer;
    user-select: none;
    list-style: none;
}
.issues-thread-reply-toggle::-webkit-details-marker { display: none; }
.issues-thread-reply-toggle:hover {
    color: var(--gh-fg-default);
    background: var(--gh-canvas-subtle);
}
.issues-thread-reply-form {
    margin-top: 8px;
    display: flex;
    flex-direction: column;
    gap: 8px;
}
/* No per-reply textarea override. The reply composer authors through
   the canonical shared markdown editor (.bf-md-editor /
   .bf-md-editor-textarea in markdown-editor.css), exactly like the
   "Add a comment" composer, so it inherits that editor's sizing
   (200px min-height, auto-grow, internal scroll past the cap),
   borderless-inside-the-shell chrome, and prose font. The earlier
   64px / bordered / resize:vertical override made the reply diverge
   from every other composer (cramped height, double border, a manual
   resize handle the auto-grow path makes redundant). */
.issues-thread-reply-actions {
    display: flex; justify-content: flex-end;
}
.issues-bubble-edit {
    display: inline-flex; align-items: center; justify-content: center;
    width: 28px; height: 28px;
    background: transparent;
    border: 1px solid transparent;
    border-radius: 6px;
    color: var(--gh-fg-muted);
    cursor: pointer;
    transition: background-color .12s, color .12s;
}
.issues-bubble-edit:hover { background: var(--gh-canvas); color: var(--gh-fg-default); border-color: var(--gh-border); }
.issues-bubble-edit-danger:hover { color: var(--gh-danger-fg); }
.issues-bubble-delete-form { display: inline; margin: 0; }

/* Issue thread bubble SHELL - padding + reading typography only.
   Every markdown element inside (p, h*, ul, ol, blockquote, code,
   pre, a, table, …) is rendered by the canonical `.markdown-body`
   block higher up in this file. Issue comments already mount the
   class (`<div class="issues-thread-bubble-body markdown-body">`)
   so the canonical rules attach automatically. The font-size on
   the shell determines the proportional scale of every nested
   element via em-based sizing. */
.issues-thread-bubble-body {
    padding: 16px;
    line-height: 1.5;
    font-size: 14px;
    cursor: default;
    user-select: text;
    max-width: 100%;
}
.issues-empty-body { color: var(--gh-fg-muted); }

/* standard-shape flat comment composer.
   No card shell wrapping the editor; the avatar floats on the left
   and the composer sits to the right with just a small label header
   above the editor. The editor itself owns its border + focus ring
   (styled by the shared .bf-md-editor in markdown-editor.css). The
   actions row is part of the composer, not nested inside a bubble. */
.issues-compose {
    flex: 1; min-width: 0;
    display: flex; flex-direction: column;
    gap: 0;
}
.issues-compose-label {
    font-size: 14px;
    font-weight: 600;
    color: var(--gh-fg-default);
    margin-bottom: 8px;
}
.issues-compose-state-btn {
    color: var(--gh-fg-default);
    background: var(--gh-btn-bg);
    border: 1px solid var(--gh-btn-border);
}
.issues-compose-state-btn:hover { background: var(--gh-btn-hover); }

/* In-place comment / reply editor. Hosts the canonical .bf-md-editor
   (Pages/Issues/_IssueCommentEditForm.cshtml), so the textarea inherits
   that composer's sizing (200px min-height, auto-grow) + prose font - the
   old bare-textarea monospace / resize override is gone so editing matches
   creating exactly. Padding is scoped per host below so the box lands where
   the rendered body sat and the swap doesn't shift its position. */
.issues-comment-edit-form { display: flex; flex-direction: column; gap: 12px; }
/* Root comment: the bubble itself has no padding (its body block carries
   16px), so the editor supplies its own to line up with the rendered body. */
.issues-thread-bubble > .issues-comment-edit-form { padding: 16px; }
/* Reply: the slim reply bubble is already padded (8px 12px), so the editor
   only needs a small top gap under the header. */
.issues-thread-reply-bubble > .issues-comment-edit-form { padding: 4px 0 0; }

/* Sidebar.
   Section vertical rhythm is tight by design: workspaces with
   many custom fields (Assignees + Type + 8+ custom + Labels)
   would otherwise overflow even at large viewports, forcing
   the sticky sidebar into its internal scroll on every page
   render. Picking 10px padding + 6px header gap saves ~150px
   of total height against a 12-section sidebar - enough to
   fit comfortably inside the sticky max-height on a 900px
   viewport without scrolling. */
.issues-detail-sidebar {
    display: flex; flex-direction: column;
    gap: 0;
    padding-top: 4px;
}
.issues-detail-side-section {
    border-bottom: 1px solid var(--gh-border-muted);
    padding: 8px 0;
}
.issues-detail-side-section:first-child { padding-top: 0; }
.issues-detail-side-section:last-child { border-bottom: 0; }

/* ── Notifications control (subscribe / unsubscribe) ──────────────
   A per-viewer toggle in the sidebar. The button is a bordered,
   full-width action (no filled background - state is carried by the
   icon + the reason line, per the no-filled-bg-on-active rule). When
   subscribed it takes a subtle accent border + accent icon. */
.issue-subscribe { display: flex; flex-direction: column; gap: 8px; }
.issue-subscribe-form { margin: 0; }
.issue-subscribe-btn {
    width: 100%;
    display: inline-flex; align-items: center; justify-content: center; gap: 7px;
    padding: 6px 12px;
    min-height: 30px;
    font-size: 13px; font-weight: 600;
    color: var(--gh-fg-default);
    background: var(--gh-canvas);
    border: 1px solid var(--gh-border);
    border-radius: 6px;
    cursor: pointer;
    transition: background .12s ease, border-color .12s ease, color .12s ease, opacity .12s ease;
}
.issue-subscribe-btn:hover { background: var(--gh-canvas-subtle); border-color: var(--gh-fg-muted); }
.issue-subscribe-btn:focus-visible { outline: 2px solid var(--gh-accent-fg); outline-offset: 1px; }
.issue-subscribe-btn.is-on { border-color: var(--gh-accent-emphasis, var(--gh-accent-fg)); color: var(--gh-accent-fg); }
.issue-subscribe-btn.is-on .issue-subscribe-icon { color: var(--gh-accent-fg); }
.issue-subscribe-btn.is-busy { opacity: .6; pointer-events: none; }
.issue-subscribe-icon { flex-shrink: 0; color: var(--gh-fg-muted); }
.issue-subscribe-btn-label { white-space: nowrap; }
.issue-subscribe-reason {
    margin: 0;
    font-size: 12px;
    line-height: 1.45;
    color: var(--gh-fg-muted);
}
.issue-subscribe-watchers {
    display: flex; align-items: center; gap: 8px;
    flex-wrap: wrap;
}
.issue-subscribe-avatars { display: inline-flex; align-items: center; }
.issue-subscribe-avatars .issues-mini-avatar { margin-right: -6px; box-shadow: 0 0 0 2px var(--gh-canvas); border-radius: 50%; }
.issue-subscribe-avatars .issues-mini-avatar:last-child { margin-right: 0; }
/* Named WITHOUT "avatar": it is a text chip beside the avatar stack, and the
   avatar single-source-of-truth pin forbids typography on avatar-named
   selectors (they must never shadow the canonical avatar type). */
.issue-subscribe-overflow-count {
    margin-left: 2px;
    font-size: 11px; font-weight: 600;
    color: var(--gh-fg-muted);
}
.issue-subscribe-count { font-size: 12px; color: var(--gh-fg-muted); }
.issue-subscribe-count strong { color: var(--gh-fg-default); font-weight: 600; }
.issues-detail-side-head {
    display: flex; align-items: center; justify-content: space-between;
    font-size: 12px;
    font-weight: 600;
    color: var(--gh-fg-muted);
    text-transform: none;
    margin-bottom: 4px;
}
.issues-detail-side-head > span { font-size: 12px; font-weight: 600; color: var(--gh-fg-muted); }
.issues-detail-side-picker { position: relative; }
.issues-detail-side-picker summary {
    list-style: none;
    cursor: pointer;
    width: 24px; height: 24px;
    display: inline-flex; align-items: center; justify-content: center;
    color: var(--gh-fg-muted);
    border-radius: 6px;
}
.issues-detail-side-picker summary::-webkit-details-marker { display: none; }
.issues-detail-side-picker summary:hover { background: var(--gh-canvas-subtle); color: var(--gh-fg-default); }
.issues-detail-side-pop {
    /* `fixed` rather than `absolute` so the popover escapes the
       sticky sidebar's `overflow-y: auto` scroll/clip context. An
       absolutely-positioned descendant is clipped to the nearest
       scroll container's content box; a `fixed` element is
       positioned against the viewport and immune to that clip.
       JS in issues-ui.js sets `top` / `right` (in viewport coords)
       from the trigger's bounding rect on `<details>` toggle. The
       fallback values below place the card off-screen so a missed
       JS positioning doesn't leak a card stuck in the corner. */
    position: fixed;
    top: -9999px;
    right: -9999px;
    /* The popover tier (.bf-popover: 1080), not a page-level 50: the markdown
       editor under the thread paints at 50 too, and since the properties column
       precedes the thread in the markup (2026-09-12) a tie went to the editor - a
       Hand over popover sat UNDER the comment box at 1280px. */
    z-index: 1080;
    width: 320px;
    max-height: 480px;
    background: var(--gh-canvas);
    border: 1px solid var(--gh-border);
    border-radius: 12px;
    box-shadow: 0 8px 24px rgba(31, 35, 40, .18);
    overflow: hidden;
    display: flex; flex-direction: column;
}
.issues-detail-side-pop-head {
    padding: 10px 14px;
    font-size: 13px;
    font-weight: 600;
    color: var(--gh-fg-default);
    background: var(--gh-canvas-subtle);
    border-bottom: 1px solid var(--gh-border);
}
/* "Clear filter" row inside a column-filter popover. Painted
   muted so it reads as a secondary action - the primary action
   in the popover is still picking an option below. */
.issues-detail-side-pop-clear {
    display: flex; align-items: center; gap: 8px;
    padding: 8px 14px;
    font-size: 12px;
    color: var(--gh-fg-muted);
    text-decoration: none;
    border-bottom: 1px solid var(--gh-border-muted);
}
.issues-detail-side-pop-clear:hover {
    background: var(--gh-canvas-subtle);
    color: var(--gh-fg-default);
}
.issues-detail-side-pop-clear svg { flex-shrink: 0; }
/* Active-filter readout for non-SingleSelect cf types
   (text / number / date) where the popover doesn't list
   pickable options - it just shows what's currently
   filtered so the user knows what they're clearing. */
.issues-detail-side-pop-active {
    padding: 10px 14px;
    display: flex; flex-direction: column; gap: 2px;
}
.issues-detail-side-pop-active-label {
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .05em;
    color: var(--gh-fg-muted);
}
.issues-detail-side-pop-active-value {
    font-size: 13px;
    color: var(--gh-fg-default);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* ── Shared picker scroll-pane primitive ────────────────────────
   The SINGLE source of truth for the scroll behaviour of every issue
   picker popover list: the New/Detail sidebar pickers + list-page
   filter dropdowns + custom-field input (.issues-detail-side-pop-list)
   and the list-row inline picker (.issues-inline-edit-pop-list). Each
   card is a bounded `display:flex; flex-direction:column;
   overflow:hidden` box; its option list must be able to shrink below
   its content so its own overflow-y scrolls the roster inside the card
   instead of overflowing and leaving unreachable dead space.
   min-height:0 overrides the flex default (auto) that causes that.
   Defined ONCE here - every picker list carries this class - so the
   surfaces can never drift apart again. Pinned by
   IssuePickerPopoverScrollContainmentTests. */
.issues-pop-scroll {
    min-height: 0;
    overflow-y: auto;
}
.issues-detail-side-pop-list {
    list-style: none; padding: 0; margin: 0;
    max-height: 360px;
}
.issues-detail-side-pop-list li { border-bottom: 1px solid var(--gh-border-muted); }
.issues-detail-side-pop-list li:last-child { border-bottom: 0; }
.issues-detail-side-pop-list form { margin: 0; }
/* Labels / Type / Assignee picker rows.
   Two-column layout: check + chip left, description right. The
   check column is fixed width so the row's chip column starts at
   the same x-coordinate regardless of whether the row is selected
   or not. Description gets `flex: 1` and `min-width: 0` so it
   wraps cleanly inside the popover's max-width without pushing
   the chip column. */
.issues-detail-side-pop-opt {
    display: flex; align-items: center; gap: 10px;
    width: 100%;
    padding: 7px 12px;
    background: transparent;
    border: 0;
    text-align: left;
    cursor: pointer;
    color: var(--gh-fg-default);
    font-size: 13px;
    line-height: 1.4;
    transition: background-color .1s;
    /* The row is rendered as an <a> so the whole pill is
       keyboard-navigable + middle-click-able. Anchors default
       to text-decoration: underline, which bled through onto
       the label-chip text inside ("Portal", "Portal 2.0",
       ...). Disable it explicitly so the chips read as
       chips, not as a list of links. */
    text-decoration: none;
}
.issues-detail-side-pop-opt:hover { background: var(--gh-canvas-subtle); }
.issues-detail-side-pop-opt > .issues-label-chip,
.issues-detail-side-pop-opt > .issues-type-icon {
    flex-shrink: 0;
}
.issues-detail-side-pop-opt .issues-detail-side-pop-meta {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.issues-detail-side-pop-opt > .issues-type-icon { margin-top: 1px; }
.issues-pop-check {
    flex-shrink: 0;
    width: 12px; height: 12px;
    margin-top: 5px;
    color: var(--gh-fg-default);
    visibility: hidden;
}
.is-on .issues-pop-check { visibility: visible; }
/* Inside the type picker rows the label + description stack
   vertically. Description is muted small text. */
.issues-detail-side-pop-text {
    display: flex; flex-direction: column;
    gap: 2px;
    min-width: 0;
    flex: 1;
}
.issues-detail-side-pop-text strong {
    font-weight: 600;
    color: var(--gh-fg-default);
    line-height: 1.3;
}
.issues-detail-side-pop-text .issues-detail-side-pop-meta {
    font-size: 12px;
    color: var(--gh-fg-muted);
    line-height: 1.35;
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
}
.issues-detail-side-pop-opt .issues-mini-avatar {
    width: 20px !important; height: 20px !important;
    font-size: 10px !important;
}
.issues-detail-side-pop-text {
    display: flex; flex-direction: column;
    min-width: 0;
}
.issues-detail-side-pop-text strong { font-weight: 500; color: var(--gh-fg-default); }
.issues-detail-side-pop-meta {
    font-size: 12px;
    color: var(--gh-fg-muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Person picker (Assignees) - dedicated styling so the row reads
   as "name + email below" stacked, GitHub assignee-picker style. */
.issues-detail-side-pop-opt-person { gap: 10px; padding: 8px 14px; }
.issues-pop-avatar {
    width: 22px !important; height: 22px !important;
    flex-shrink: 0;
    border-radius: 50%;
    font-size: 11px !important;
    line-height: 22px;
}
.issues-detail-side-pop-person {
    display: flex; flex-direction: column;
    min-width: 0;
    flex: 1;
    gap: 1px;
}
.issues-detail-side-pop-name {
    font-size: 13px;
    font-weight: 600;
    color: var(--gh-fg-default);
    line-height: 1.3;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.issues-detail-side-pop-email {
    font-size: 12px;
    color: var(--gh-fg-muted);
    line-height: 1.3;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Filter row at top of person/label pickers. */
.issues-detail-side-pop-search {
    display: flex; align-items: center; gap: 8px;
    padding: 8px 12px;
    background: var(--gh-canvas);
    border-bottom: 1px solid var(--gh-border-muted);
    color: var(--gh-fg-muted);
}
/* Stroke-based magnifying glass: lighter weight than the prior
   filled glyph, opacity-tuned so it reads as a hint not a button. */
.issues-detail-side-pop-search-icon {
    flex-shrink: 0;
    opacity: .7;
    color: var(--gh-fg-muted);
}
.issues-detail-side-pop-filter {
    flex: 1;
    border: 0; outline: 0;
    background: transparent;
    font-size: 13px;
    color: var(--gh-fg-default);
    padding: 0;
}
.issues-detail-side-pop-filter::placeholder { color: var(--gh-fg-muted); }
.issues-detail-side-pop-noresults {
    margin: 0;
    padding: 12px 14px;
    color: var(--gh-fg-muted);
    font-size: 13px;
    font-style: italic;
    text-align: center;
}
.issues-detail-side-pop-empty {
    padding: 16px;
    color: var(--gh-fg-muted);
    font-size: 13px;
    margin: 0;
}
.issues-detail-side-pop-empty a { color: var(--gh-accent-fg); text-decoration: none; }
.issues-detail-side-pop-empty a:hover { text-decoration: underline; }
.issues-detail-side-pop-link {
    display: block;
    padding: 10px 14px;
    border-top: 1px solid var(--gh-border-muted);
    font-size: 12px;
    color: var(--gh-accent-fg);
    text-decoration: none;
    background: var(--gh-canvas-subtle);
}
.issues-detail-side-pop-link:hover { text-decoration: underline; }

.issues-detail-side-empty {
    margin: 0;
    color: var(--gh-fg-muted);
    font-size: 13px;
}

/* List-row relation tokens (issue #265): the parent ref, the sub-issue
   rollup, and one token per typed link relation - quiet numerals in the
   meta strip, same muted voice as the stage / comment tokens. The
   sentence ("Blocked by #14: Fix the importer") rides the tooltip, so
   the row stays scannable. All emitted by IssueRelationTokenRenderer. */
.issues-list-row-meta-rel {
    display: inline-flex; align-items: center; gap: 3px;
    color: var(--gh-fg-muted);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    cursor: help;
}
.issues-list-row-meta-rel svg { flex: 0 0 auto; }
/* Being blocked is the one relation that changes what the reader does
   next, so it earns the danger tone - text only, never a filled pill. */
.issues-list-row-meta-rel.is-blocked { color: var(--gh-danger-fg, #cf222e); }

/* ── Issue relationships (issue #265) ─────────────────────────────
   Parent / Linked-issues sidebar sections + the sub-issues panel.
   Rows share one grammar everywhere: state dot, muted #number, title
   link, optional trailing controls. State colors follow the list rows
   (open = success, closed = done-purple); the remove control is a
   static quiet button at the row end (never a hover-reveal over
   text). */
.issues-rel-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; }
.issues-rel-row {
    display: flex; align-items: center; gap: 7px;
    min-width: 0; padding: 3px 0;
    font-size: 13px;
}
/* State is carried by the SHARED glyph pair (_IssueStateGlyph:
   circle-dot = open, checked box = closed), never by hue alone -
   both state colors resolve to violets in this brand, which made
   the old 8px dots unreadable ("we have no idea what is closed").
   The wrapper colors the glyph and carries the state tooltip. */
.issues-rel-state {
    display: inline-flex;
    align-items: center;
    flex: 0 0 auto;
}
.issues-rel-state.is-open { color: var(--gh-success-emphasis); }
.issues-rel-state.is-closed { color: var(--gh-done-fg); }
.issues-rel-num {
    color: var(--gh-fg-muted);
    font-variant-numeric: tabular-nums;
    flex: 0 0 auto;
}
/* A linked issue from ANOTHER project names its home: "#7" means a
   different ticket in every workspace, so the bare numeral would be a lie
   here. It takes its OWN line under the ref rather than competing with the
   title for a narrow sidebar's width - a project name squeezed to
   "GedZil..." is a clipped signal, which is no signal. Muted running text,
   indented past the state glyph so it starts where the number above it
   does; no chip, no border, no fill - it is context, not a status. */
.issues-rel-row.is-elsewhere { flex-wrap: wrap; }
/* Only the project name may take a new line. A flex item wraps when its
   CONTENT width doesn't fit, so the title - which shrinks and ellipsises
   everywhere else - would jump to its own line the moment wrapping is
   allowed. A zero flex-basis keeps it on the ref's line and back to
   ellipsising, so the row is exactly two lines: the ref, then its home. */
.issues-rel-row.is-elsewhere .issues-rel-title { flex: 1 1 0%; }
.issues-rel-ws {
    order: 9;
    flex: 0 0 100%;
    /* 14px state glyph + the row's 7px gap. */
    padding-left: 21px;
    min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-size: 12px;
    color: var(--gh-fg-muted);
}
.issues-rel-title {
    flex: 1 1 auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    color: var(--gh-fg-default); text-decoration: none;
}
.issues-rel-title:hover { color: var(--gh-accent-fg); text-decoration: underline; }
.issues-rel-row.is-closed .issues-rel-title { color: var(--gh-fg-muted); }
.issues-rel-remove-form { margin: 0; flex: 0 0 auto; display: flex; }
.issues-rel-remove {
    display: inline-flex; align-items: center; justify-content: center;
    width: 20px; height: 20px; padding: 0;
    border: 0; border-radius: 6px; background: transparent;
    color: var(--gh-fg-muted); cursor: pointer;
}
.issues-rel-remove:hover { color: var(--gh-danger-fg, #cf222e); background: var(--gh-canvas-subtle); }
.issues-rel-group { margin: 0 0 6px; }
.issues-rel-group:last-child { margin-bottom: 0; }
.issues-rel-group-caption {
    font-size: 11px; color: var(--gh-fg-muted);
    margin: 4px 0 1px;
}

/* Picker popover internals: relation chips + fetched results. Chips are
   quiet bordered segments - the active one carries an accent border +
   accent text, never a filled background. */
.issues-rel-kinds {
    display: flex; flex-wrap: wrap; gap: 4px;
    padding: 8px 14px 0;
}
.issues-rel-kind {
    border: 1px solid var(--gh-border-default);
    background: transparent;
    color: var(--gh-fg-muted);
    border-radius: 999px;
    font-size: 12px;
    padding: 2.5px 9px;
    cursor: pointer;
    line-height: 1.4;
}
.issues-rel-kind:hover { color: var(--gh-fg-default); }
.issues-rel-kind.is-on {
    border-color: var(--gh-accent-fg);
    color: var(--gh-accent-fg);
}
/* Which project the link picker searches. A labelled row above the
   search box, separated by spacing rhythm rather than a rule, and
   content-sized: the chooser is as wide as the project names need, never
   stretched to the popover edge. The label and the control share one
   baseline, so the row reads as one control with its caption. */
.issues-rel-scope {
    display: flex; align-items: center; gap: 8px;
    flex-wrap: wrap;
    padding: 10px 14px 0;
    min-width: 0;
}
.issues-rel-scope-label {
    flex: 0 0 auto;
    font-size: 11px;
    color: var(--gh-fg-muted);
    margin: 0;
}
/* ONE height with the relation chips above - same font size, same
   line-height, same vertical padding: the project scope and the relation
   choice are two halves of the same "what am I linking" control, and two
   heights would read as two unrelated widgets. Declared for BOTH faces of
   the select (the pre-enhance native and the bf-select trigger), the way
   bf-select.css does, or the enhancement swap would shift the row. */
.issues-rel-scope-select:not(.bf-select-native),
.issues-rel-scope .bf-select-btn {
    font-size: 12px;
    line-height: 1.4;
    padding-top: 2.5px;
    padding-bottom: 2.5px;
}
/* Content-sized: a chooser is as wide as the project names need, never
   stretched to the popover's edge. */
.issues-rel-scope-select:not(.bf-select-native) {
    flex: 0 1 auto;
    width: auto;
    min-width: 0;
    max-width: 100%;
}
.issues-rel-scope .bf-select { flex: 0 1 auto; min-width: 0; max-width: 100%; }
.issues-rel-results { max-height: 260px; overflow-y: auto; }
.issues-rel-results[aria-busy="true"] { opacity: .55; }
.issues-rel-pop-empty {
    padding: 12px 14px;
    color: var(--gh-fg-muted);
    font-size: 13px;
    margin: 0;
}
.issues-rel-results-list { list-style: none; margin: 0; padding: 4px 0; }
.issues-rel-result {
    display: flex; align-items: center; gap: 6px;
    min-width: 0; padding: 4px 14px;
}
.issues-rel-result:hover { background: var(--gh-canvas-subtle); }
.issues-rel-result-main {
    display: flex; align-items: center; gap: 7px;
    flex: 1 1 auto; min-width: 0;
    text-decoration: none; color: var(--gh-fg-default);
    font-size: 13px;
}
.issues-rel-result-main .issues-rel-title { font-size: 13px; }
.issues-rel-result-form { margin: 0; flex: 0 0 auto; display: flex; }
.issues-rel-pick-btn {
    border: 1px solid var(--gh-border-default);
    background: var(--gh-canvas-default);
    color: var(--gh-fg-default);
    border-radius: 6px;
    font-size: 12px;
    padding: 2px 8px;
    cursor: pointer;
    white-space: nowrap;
}
.issues-rel-pick-btn:hover { border-color: var(--gh-accent-fg); color: var(--gh-accent-fg); }
.issues-rel-clear-opt { border-top: 1px solid var(--gh-border-muted); }

/* Sub-issues panel (main column, under the issue body): quiet card
   with an enumerated progress readout + a thin bar. It is a direct
   child of .issues-thread, so it must start at the BUBBLE column
   (40px avatar + 16px gap = 56px) - at full width it sits under the
   avatar rail and the positioned ::before timeline spine paints its
   2px line straight across the card. position+z-index lift it above
   the spine so the line can never draw through it. */
.issues-subissues {
    border: 1px solid var(--gh-border-default);
    border-radius: 10px;
    padding: 10px 14px 8px;
    margin: 12px 0 0 56px;
    background: var(--gh-canvas-default);
    position: relative;
    z-index: 1;
}
.issues-subissues-head {
    display: flex; align-items: center; gap: 10px;
    min-width: 0; margin-bottom: 4px;
}
.issues-subissues-title { font-size: 13px; font-weight: 600; color: var(--gh-fg-default); }
.issues-subissues-progress {
    font-size: 12px; color: var(--gh-fg-muted);
    flex: 0 0 auto; font-variant-numeric: tabular-nums;
}
.issues-subissues-bar {
    flex: 1 1 auto; min-width: 40px; height: 4px;
    border-radius: 2px; background: var(--gh-border-muted);
    overflow: hidden;
}
.issues-subissues-fill {
    display: block; height: 100%;
    border-radius: 2px;
    background: var(--gh-success-emphasis);
    /* The add/remove swaps re-render this bar; easing the width
       makes progress read as movement instead of a jump cut. */
    transition: width .25s cubic-bezier(.22, .61, .36, 1);
}
.issues-subissues-add { position: relative; flex: 0 0 auto; }
.issues-subissues-add > summary {
    display: inline-flex; align-items: center; gap: 5px;
    list-style: none; cursor: pointer;
    border: 1px solid var(--gh-border-default);
    border-radius: 6px;
    background: transparent;
    color: var(--gh-fg-muted);
    font-size: 12px;
    padding: 2.5px 8px;
    white-space: nowrap;
}
.issues-subissues-add > summary::-webkit-details-marker { display: none; }
.issues-subissues-add > summary:hover { color: var(--gh-fg-default); border-color: var(--gh-fg-muted); }
/* "New sub-issue" (issue #265 v3): an anchor twin of the add trigger -
   same quiet bordered look, navigates to /new?parent=N. */
.issues-subissues-new {
    display: inline-flex; align-items: center; gap: 5px;
    border: 1px solid var(--gh-border-default);
    border-radius: 6px;
    color: var(--gh-fg-muted);
    font-size: 12px;
    padding: 2.5px 8px;
    white-space: nowrap;
    text-decoration: none;
    flex: 0 0 auto;
}
.issues-subissues-new:hover { color: var(--gh-fg-default); border-color: var(--gh-fg-muted); text-decoration: none; }
/* The create form's parent note: a quiet line above the title input
   naming what the new issue will nest under. */
.issues-create-parent-note {
    display: flex; align-items: center; gap: 6px;
    margin: 0 0 10px;
    font-size: 13px;
    color: var(--gh-fg-muted);
    min-width: 0;
}
.issues-create-parent-note .issues-rel-title { flex: 0 1 auto; }
.issues-subissues-list { list-style: none; margin: 0; padding: 0; }
.issues-subissues-list .issues-rel-row {
    border-top: 1px solid var(--gh-border-muted);
    padding: 6px 0;
}
.issues-rel-pips { display: inline-flex; align-items: center; gap: 2px; flex: 0 0 auto; }
.issues-rel-pip { width: 18px; height: 18px; border-radius: 50%; }
.issues-rel-pip-more { font-size: 11px; color: var(--gh-fg-muted); }
.issues-detail-side-people {
    list-style: none; padding: 0; margin: 0;
    display: flex; flex-direction: column; gap: 6px;
}
.issues-detail-side-people li {
    display: flex; align-items: center; gap: 8px;
    font-size: 14px;
    color: var(--gh-fg-default);
}
.issues-side-people-name {
    cursor: pointer;
    transition: color .12s;
}
.issues-side-people-name:hover { color: var(--gh-accent-fg); }

/* Issue type icon - clean colored shape with a centered white
   glyph drawn purely in CSS (no SVG). Each type has its own
   distinct shape + glyph so the icon reads at any size without
   the text label:
     - Bug:     red disc + white "!" (problem / alert signal)
     - Feature: blue disc + white "+" (additive signal)
     - Task:    yellow rounded square + white "✓" (done signal)
     - None:    dashed empty circle (placeholder)
   The wrapper sizes the icon; ::before paints the inner glyph
   with a font-based glyph so the strokes stay crisp at every
   size and don't look stitched-together. */
.issues-type-icon {
    display: inline-flex; align-items: center; justify-content: center;
    width: 18px; height: 18px;
    flex-shrink: 0;
    box-sizing: border-box;
    border-radius: 50%;
    position: relative;
    color: #ffffff;
    font-size: 12px;
    font-weight: 700;
    line-height: 1;
    font-family: -apple-system, "Segoe UI", Roboto, sans-serif;
}
.issues-type-icon::before {
    content: "";
    display: inline-block;
    line-height: 1;
}
/* Bug: solid red disc with white "!" glyph. */
.issues-type-icon[data-type="bug"] {
    background: #d1242f;
}
.issues-type-icon[data-type="bug"]::before {
    content: "!";
    margin-top: -1px;
}
/* Feature: blue disc with white "+" glyph. */
.issues-type-icon[data-type="feature"] {
    background: #0969da;
}
.issues-type-icon[data-type="feature"]::before {
    content: "+";
    font-size: 14px;
    font-weight: 700;
    margin-top: -1px;
}
/* Task: rounded yellow-orange square with white "✓" check. */
.issues-type-icon[data-type="task"] {
    background: #bf8700;
    border-radius: 4px;
}
.issues-type-icon[data-type="task"]::before {
    content: "✓";
    font-size: 11px;
    font-weight: 700;
}
/* No type: dashed outline only - placeholder. */
.issues-type-icon[data-type="none"] {
    background: transparent;
    border: 1.5px dashed var(--gh-fg-muted);
}
.issues-type-icon[data-type="none"]::before { content: none; display: none; }
/* Hide any leftover SVG nodes (templates may still pass them). */
.issues-type-icon svg { display: none; }
/* Sidebar Type display - clean icon-then-text inline. No outer
   pill / chip wrapper. The colored icon is the visual cue; the
   word "Bug" / "Feature" / "Task" is plain text in the default
   color so it doesn't compete with the icon for attention. */
.issues-detail-side-type {
    display: inline-flex; align-items: center; gap: 8px;
    font-size: 14px;
    font-weight: 500;
    line-height: 1.4;
    color: var(--gh-fg-default);
    padding: 0;
    border: 0;
    background: transparent;
}
.issues-detail-side-type .issues-type-icon {
    width: 16px; height: 16px;
}
/* Compact type indicator on list rows. Just the colored icon -
   no text chip - so the row stays scannable and the type doesn't
   compete visually with the labels and title. Hover tooltip
   carries the full type name. */
.issues-row-type-mark {
    display: inline-flex; align-items: center;
    margin-right: 4px;
    cursor: help;
}

/* Type chip on list rows. Tinted pill that picks up the type's
   accent so a quick scan tells the reader Bug vs Feature vs Task
   without reading the word. */
.issues-row-type {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 2px 10px;
    margin-right: 6px;
    font-size: 12px;
    font-weight: 500;
    line-height: 18px;
    border-radius: 999px;
    text-transform: capitalize;
    border: 1px solid currentColor;
    background: transparent;
    color: var(--gh-fg-muted);
    transition: filter .12s;
}
.issues-row-type:hover { filter: brightness(0.95); }
.issues-row-type[data-type="bug"] {
    color: #d1242f;
    background: color-mix(in srgb, #d1242f 12%, transparent);
}
.issues-row-type[data-type="feature"] {
    color: #0969da;
    background: color-mix(in srgb, #0969da 12%, transparent);
}
.issues-row-type[data-type="task"] {
    color: #9a6700;
    background: color-mix(in srgb, #9a6700 12%, transparent);
}
.issues-row-type .issues-type-icon {
    width: 10px; height: 10px;
    border-width: 1.5px;
    border-color: currentColor;
}

/* @-mention autocomplete popover. Positioned absolutely near the
   textarea where the user typed `@`, listing workspace
   collaborators ranked by display-name / email match. Arrow keys
   navigate; Enter/Tab inserts the email handle. */
/* Mention autocomplete popover. Auto-sizes to its content within
   bounded min/max widths so a single short result doesn't render
   inside an oversized 280px panel. fit-content lets the box shrink
   to the longest row's natural width. */
.issues-detail-side-people .issues-mini-avatar {
    width: 20px !important; height: 20px !important;
    font-size: 10px !important;
}
.issues-detail-side-labels {
    display: flex; flex-wrap: wrap; gap: 4px;
}

/* Stack on narrow screens. The fixed-header / scrolling-thread
   layout doesn't make sense once the sidebar stacks above the thread,
   so revert to a normal page-level scroll: .issues-page itself
   scrolls, the grid + main return to their natural overflow.
   Same treatment is applied to .issues-create-grid (the
   new-issue page) so its 296px sidebar stops pushing the form
   contents off the right edge of the viewport on tablet /
   phone - the sidebar now stacks ABOVE the title input. */
@media (max-width: 960px) {
    .issues-page { overflow-y: auto; overflow-x: hidden; }
    .issues-detail-grid,
    .issues-create-grid {
        /* minmax(0, 1fr), NEVER bare 1fr: 1fr's implied minimum is
           the content's min-content, and WebKit sizes the collapsed
           track from it even with min-width:0 on the item - on a
           real iPhone the single column resolved to 589px in a
           430px viewport and every bubble was cut at the edge
           (Blink clamps, so no emulator showed it). The desktop
           template above already says minmax(0, 1fr) for exactly
           this reason. */
        grid-template-columns: minmax(0, 1fr);
        gap: 16px;
        overflow: visible;
        flex: 0 0 auto;
    }
    .issues-detail-main { overflow-y: visible; padding-bottom: 32px; padding-right: 0; }
    /* Detail page: the reader wants the topic (title + issue body)
       first, NOT a full-width wall of Assignees / Type / Milestone /
       Labels before they can read what the issue is about. Push the
       sidebar BELOW the thread on collapse (order:1, main stays 0).
       The new-issue page keeps the same order via its own rule below. */
    .issues-detail-sidebar { order: 1; }
    /* Smart-sticky is desktop-only: below the collapse the sidebar is
       stacked static under the thread, so drop the sticky position and
       any inline top issues-ui.js may have set on a wider viewport. */
    body.is-issues-detail-flow .issues-detail-sidebar { position: static; top: auto; }

    /* New-issue page: keep title + body on top (where the user
       starts typing), sidebar pickers below - opposite of the
       detail page where the sidebar carries already-set values
       and reads more useful at the top. */
    .issues-new-page .issues-create-main { order: 0; }
    .issues-new-page .issues-detail-sidebar { order: 1; }
}

/* Phone - tighten the new-issue page even more. */
@media (max-width: 540px) {
    .issues-new-page { padding: 12px 12px 0; }
    .issues-new-title { font-size: 20px; margin-bottom: 12px; }
    .issues-new-title-input { height: 36px; font-size: 14px; }
    .issues-new-actions {
        flex-wrap: wrap;
        gap: 8px;
    }
    .issues-new-actions > * {
        flex: 1 1 auto;
        min-width: 0;
        text-align: center;
        justify-content: center;
    }
    /* The draft-status whisper shares this row; stretched between
       the buttons it RESIZED them every time its text changed
       ("Draft restored" at load = a real layout shift, tripwire-
       caught). It gets its own always-reserved line above the
       buttons instead - same honest-reservation pattern as the
       bubble lang row. */
    .issues-new-actions > .bf-md-draft-status {
        flex: 1 1 100%;
        order: -1;
        margin-right: 0;
        text-align: left;
        min-height: 17px;
    }
    .issues-detail-side-section {
        padding: 12px 12px;
    }
    .issues-create-label { font-size: 13px; }
}

/* ─── Issue DETAIL page responsive ───────────────────────────
   The 960 px breakpoint above already collapses the 2-col
   grid (main + sidebar) to single column with sidebar on top.
   These extra rules tighten the title row, meta line, action
   buttons, and thread bubbles so the detail page reads cleanly
   on tablet / phone. */

/* Tablet portrait - gutters + smaller title + the full-width
   conversation. */
@media (max-width: 720px) {
    .issues-detail-page { padding: 16px 14px 0; }
    .issues-detail-title-row { margin: 0 0 16px; padding-bottom: 12px; }
    .issues-detail-title { font-size: 24px; line-height: 1.3; }
    /* The title-block column stack + action alignment + vote
       flush-left now live in the 960px block above (the grid
       collapses there, so the title needs the full row from
       960px down, not only from 720px). */

    /* ── Full-width conversation ─────────────────────────────────
       The 56px avatar rail is the single biggest tax on a phone's
       text width (~15% at 390px). The thread item becomes a block,
       the author avatar folds INTO the bubble head (the head text
       clears it with padding), and the bubble spans the whole
       column. The timeline spine is already hidden at ≤960. */
    .issues-thread-item { display: block; position: relative; }

    .issues-thread-bubble-head { padding: 10px 12px 10px 46px; }

    /* Replies: the 56px root indent compresses to a slim guide so
       nested text keeps its width. */
    .issues-thread-replies { margin-left: 12px; padding-left: 12px; }
    /* Activity rows: the dot/avatar column compresses with the rail. */
    .issues-thread-event { padding-left: 44px; }
    /* The sub-issues card follows the full-width column. */
    .issues-subissues { margin-left: 0; }
}

/* Phone - even smaller title, drop the source/relationship
   card padding, tighten the thread bubbles. */
@media (max-width: 540px) {
    .issues-detail-page { padding: 12px 12px 0; }
    .issues-detail-title { font-size: 20px; }
    .issues-detail-meta { font-size: 13px; gap: 6px; }

    .issues-detail-title-row { margin-bottom: 12px; padding-bottom: 10px; }
    /* Thread items are full-width blocks with the avatar folded
       into the bubble head from 720px down (see the ≤720 block);
       here the head just earns the right to wrap. */
    .issues-thread-bubble-head {
        flex-wrap: wrap;
        gap: 4px 8px;
    }
    /* Sub-issues head: the one-line desktop strip (title · readout ·
       bar · two triggers) cannot fit 390px - it wrapped the TITLE
       mid-word and clipped the add trigger off the card edge. Phone
       grammar: the pieces wrap as whole units and the progress bar
       takes its own full row under them. */
    .issues-subissues-head { flex-wrap: wrap; row-gap: 6px; }
    .issues-subissues-title { white-space: nowrap; }
    .issues-subissues-bar { order: 9; flex-basis: 100%; }
    /* Child rows: the whole point of the panel is the child titles -
       let them wrap instead of ellipsizing at ~40 characters. */
    .issues-subissues-list .issues-rel-title { white-space: normal; }
    /* Touch targets: the 20px remove control and the 26px bubble
       head actions sit under fingers here - give them real hit
       areas (the icons keep their size; only the pads grow). */
    .issues-rel-remove { width: 30px; height: 30px; }
    .issues-subissues-list .issues-rel-row { padding: 8px 0; }
    .issues-thread-bubble-actions .issues-bubble-edit,
    .issues-thread-bubble-actions .issues-bubble-copy {
        width: 30px; height: 30px;
    }
    .issues-detail-side-picker summary { width: 32px; height: 32px; }
    .issues-thread-bubble-body {
        padding: 12px 14px;
        /* Phone reading size: 15px with looser leading - long
           structured tickets (the P4-class bodies) read as prose,
           not as a compressed wall. */
        font-size: 15px;
        line-height: 1.65;
    }
    .issues-thread-bubble-actions {
        gap: 4px;
    }
    /* Source card (relationship card to the email) - tighter
       padding so it doesn't dominate the column on phones. */
    .issue-source-card { padding: 12px 14px; gap: 8px; }
    .issue-source-card-row { flex-wrap: wrap; }
    /* Sidebar pickers: tighter padding. */
    .issues-detail-side-section { padding: 12px 14px; }
    /* (Bubble head action sizing lives with the touch-target rules
       above - 30px hit areas, not the old 26px shrink.) */
    /* Comment compose form: full width inputs. */
    .issues-comment-form .issues-body-textarea { font-size: 14px; }
}

/* ── Compose-issue page polish (uses .issues-create-grid) ────────── */

.issues-new-page { max-width: 1280px; }
.issues-new-title {
    font-size: 24px;
    font-weight: 400;
    margin: 0 0 16px;
    color: var(--gh-fg-default);
    letter-spacing: -.01em;
}
.issues-new-title-input {
    width: 100%;
    height: 40px;
    padding: 5px 12px;
    font-size: 14px;
    line-height: 20px;
    color: var(--gh-fg-default);
    background: var(--gh-canvas);
    border: 1px solid var(--gh-border);
    border-radius: 6px;
    transition: border-color .15s, box-shadow .15s;
    margin-bottom: 16px;
}
.issues-new-title-input:focus {
    outline: none;
    border-color: var(--gh-accent-emphasis);
    box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.30);
}
.issues-new-actions {
    display: flex; align-items: center; justify-content: flex-end;
    gap: 8px;
    margin-top: 16px;
}
.issues-new-submit {
    display: inline-flex; align-items: center;
    height: 32px;
    padding: 0 14px;
    font-size: 14px;
    font-weight: 500;
    color: var(--surface, #fff);
    background: var(--text);
    border: 1px solid rgba(31, 35, 40, 0.15);
    border-radius: 6px;
    cursor: pointer;
}
.issues-new-submit:hover { background: color-mix(in srgb, var(--text) 84%, var(--surface)); }
.issues-new-submit:disabled { opacity: .55; cursor: not-allowed; }
.issues-new-submit:disabled:hover { background: var(--text); }

/* Inline validation hint shown by issues-new-validate.js when the
   body fails one of the two server-enforced rules (empty / matches
   title). Soft amber for "heads up", not red - the user hasn't
   submitted yet, so the rule is COACHING, not REJECTING. Standard
   form-validation soft-error treatment (GitHub / Linear / Notion). */
.issues-new-validation-hint {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin: 8px 0 0;
    padding: 0;
    font-size: 13px;
    line-height: 1.4;
    color: var(--gh-attention-fg, #9a6700);
    /* No filled background, no border - this is a quiet inline
       cue, not a banner. Same posture every form-validation hint
       in the app uses. */
}
.issues-new-validation-hint svg {
    flex: 0 0 auto;
    color: currentColor;
}
.issues-new-validation-hint[hidden] {
    display: none;
}

/* Editor look-and-feel */
.issues-create-label {
    display: block;
    font-size: 14px;
    font-weight: 500;
    color: var(--gh-fg-default);
    margin: 8px 0 4px;
}
.issues-create-required { color: var(--gh-danger-fg); }

/* Markdown rendering - ALL `.markdown-body` styling lives in
   the dedicated component file `wwwroot/css/markdown-body.css`.
   Anything that adds a `.markdown-body element` rule here MUST
   move it to that file instead - the source-pin
   `MarkdownBodyIsSingleSourceOfTruthTests` enforces. */

/* Q7 - breathing room between the title row (title + state badge
   + meta line) and the thread that follows. Without this the meta
   line ("RATAM opened this issue 1 minute ago · 1 comment") sits
   flush against the first thread bubble; the eye reads it as a
   continuation of the meta rather than the start of a separate
   region. 24px gap matches GitHub's vertical rhythm. */
.issues-detail-title-row {
    margin-bottom: 24px;
}

/* Sidebar: re-use .issues-detail-side-* tokens. Override the
   compose layout's grid columns to match the narrower sidebar shape. */
.issues-create-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 296px;
    gap: 32px;
    align-items: start;
}
.issues-create-main { min-width: 0; }

/* Sidebar pickers in compose form: form labels (label[for]) include
   a checkbox we hide visually; clicking the row toggles the checkbox.
   This keeps server-side multi-select working without JS while still
   looking like the check-mark UI. */
.issues-pop-checkbox {
    position: absolute;
    width: 1px; height: 1px;
    opacity: 0;
    pointer-events: none;
}
.issues-detail-side-pop-opt {
    /* When using <label>, the entire row toggles the checkbox. */
    cursor: pointer;
    /* Anchor the row as the containing block for its own visually-
       hidden <input class="issues-pop-checkbox"> (position:absolute,
       below). WITHOUT this, that checkbox's containing block resolves
       up to the position:fixed `.issues-detail-side-pop` card - NOT
       the scrolling option list - so its static position is pinned to
       the card while the list scrolls under it. On clicking a row near
       the bottom, focus moves to the checkbox and the browser's native
       focus-scroll can't reconcile the card-pinned checkbox against the
       scrolled list: it scrolls the <ul> to its end (a band of dead
       white space) and jogs the page. This was the user-reported
       "create-issue picker shows white space and self-scrolls" defect.
       position:relative makes the checkbox track its own row inside the
       scroller, so focus-scroll becomes a correct no-op. Only the
       create/form-bound picker renders the hidden checkbox (the detail
       picker uses <button> rows), which is why the bug was create-only;
       the rule is harmless on every other row (no abspos descendant to
       re-anchor). Pinned by IssuePickerPopoverScrollContainmentTests. */
    position: relative;
}
.issues-detail-side-pop-opt input[type="checkbox"] { /* keep accessible */ }

@media (max-width: 960px) {
    .issues-create-grid { grid-template-columns: 1fr; }
    /* Sidebar reads below the thread on collapse (see the detail
       responsive block above) so the issue topic comes first. */
    .issues-detail-sidebar { order: 1; }
}

.issues-form-layout,
.issues-detail-layout {
    max-width: 880px;
}
.issues-layout {
    /* Legacy alias kept so the form + detail pages keep working
       without duplicating their wrapper class. The new list page
       uses .issues-page directly (wider). */
    max-width: 960px;
    margin: 0 auto;
    padding: 1rem 1.25rem 2rem;
}

/* Flash banner shown after label save / delete + similar redirects
   so the user sees confirmation without an alert dialog. */
.issues-flash {
    margin: 0 0 1rem;
    padding: .65rem .85rem;
    background: color-mix(in srgb, #16a34a 8%, var(--surface));
    border: 1px solid color-mix(in srgb, #16a34a 30%, var(--border));
    border-radius: 8px;
    color: var(--text);
    font-size: 12px;
}

/* In-app confirm modal (used by label delete + future destructive
   actions). Replaces native confirm() per user preference for
   styled in-app UI. */
.issues-modal-overlay {
    position: fixed; inset: 0;
    background: rgba(15, 23, 42, .55);
    display: flex; align-items: center; justify-content: center;
    z-index: 9999;
    animation: issues-modal-fade .12s ease;
}
@keyframes issues-modal-fade {
    from { opacity: 0; }
    to { opacity: 1; }
}
.issues-modal {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 1.25rem 1.4rem;
    max-width: 420px;
    box-shadow: 0 18px 48px rgba(15, 23, 42, .25);
}
.issues-modal p { margin: 0 0 1rem; color: var(--text); font-size: 13px; line-height: 1.55; }
.issues-modal-actions {
    display: flex; gap: .5rem; justify-content: flex-end;
}

/* Label catalog page */
/* ── Catalog editors (labels + custom fields) - premium pass
   2026-08-28. One visual grammar for both settings catalogs:
   hairline cards with quiet heads (no filled slabs), rows on a
   shared aligned grid, per-row actions in the LEAD next to the
   name, meta columns that wrap whole onto their own line instead
   of clipping, tokens only so both themes just work. */
.issues-labels-page { max-width: 1100px; }
.issues-labels-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 360px;
    gap: 1.5rem;
    align-items: start;
}
.issues-labels-list-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    overflow: hidden;
}
.issues-labels-list-head {
    display: flex;
    align-items: center;
    gap: .45rem;
    padding: .75rem 1rem;
    border-bottom: 1px solid var(--border);
    font-size: 12px;
    font-weight: 500;
    color: var(--text-muted);
}
.issues-labels-list-head strong { color: var(--text); font-weight: 700; }

/* The rows share ONE grid via subgrid: the lead column sizes to the
   WIDEST chip+actions cluster across all rows (identity shrinks
   last - the description gives way first), and the columns stay
   aligned row to row instead of going ragged per-row. The LIST owns
   the horizontal padding (a subgrid row's own inline padding would
   eat into the parent-defined tracks); dividers inset to content. */
.issues-labels-rows {
    list-style: none; margin: 0;
    padding: 0 1rem;
    display: grid;
    grid-template-columns: minmax(180px, max-content) minmax(0, 1fr) max-content;
    column-gap: .85rem;
}
.issues-labels-row {
    grid-column: 1 / -1;
    display: grid;
    grid-template-columns: subgrid;
    align-items: center;
    padding: .6rem 0;
    min-height: 48px;
    border-top: 1px solid var(--border);
}
.issues-labels-row:first-child { border-top: none; }
.issues-labels-row:hover { background: var(--surface-2); }
/* Lead cluster: the chip and its actions travel together so the
   cursor never crosses the row (feedback: actions near the name,
   not far right). The chip hugs its text; one absurd name cannot
   widen every row - it caps and ellipsizes instead. */
.issues-labels-row-lead {
    display: inline-flex;
    align-items: center;
    gap: .6rem;
    min-width: 0;
}
.issues-labels-row-lead .issues-label-chip {
    flex: 0 1 auto;
    min-width: 0;
    max-width: 280px;
}
.issues-labels-row-desc {
    font-size: 12px; color: var(--text);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.issues-labels-row-desc-empty { color: var(--text-muted); font-style: italic; }
.issues-labels-row-count { font-size: 11px; text-align: right; }
.issues-labels-row-count a { color: var(--primary); text-decoration: none; }
.issues-labels-row-count a:hover { text-decoration: underline; }
.issues-labels-row-count-zero { color: var(--text-muted); font-style: italic; }
/* Always-visible quiet buttons: a settings catalog has few rows, so
   the calm hairline pair beats a hover reveal (and stays honest on
   touch, where hover does not exist). */
.issues-labels-row-actions {
    display: inline-flex; gap: .35rem;
}
/* Edit + Delete row buttons - match height / padding so the
   pair reads as one segment, no wonky boundary. The Delete
   variant uses the same border-color as Edit (neutral) and only
   tints red on hover - keeps the row scannable without screaming
   destructive at every Delete row. */
.issues-labels-row-btn {
    display: inline-flex; align-items: center; justify-content: center;
    height: 28px;
    padding: 0 12px;
    background: var(--gh-btn-bg, var(--surface));
    border: 1px solid var(--gh-btn-border, var(--border));
    border-radius: 6px;
    color: var(--gh-fg-default, var(--text));
    font-size: 12px;
    font-weight: 500;
    text-decoration: none;
    cursor: pointer;
    transition: background-color .12s, color .12s, border-color .12s;
}
.issues-labels-row-btn:hover {
    background: var(--gh-btn-hover, var(--surface));
    color: var(--gh-fg-default, var(--text));
}
.issues-labels-row-btn-danger {
    color: var(--gh-fg-default, var(--text));
}
.issues-labels-row-btn-danger:hover {
    background: color-mix(in srgb, #cf222e 8%, transparent);
    color: #cf222e;
    border-color: color-mix(in srgb, #cf222e 40%, var(--gh-btn-border, var(--border)));
}
.issues-labels-row-btn-form { display: inline; margin: 0; }

/* Read-only seat: the form card slot explains who curates. */
.issues-labels-readonly-note {
    margin: 0;
    padding: 1rem .95rem;
    font-size: 12px;
    color: var(--text-muted);
    line-height: 1.5;
}

/* Form (new label / edit) */
.issues-labels-form-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    overflow: hidden;
    position: sticky;
    top: 1rem;
}
.issues-labels-form-head {
    display: flex; align-items: center; justify-content: space-between;
    gap: .5rem;
    padding: .75rem 1rem;
    border-bottom: 1px solid var(--border);
}
.issues-labels-form-head h2 { margin: 0; font-size: 13px; font-weight: 650; color: var(--text); letter-spacing: .01em; }
/* Cancel = a quiet hairline button (never an underlined word). It
   composes the shared .issues-labels-row-btn footprint; this class
   only keeps it from stretching. */
.issues-labels-form-cancel { flex-shrink: 0; }
.issues-labels-form { padding: 1rem; display: flex; flex-direction: column; gap: .25rem; }
.issues-labels-form .form-label { font-weight: 600; font-size: 12px; margin-top: .35rem; margin-bottom: .3rem; color: var(--text); }
.issues-labels-form .form-label .pc-help,
.issues-custom-fields-form .form-label .pc-help { margin-left: .4rem; }

/* Premium inputs on both catalog forms: quiet hairline fields with
   an ink focus ring - no framework chrome, one 34px height shared
   with the buttons beside them (control-size harmonization). */
.issues-labels-form .form-control,
.issues-custom-fields-form .form-control {
    height: 34px;
    padding: 0 10px;
    font-size: 13px;
    color: var(--text);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    box-shadow: none;
    transition: border-color .12s ease, box-shadow .12s ease;
}
.issues-labels-form .form-control:focus,
.issues-custom-fields-form .form-control:focus {
    outline: none;
    border-color: var(--text-subtle);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--text) 8%, transparent);
}
.issues-labels-form .form-control { margin-bottom: .5rem; }

.issues-color-row {
    display: flex; align-items: center; gap: .5rem;
    margin-bottom: .5rem;
}
.issues-color-input { font-family: ui-monospace, SFMono-Regular, monospace; }
.issues-labels-form .issues-color-input { margin-bottom: 0; }
/* Preview swatch shares the input's exact height so the pair reads
   as one control. */
.issues-color-preview {
    width: 34px; height: 34px;
    border-radius: 8px;
    border: 1px solid var(--border);
    flex-shrink: 0;
}
/* Swatches are content-sized marks, never stretched to fill the
   card - a palette is a set of chips, not a layout row. */
.issues-color-palette {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-bottom: .85rem;
}
.issues-color-sw {
    width: 22px; height: 22px;
    border-radius: 8px;
    border: 1px solid color-mix(in srgb, var(--text) 12%, transparent);
    cursor: pointer;
    padding: 0;
    transition: transform .1s ease, box-shadow .1s ease;
}
.issues-color-sw:hover {
    transform: scale(1.12);
    box-shadow: 0 2px 8px rgba(15, 23, 42, .18);
}
.issues-labels-form-preview {
    margin: .35rem 0 1rem;
    padding: .6rem .75rem;
    border: 1px solid var(--border);
    border-radius: 8px;
    display: flex; align-items: center; gap: .55rem;
    font-size: 11px;
}
.issues-labels-form-preview-label { color: var(--text-muted); }
.issues-labels-form-actions {
    display: flex; justify-content: flex-end;
    margin-top: .25rem;
}

@media (max-width: 900px) {
    .issues-labels-grid { grid-template-columns: 1fr; }
    .issues-labels-form-card { position: static; }
}
@media (max-width: 700px) {
    .issues-labels-rows { display: block; }
    .issues-labels-row {
        grid-template-columns: 1fr;
        gap: .35rem;
        /* Stacked grid items stretch by default, which inflated every
           label chip into a full-width colored bar - a chip is a chip
           at any width; it hugs its own text. */
        justify-items: start;
    }
    .issues-labels-row-count { text-align: left; }
}

/* Custom fields catalog (per-workspace settings page at
   /Issues/{ws}/custom-fields). Mirrors the labels page's grid
   layout so admins recognise the surface; the rows carry a
   leading type glyph, the name + its actions in the lead, and a
   trailing meta cluster (type · list state) that drops WHOLE
   onto its own line on narrow frames - nothing is ever bitten. */
.issues-custom-fields-page { max-width: 1100px; }
.issues-custom-fields-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 380px;
    gap: 1.5rem;
    align-items: start;
}
.issues-custom-fields-list-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    overflow: hidden;
}
.issues-custom-fields-list-head {
    display: flex;
    align-items: center;
    gap: .45rem;
    padding: .75rem 1rem;
    border-bottom: 1px solid var(--border);
    font-size: 12px;
    font-weight: 500;
    color: var(--text-muted);
}
.issues-custom-fields-list-head strong { color: var(--text); font-weight: 700; }
/* The rows share ONE grid via subgrid: name column, then TWO meta
   columns (type, list state) each ALIGNED across every row - meta
   is never a ragged cluster of adjacent words (owner 2026-08-28).
   Manager rows carry the drag handle; viewer rows don't (the
   manager <ul> is the one marked data-cf-reorder). */
.issues-custom-fields-rows {
    list-style: none; margin: 0;
    /* The LIST owns the horizontal padding: a subgrid row's own
       inline padding would eat into the parent-defined tracks and
       glue the 14px handle track onto the name. Dividers + hover
       inset to the content edge (grouped-list style). */
    padding: 0 1rem;
    display: grid;
    grid-template-columns: minmax(0, 1fr) max-content max-content;
    column-gap: .8rem;
}
ul[data-cf-reorder].issues-custom-fields-rows {
    grid-template-columns: 14px minmax(0, 1fr) max-content max-content;
}
.issues-custom-fields-row {
    grid-column: 1 / -1;
    display: grid;
    grid-template-columns: subgrid;
    align-items: center;
    padding: .6rem 0;
    min-height: 48px;
    border-top: 1px solid var(--border);
}
.issues-custom-fields-row:first-child { border-top: none; }
.issues-custom-fields-row:hover { background: var(--surface-2); }
.issues-custom-fields-row-handle {
    color: var(--text-subtle);
    font-size: 13px;
    line-height: 1;
    cursor: grab;
    user-select: none;
    text-align: center;
}
.issues-custom-fields-row.is-dragging {
    opacity: .45;
    background: var(--surface-2);
}
.issues-custom-fields-row.is-dragging .issues-custom-fields-row-handle {
    cursor: grabbing;
}
/* Type glyph slot on the form's locked-type value row. */
.issues-custom-fields-row-icon {
    color: var(--text-muted);
    width: 26px; height: 26px;
    display: inline-flex; align-items: center; justify-content: center;
    flex-shrink: 0;
}
.issues-custom-fields-row-lead {
    display: inline-flex;
    align-items: center;
    gap: .6rem;
    min-width: 0;
}
.issues-custom-fields-row-name {
    font-weight: 600; color: var(--text);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    min-width: 0;
}
/* The meta wrapper hands its two cells to the shared grid (nested
   subgrid), so the type column and the state column line up across
   rows instead of right-anchoring per row. */
.issues-custom-fields-row-meta {
    grid-column: span 2;
    display: grid;
    grid-template-columns: subgrid;
    align-items: center;
}
/* Each meta cell is a designed glyph+word unit, never a bare word. */
.issues-custom-fields-row-type {
    display: inline-flex;
    align-items: center;
    gap: .45rem;
    font-size: 12px;
    color: var(--text-muted);
    white-space: nowrap;
}
.issues-custom-fields-row-type svg { flex-shrink: 0; }
/* Flat state indicator - explicitly NOT a rounded pill per
   feedback_no_rounded_pill_badges.md. Communicates the
   "shown in list" / "sidebar only" bit via a checkmark icon
   + text rather than a background fill. Green accent for the
   "on" state matches the workspace's existing success
   palette. */
.issues-custom-fields-row-pin {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: 11px;
    font-weight: 600;
    color: var(--success, #0a7c3a);
    white-space: nowrap;
}
.issues-custom-fields-row-pin svg {
    color: var(--success, #0a7c3a);
    flex-shrink: 0;
}
.issues-custom-fields-row-pin-off {
    color: var(--text-muted);
    font-weight: 500;
}
.issues-custom-fields-form-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    overflow: hidden;
    position: sticky;
    top: 1rem;
}
.issues-custom-fields-form-head {
    display: flex; align-items: center; justify-content: space-between;
    gap: .5rem;
    padding: .75rem 1rem;
    border-bottom: 1px solid var(--border);
}
.issues-custom-fields-form-head h2 { margin: 0; font-size: 13px; font-weight: 650; color: var(--text); letter-spacing: .01em; }
.issues-custom-fields-form { padding: 1rem; display: flex; flex-direction: column; gap: .35rem; }
.issues-custom-fields-form .form-label { font-weight: 600; font-size: 12px; margin-top: .35rem; margin-bottom: .3rem; color: var(--text); }
.issues-custom-fields-form .form-hint { font-size: 11px; color: var(--text-muted); margin: 0; }
/* The breadcrumb is its own header row (no side-by-side
   subtitle squeezing it into a narrow column). The page
   explainer lives on the list head's "?" help glyph. */
.issues-custom-fields-head {
    /* Override the parent .issues-page-head's space-between
       flex layout so the breadcrumb gets the full width. */
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
}
.issues-custom-fields-head .app-breadcrumb {
    flex-wrap: nowrap;
    overflow: hidden;
}
.issues-custom-fields-head .app-breadcrumb-link,
.issues-custom-fields-head .app-breadcrumb-current {
    white-space: nowrap;
    text-overflow: ellipsis;
    overflow: hidden;
    min-width: 0;
}

/* Locked field type on the edit branch: a static VALUE row (glyph +
   label + "?" hint) - a locked fact is information, never a disabled
   control. Shares the input footprint so the form rhythm holds. */
.issues-custom-fields-type-locked {
    display: flex;
    align-items: center;
    gap: .5rem;
    min-height: 34px;
    padding: 0 10px 0 4px;
    font-size: 13px;
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 8px;
    background: color-mix(in srgb, var(--text) 3%, var(--surface));
}
.issues-custom-fields-type-locked .pc-help { margin-left: auto; }

/* Three-state display-mode choice. The native radio stays in the
   DOM for form + a11y state but never shows as a browser control:
   the styled ring/dot mark renders the pick, :checked drives it.
   Selection = ink mark + ink-leaning hairline; never a filled bg. */
.issues-custom-fields-display-mode .form-check {
    display: flex;
    align-items: flex-start;
    gap: .6rem;
    font-size: 13px;
    font-weight: 500;
    margin: 0 0 .5rem;
    cursor: pointer;
    user-select: none;
    padding: .6rem .7rem;
    border: 1px solid var(--border);
    border-radius: 10px;
    position: relative;
    transition: border-color .12s ease;
}
.issues-custom-fields-display-mode .form-check:hover {
    border-color: var(--text-subtle);
}
.issues-custom-fields-display-mode .form-check input[type="radio"] {
    position: absolute;
    opacity: 0;
    width: 1px; height: 1px;
    margin: 0;
    pointer-events: none;
}
.issues-custom-fields-mode-mark {
    flex-shrink: 0;
    color: var(--text-muted);
    display: inline-flex;
    margin-top: 1px;
}
.issues-custom-fields-mode-mark-dot {
    fill: currentColor;
    opacity: 0;
    transition: opacity .12s ease;
}
.issues-custom-fields-display-mode .form-check:has(input:checked) {
    border-color: color-mix(in srgb, var(--text) 45%, var(--border));
}
.issues-custom-fields-display-mode input:checked + .issues-custom-fields-mode-mark {
    color: var(--text);
}
.issues-custom-fields-display-mode input:checked + .issues-custom-fields-mode-mark .issues-custom-fields-mode-mark-dot {
    opacity: 1;
}
.issues-custom-fields-display-mode .form-check:has(input:focus-visible) {
    border-color: var(--text-subtle);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--text) 8%, transparent);
}
.issues-custom-fields-mode-body {
    display: flex;
    flex-direction: column;
    gap: .15rem;
    color: var(--text);
    line-height: 1.35;
    min-width: 0;
}

/* Per-field input on the issue sidebar (shared across the
   New-issue form and the Detail-issue editor - same Razor
   partial drives both). Reads at the same weight as the
   value chips on the detail / list pages - a single input
   or <select> per section, sized to the sidebar column.
   Picks up the standard --border / --surface tokens so
   light / dark themes both stay legible. */
.issues-detail-side-cf-input {
    width: 100%;
    box-sizing: border-box;
    padding: 5px 8px;
    font-size: 13px;
    line-height: 1.4;
    border-radius: 6px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
}
.issues-detail-side-cf-input:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 20%, transparent);
}
select.issues-detail-side-cf-input {
    appearance: auto;
}

/* Per-field form on the Detail-issue editor (one form per
   custom field, posting independently to the SetCustomField
   handler). Visually identical to the New-page section: a
   single input filling the sidebar column, no buttons.
   Autosave-on-change in issues-ui.js drives the submit so the
   user reads "edit-in-place" rather than "fill-and-Save". The
   form element is here purely as a host for the antiforgery
   token + handler route + (future) global submit-spinner /
   failure-filter behaviour. */
.issues-detail-side-cf-form {
    display: block;
}
.issues-detail-side-cf-form .issues-detail-side-cf-input {
    width: 100%;
}

/* Custom-field SingleSelect picker. Styled popover that
   replaces the native <select> so the option chips read the
   same colour-aware way they do in the Labels picker, the
   hover / focus states match the rest of the sidebar, and
   the popover-hygiene JS (one open at a time, click-outside
   closes, Escape closes) applies uniformly. The hidden
   input data-cf-select-input carries the actual Guid for
   form submission; the trigger + popover are purely visual.
*/
.issues-cf-select {
    position: relative;
    display: block;
}
.issues-cf-select-picker {
    width: 100%;
    list-style: none;
}
.issues-cf-select-picker > summary {
    list-style: none;
}
.issues-cf-select-picker > summary::-webkit-details-marker {
    display: none;
}
.issues-cf-select-trigger {
    display: flex;
    align-items: center;
    gap: 6px;
    width: 100%;
    box-sizing: border-box;
    padding: 5px 8px;
    min-height: 30px;
    font-size: 13px;
    line-height: 1.4;
    border-radius: 6px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    cursor: pointer;
    transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.issues-cf-select-trigger:hover {
    border-color: color-mix(in srgb, var(--primary) 35%, var(--border));
}
.issues-cf-select-picker[open] > .issues-cf-select-trigger,
.issues-cf-select-trigger:focus-visible {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 20%, transparent);
}
.issues-cf-select-empty {
    color: var(--text-muted);
    flex: 1 1 auto;
    text-align: left;
}
.issues-cf-select-trigger .issues-label-chip {
    flex: 0 1 auto;
    min-width: 0;
    max-width: 100%;
}
.issues-cf-select-caret {
    flex: 0 0 auto;
    margin-left: auto;
    color: var(--text-muted);
}
.issues-cf-select-picker[open] .issues-cf-select-caret {
    transform: rotate(180deg);
}

/* Single-select option editor inside the form */
.issues-custom-fields-options {
    margin-top: .35rem;
    padding-top: .55rem;
    border-top: 1px solid var(--border);
}
.issues-custom-fields-options-list {
    list-style: none; padding: 0; margin: 0 0 .5rem;
    display: flex; flex-direction: column; gap: .35rem;
}
.issues-custom-fields-option-row {
    display: grid;
    grid-template-columns: 22px 80px minmax(0, 1fr) 28px 28px;
    gap: .4rem;
    align-items: center;
}
.issues-custom-fields-option-swatch {
    width: 22px; height: 22px;
    border-radius: 6px;
    border: 1px solid var(--border);
}
/* Option-row controls share ONE 28px height (compact row scale -
   the 34px form fields above them are section-level controls). */
.issues-custom-fields-form .form-control.issues-custom-fields-option-color {
    height: 28px;
    font-family: ui-monospace, SFMono-Regular, monospace;
    font-size: 11px;
    padding: 0 6px;
}
.issues-custom-fields-form .form-control.issues-custom-fields-option-name {
    height: 28px;
    font-size: 12px;
    padding: 0 8px;
}
/* Radio-like default marker: ring always drawn (reserved footprint,
   zero shift), the inner dot fades in on the ONE marked row. Ink when
   on - a default is a fact, not an accent. */
.issues-custom-fields-option-default {
    background: transparent;
    border: 1px solid transparent;
    color: var(--text-muted);
    width: 28px; height: 28px;
    border-radius: 6px;
    cursor: pointer;
    display: inline-flex; align-items: center; justify-content: center;
    transition: color .12s, background-color .12s, border-color .12s;
}
.issues-custom-fields-option-default:hover {
    color: var(--text);
    background: color-mix(in srgb, var(--text) 5%, transparent);
    border-color: var(--border);
}
.issues-custom-fields-option-default-dot {
    fill: currentColor;
    opacity: 0;
    transition: opacity .12s;
}
.issues-custom-fields-option-default.is-on { color: var(--text); }
.issues-custom-fields-option-default.is-on .issues-custom-fields-option-default-dot { opacity: 1; }
.issues-custom-fields-option-remove {
    background: transparent;
    border: 1px solid transparent;
    color: var(--text-muted);
    font-size: 13px;
    width: 28px; height: 28px;
    border-radius: 6px;
    cursor: pointer;
    transition: color .12s, background-color .12s, border-color .12s;
}
.issues-custom-fields-option-remove:hover {
    color: #cf222e;
    background: color-mix(in srgb, #cf222e 8%, transparent);
    border-color: color-mix(in srgb, #cf222e 30%, transparent);
}
.issues-custom-fields-add-option {
    background: transparent;
    border: 1px dashed var(--border);
    color: var(--primary);
    border-radius: 6px;
    padding: .4rem .65rem;
    font-size: 11px;
    cursor: pointer;
    margin-bottom: .5rem;
}
.issues-custom-fields-add-option:hover {
    background: color-mix(in srgb, var(--text) 5%, transparent);
}
.issues-custom-fields-form-actions {
    display: flex; justify-content: flex-end;
    margin-top: .5rem;
}

@media (max-width: 900px) {
    .issues-custom-fields-grid { grid-template-columns: 1fr; }
    .issues-custom-fields-form-card { position: static; }
}
/* Narrow rows: the meta cells drop WHOLE onto their own line under
   the lead, aligned with the name - never a clipped sliver. */
@media (max-width: 700px) {
    .issues-custom-fields-rows,
    ul[data-cf-reorder].issues-custom-fields-rows {
        display: block;
    }
    .issues-custom-fields-row {
        grid-template-columns: minmax(0, 1fr);
        row-gap: .3rem;
    }
    ul[data-cf-reorder] > .issues-custom-fields-row {
        grid-template-columns: 14px minmax(0, 1fr);
    }
    ul[data-cf-reorder] > .issues-custom-fields-row .issues-custom-fields-row-handle {
        grid-row: 1 / span 2;
        align-self: center;
    }
    ul[data-cf-reorder] > .issues-custom-fields-row .issues-custom-fields-row-lead,
    ul[data-cf-reorder] > .issues-custom-fields-row .issues-custom-fields-row-meta {
        grid-column: 2;
    }
    .issues-custom-fields-row-meta {
        grid-column: auto;
        display: inline-flex;
        flex-wrap: wrap;
        gap: .3rem .9rem;
    }
}

/* Issue list row: per-row custom-field chips. Slots in next
   to the label chips on the title row, one per pinned
   (ShowInList) field that has a value on this row. The chip
   is a filter link - clicking narrows the list to that
   value. Single-select chips inherit the option's color via
   --cf-color; text / number / date chips read neutral. */
.issues-list-row-cf-chips {
    display: inline-flex;
    flex-wrap: wrap;
    gap: .35rem;
    margin-left: .35rem;
    vertical-align: middle;
}
.issues-list-row-cf-chip {
    display: inline-flex;
    align-items: baseline;
    gap: .25rem;
    padding: 1px 8px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 500;
    text-decoration: none;
    line-height: 1.55;
    white-space: nowrap;
    max-width: 220px;
    overflow: hidden;
    text-overflow: ellipsis;
}
.issues-list-row-cf-chip.is-pill {
    background: color-mix(in srgb, var(--cf-color, var(--text-muted)) 14%, transparent);
    border: 1px solid color-mix(in srgb, var(--cf-color, var(--text-muted)) 36%, transparent);
    color: var(--text);
}
.issues-list-row-cf-chip.is-neutral {
    background: var(--surface-2);
    border: 1px solid var(--border);
    color: var(--text);
}
.issues-list-row-cf-chip:hover {
    text-decoration: none;
    filter: brightness(0.96);
}
.issues-list-row-cf-chip-name {
    color: var(--text-muted);
    font-weight: 500;
}
.issues-list-row-cf-chip-value {
    color: var(--text);
    font-weight: 600;
}

.issues-header {
    display: flex; align-items: flex-start; justify-content: space-between;
    gap: 1rem;
    margin-bottom: 1rem;
}
.issues-identity {
    display: flex; align-items: flex-start; gap: .65rem; min-width: 0;
}
.issues-identity .btn-back {
    display: inline-flex; align-items: center; justify-content: center;
    width: 32px; height: 32px;
    color: var(--text-muted);
    text-decoration: none;
    border-radius: 8px;
    flex-shrink: 0;
}
.issues-identity .btn-back:hover { background: var(--surface-2); color: var(--text); }
.issues-identity h2 {
    margin: 0;
    font-size: 1.4rem; font-weight: 700; letter-spacing: -.01em;
    color: var(--text);
    line-height: 1.3;
}
.issues-manage-link {
    display: inline-flex; align-items: center; gap: .35rem;
    margin-left: .5rem;
    padding: .25rem .55rem;
    color: var(--text-muted); text-decoration: none;
    font-size: 11px;
    border-radius: 6px;
}
.issues-manage-link:hover { background: var(--surface-2); color: var(--text); }
.issues-new-cta { flex-shrink: 0; }

.issues-toolbar {
    display: flex; align-items: center; gap: .9rem;
    margin-bottom: .85rem;
    flex-wrap: wrap;
}
.issues-search {
    position: relative;
    flex: 1 1 280px;
    min-width: 220px;
}
.issues-search-icon {
    position: absolute; left: .65rem; top: 50%; transform: translateY(-50%);
    color: var(--text-muted);
}
.issues-search-input {
    padding-left: 2.1rem;
    height: 36px;
}
.issues-filters {
    display: inline-flex; gap: .35rem;
}
.issues-filter-chip {
    display: inline-flex; align-items: center;
    padding: .35rem .7rem;
    font-size: 12px;
    color: var(--text-muted);
    background: transparent;
    border: 1px solid transparent;
    border-radius: 8px;
    text-decoration: none;
    transition: background-color .12s ease, color .12s ease, border-color .12s ease;
}
.issues-filter-chip:hover { background: var(--surface-2); color: var(--text); }
.issues-filter-chip.is-active {
    background: var(--surface);
    border-color: var(--border);
    color: var(--text);
    font-weight: 600;
}

.issues-tabs {
    display: flex; gap: 1.2rem;
    border-bottom: 1px solid var(--border);
    margin-bottom: 0;
}
.issues-tab {
    display: inline-flex; align-items: center; gap: .45rem;
    padding: .65rem .15rem;
    font-size: 13px;
    color: var(--text-muted);
    text-decoration: none;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
}
.issues-tab:hover { color: var(--text); }
.issues-tab.is-active {
    color: var(--text);
    border-bottom-color: var(--primary);
    font-weight: 600;
}
.issues-tab-count {
    display: inline-block;
    padding: 1px 7px;
    font-size: 10px; font-weight: 600;
    color: var(--text-muted);
    background: var(--surface-2);
    border-radius: 6px;
    line-height: 1.5;
}
.issues-tab.is-active .issues-tab-count {
    background: color-mix(in srgb, var(--text) 7%, transparent);
    color: var(--text);
}

.issues-list {
    list-style: none; padding: 0;
    margin: 0 0 1rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-top: none;
    border-radius: 0 0 12px 12px;
    overflow: hidden;
}
.issues-row {
    display: flex; align-items: center; gap: .75rem;
    padding: .75rem 1rem;
    border-top: 1px solid var(--border);
    transition: background-color .1s ease;
}
.issues-row:first-child { border-top: none; }
.issues-row:hover { background: var(--surface-2); }
.issues-row-main {
    display: flex; align-items: flex-start; gap: .75rem;
    flex: 1; min-width: 0;
    text-decoration: none; color: inherit;
}
.issues-row-state {
    flex-shrink: 0; padding-top: 2px;
}
.issues-row[data-issue-state="open"] .issues-row-state { color: #16a34a; }
.issues-row[data-issue-state="closed"] .issues-row-state { color: var(--gh-done-fg); }
.issues-row-text { min-width: 0; flex: 1; }
.issues-row-title {
    display: flex; align-items: baseline; gap: .5rem;
    flex-wrap: wrap;
}
.issues-row-title-text {
    font-size: 13px; font-weight: 600; color: var(--text);
    line-height: 1.35;
}
.issues-row-num {
    font-size: 11px; color: var(--text-muted); font-weight: 500;
}
.issues-row-meta {
    margin-top: .15rem;
    font-size: 11px; color: var(--text-muted);
}
.issues-row-comments {
    flex-shrink: 0;
    display: inline-flex; align-items: center; gap: .25rem;
    padding: .25rem .5rem;
    color: var(--text-muted); text-decoration: none;
    font-size: 11px;
    border-radius: 6px;
}
.issues-row-comments:hover { background: var(--surface); color: var(--text); }

.issues-empty {
    padding: 2.5rem 1.5rem;
    text-align: center;
    background: var(--surface);
    border: 1px solid var(--border);
    border-top: none;
    border-radius: 0 0 12px 12px;
}
.issues-empty h3 { margin: 0 0 .5rem; font-size: 15px; color: var(--text); }
.issues-empty p { margin: 0 0 1rem; color: var(--text-muted); font-size: 13px; }

/* GitHub-style numbered pager: Previous arrow + numbered page
   buttons with smart elision (1 2 3 … 17 18) + Next arrow. The
   current page reads as a solid filled square; other numbers are
   ghost links; the gap markers (…) carry no chrome.
   The whole pager is centred, with consistent 36px-tall hit
   targets so it never clips off-axis. */
.issues-pager {
    display: flex; align-items: center; justify-content: center;
    flex-wrap: wrap;
    gap: 4px; margin: .6rem 0 .25rem;
    font-size: 12px; color: var(--text);
    font-variant-numeric: tabular-nums;
}
.issues-pager-num,
.issues-pager-btn {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 32px; height: 32px;
    padding: 0 .55rem;
    color: var(--primary); text-decoration: none;
    background: transparent;
    border: 1px solid transparent;
    border-radius: 8px;
    font-weight: 500;
    transition: background .12s ease, color .12s ease, border-color .12s ease;
}
.issues-pager-num:hover,
.issues-pager-btn:hover {
    background: color-mix(in srgb, var(--text) 5%, transparent);
    color: var(--text);
    text-decoration: none;
}
.issues-pager-num.is-current {
    background: color-mix(in srgb, var(--text) 7%, transparent);
    color: var(--text);
    border-color: transparent;
    cursor: default;
}
.issues-pager-num.is-current:hover {
    background: color-mix(in srgb, var(--text) 11%, transparent);
    color: var(--text);
}
.issues-pager-step {
    /* Previous / Next get a tiny extra gap between chevron and
       label so the icon and text don't kiss. */
    gap: .35rem;
    padding: 0 .7rem;
}
.issues-pager-step.is-disabled {
    color: var(--text-muted);
    opacity: .55;
    cursor: not-allowed;
    pointer-events: none;
    background: transparent;
}
.issues-pager-gap {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 24px; height: 32px;
    color: var(--text-muted);
    user-select: none;
}

/* New-issue + comment form */
.issues-form,
.issues-comment-form { margin-top: 1rem; }
.issues-body-textarea {
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
    font-size: 12px;
    resize: vertical;
}
.issues-form-actions,
.issues-comment-form-actions {
    display: flex; gap: .5rem; justify-content: flex-end;
    margin-top: .75rem;
}
.form-hint-inline { color: var(--text-muted); font-size: 11px; font-weight: 400; margin-left: .35rem; }

.issues-detail-meta {
    margin-top: .35rem;
    display: flex; align-items: center; gap: .5rem;
    font-size: 12px; color: var(--text-muted);
    flex-wrap: wrap;
    /* Zero-CLS: the page-presence stack reveals its 26px avatar
       bubbles in this row after the roster poll - reserve the
       bubble envelope so the reveal never grows the row. */
    min-height: 26px;
}
.issues-detail-meta-text strong { color: var(--text); font-weight: 600; }

.issues-thread { display: flex; flex-direction: column; gap: 1rem; }
.issues-comment {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    overflow: hidden;
}
.issues-comment-head {
    padding: .6rem .85rem;
    background: var(--surface-2);
    border-bottom: 1px solid var(--border);
    display: flex; align-items: center; gap: .5rem;
    font-size: 12px;
}
.issues-comment-head strong { color: var(--text); font-weight: 600; }
.issues-comment-meta { color: var(--text-muted); font-size: 11px; }
.issues-comment-body {
    padding: .9rem 1rem;
    color: var(--text);
    line-height: 1.6;
    cursor: default;
    user-select: text;
}
.issues-comment-body p { margin: 0 0 .85rem; }
.issues-comment-body p:last-child { margin-bottom: 0; }
.issues-comment-body code {
    background: var(--surface-2);
    padding: .1rem .35rem;
    border-radius: 4px;
    font-size: .85em;
}
.issues-comment-body pre {
    background: var(--surface-2);
    padding: .75rem 1rem;
    border-radius: 8px;
    overflow-x: auto;
}
.issues-comment-body pre code { background: none; padding: 0; }
.issues-comment-body a { color: var(--primary); text-decoration: underline; }
.issues-comment-body ul,
.issues-comment-body ol { margin: 0 0 .85rem; padding-left: 1.5rem; }
.issues-empty-body { color: var(--text-muted); }

@media (max-width: 720px) {
    .issues-toolbar { flex-direction: column; align-items: stretch; }
    .issues-search { flex: 1 1 auto; min-width: 0; }
    .issues-header { flex-direction: column; align-items: stretch; }
    .issues-new-cta { align-self: flex-start; }
}

/* ── New-issue page (standard-shape) ────────────────────────────────── */
/* Wide two-column composer with a markdown editor on the left and a
   sidebar of pickers on the right. Mirrors Burofolk's "Create new issue"
   surface so users coming feel familiar. */

.issues-create-page {
    max-width: 1280px;
    margin: 0 auto;
    padding: 1.5rem 1.75rem 3rem;
}
.issues-create-header {
    display: flex; align-items: flex-start; gap: .75rem;
    padding-bottom: 1rem;
    border-bottom: 1px solid var(--border);
    margin-bottom: 1.5rem;
}
.issues-create-header .btn-back {
    display: inline-flex; align-items: center; justify-content: center;
    width: 32px; height: 32px;
    color: var(--text-muted); text-decoration: none;
    border-radius: 8px;
    flex-shrink: 0;
}
.issues-create-header .btn-back:hover { background: var(--surface-2); color: var(--text); }
.issues-create-eyebrow {
    font-size: 11px; color: var(--text-muted);
    margin-bottom: .15rem;
}
.issues-create-title {
    margin: 0;
    font-size: 1.4rem; font-weight: 700; letter-spacing: -.01em;
    color: var(--text);
}

.issues-create-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 296px;
    gap: 1.75rem;
    align-items: start;
}
.issues-create-main { min-width: 0; }

.issues-create-label {
    display: block;
    font-size: 12px; font-weight: 600;
    color: var(--text);
    margin: 1rem 0 .35rem;
}
.issues-create-required { color: #dc2626; margin-left: .15rem; }
.issues-create-title-input {
    font-size: 14px;
    height: 40px;
}

.issues-create-actions {
    display: flex; gap: .5rem; justify-content: flex-end;
    margin-top: 1rem;
}

/* Sidebar */
.issues-create-sidebar {
    display: flex; flex-direction: column;
    gap: .75rem;
}
.issues-side-card {
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--surface);
    overflow: hidden;
}
.issues-side-card[open] .issues-side-chevron { transform: rotate(180deg); }
.issues-side-card-head {
    list-style: none;
    cursor: pointer;
    display: flex; align-items: center; justify-content: space-between;
    padding: .65rem .85rem;
    font-size: 12px; font-weight: 600;
    color: var(--text);
    background: var(--surface-2);
    border-bottom: 1px solid transparent;
}
.issues-side-card[open] .issues-side-card-head { border-bottom-color: var(--border); }
.issues-side-card-head::-webkit-details-marker { display: none; }
.issues-side-chevron {
    color: var(--text-muted);
    transition: transform .15s ease;
}
.issues-side-card-body {
    padding: .35rem 0;
    max-height: 320px;
    overflow-y: auto;
}
.issues-side-empty {
    padding: .75rem .9rem;
    color: var(--text-muted);
    font-size: 12px;
    margin: 0;
}
.issues-side-options {
    list-style: none; padding: 0; margin: 0;
}
.issues-side-options li { border-top: 1px solid transparent; }
.issues-side-options li + li { border-top-color: var(--border); }
.issues-side-option {
    display: flex; align-items: center; gap: .55rem;
    padding: .5rem .85rem;
    cursor: pointer;
    font-size: 12px;
    color: var(--text);
}
.issues-side-option:hover { background: var(--surface-2); }
.issues-side-option input[type="checkbox"] {
    flex-shrink: 0;
    accent-color: var(--primary);
}
.issues-side-option-avatar { flex-shrink: 0; }
.issues-mini-avatar {
    /* LAYOUT + size only. Fill / hue / weight / color come from
       the canonical `.bf-avatar-letter` rule. */
    width: 22px; height: 22px;
    border-radius: 50%;
    object-fit: cover;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 10px;
}
.issues-side-option-text {
    display: flex; flex-direction: column;
    min-width: 0;
}
.issues-side-option-text strong { font-weight: 600; color: var(--text); }
.issues-side-option-meta {
    font-size: 11px; color: var(--text-muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Label chip - GitHub-style solid pill.
   The chip's background IS the label colour (no tint, no
   border-tint, no dot). The text colour flips between
   near-black (#1f2328) and white based on the bg's WCAG
   relative luminance - computed server-side by
   IssueLabelColorHelper.ForegroundFor and emitted as the
   inline `--label-fg` custom property. Drops the previous
   "26 % tint + dark text + dot" treatment that made every
   label read as the same muddy pill regardless of its colour
   identity. Compact 0/8 padding + 2em radius matches
   GitHub's gh-fg labels exactly. */
.issues-label-chip {
    /* Standard issue-label paint: padding 0 10px, font-size 12,
       weight 500, line-height 22, 2em radius. The chip reads as
       a single flat coloured surface so the label's hue carries
       the full identity. Foreground is picked server-side
       (IssueLabelColorHelper.ForegroundFor) via WCAG luminance
       so pale yellows / cyans get black text and saturated reds
       / blues get white text.

       An inset 1px hairline at 15% black keeps the chip
       readable even when the label's catalog color is white /
       near-white (e.g. the default "wontfix"), which would
       otherwise paint invisibly against the page background.
       The hairline sits INSIDE the chip so its outer geometry
       (alignment, wrap math) doesn't shift on theme switches. */
    display: inline-block;
    padding: 0 10px;
    background: var(--label-color, var(--text-subtle));
    color: var(--label-fg, #ffffff);
    border: 1px solid transparent;
    border-radius: 2em;
    box-shadow: inset 0 0 0 1px rgba(27, 31, 36, 0.15);
    font-size: 12px;
    font-weight: 500;
    line-height: 22px;
    text-decoration: none;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
    letter-spacing: 0;
    white-space: nowrap;
    max-width: 22em;
    overflow: hidden;
    text-overflow: ellipsis;
    vertical-align: middle;
}
.issues-label-chip:hover {
    /* Match GitHub: shift the bg one tone darker on hover, keep
       the fg locked. brightness() filter is fine because the bg
       is solid; no transparency to multiply against. */
    filter: brightness(0.94);
    color: var(--label-fg, #ffffff);
    text-decoration: none;
}
/* Legacy dot: kept in the DOM by some existing call sites.
   GitHub-style chips don't show a dot - the whole bg already
   tells you the colour identity. Hide rather than rip-out so
   we don't have to touch every emission site at once. */
.issues-label-chip > .issues-label-dot { display: none; }
.issues-label-dot {
    width: 8px; height: 8px;
    border-radius: 50%;
    background: color-mix(in srgb, var(--label-color) 80%, #000 20%);
    flex-shrink: 0;
}

@media (max-width: 960px) {
    .issues-create-grid { grid-template-columns: 1fr; }
    .issues-create-sidebar { order: 2; }
}

/* Assignees + labels row on the issue detail page header. Sits under
   the meta line ("opened by …") so the tracker context is visible
   at a glance without opening pickers. */
.issues-detail-tags {
    margin-top: .55rem;
    display: flex; flex-wrap: wrap; gap: .9rem;
    font-size: 12px;
}
.issues-detail-tag-group {
    display: inline-flex; align-items: center; gap: .35rem;
    flex-wrap: wrap;
}
.issues-detail-tag-label {
    color: var(--text-muted);
    font-weight: 500;
}
.issues-detail-assignee {
    display: inline-flex; align-items: center; gap: .3rem;
    padding: .15rem .5rem .15rem .15rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 999px;
    font-size: 11px;
    color: var(--text);
}
.issues-detail-assignee .issues-mini-avatar {
    width: 18px; height: 18px;
    font-size: 9px;
}

/* ── Submit-spinner standard ─────────────────────────────────────
   Applied automatically to any <form method="post"> that doesn't
   carry data-no-spin. JS in submit-spinner.js adds .is-submitting
   on the form and on the ONE submit button that was pressed, and
   .is-held on the form's other submit buttons; CSS swaps in the
   spinner glyph on the pressed button + dims the rest of the form
   so the user sees the request is in flight and can't double-fire. */
form.is-submitting {
    cursor: progress;
}
/* Visually quiet the form's secondary controls while a request is
   in flight. We do NOT use disabled here: disabled inputs strip
   from the submitted form data, which kills the antiforgery token
   and lands the request in a 400. Dim + block pointer interaction
   only. The submit button itself stays disabled (handled in JS)
   so a double-click can't re-fire. Pinned by
   SubmitSpinnerStandardTests.SpinnerCannotStripAntiforgeryToken. */
form.is-submitting :is(input, textarea, select, button[type="button"], a):not([data-submit-live]) {
    opacity: .55;
    pointer-events: none;
}
/* Controls that must stay USABLE while the form is in flight - above all a
   Cancel that aborts the very request (data-ajax-abort): a lock the user
   cannot escape from is not a lock, it is a trap. */
form.is-submitting [data-submit-live] { opacity: 1; pointer-events: auto; }
/* The form's OTHER submit buttons while one verb is in flight: held, not
   working. The JS disables them so a second verb cannot fire; they step
   back like the rest of the form and never wear the spinner - two verbs
   in one form (Save / Test, Validate / Reject, Verify / Import) spin ONE
   button, the one that was pressed (owner, 2026-09-10). */
button.is-held,
input[type="submit"].is-held {
    opacity: .55;
    pointer-events: none;
}
button.is-submitting,
input[type="submit"].is-submitting {
    position: relative;
    color: transparent !important;
    pointer-events: none;
    /* Keep border + bg so the visual mass of the button is unchanged. */
}
button.is-submitting > *,
input[type="submit"].is-submitting > * {
    visibility: hidden;
}
button.is-submitting::after,
input[type="submit"].is-submitting::after {
    content: "";
    position: absolute;
    inset: 0;
    margin: auto;
    width: 14px; height: 14px;
    border: 2px solid currentColor;
    border-top-color: transparent;
    border-radius: 50%;
    /* Re-inherit color via the parent button's text color (overridden
       to transparent above; -webkit-text-fill-color keeps the spinner
       solid in Safari). */
    color: var(--gh-fg-default, #1f2328);
    -webkit-text-fill-color: var(--gh-fg-default, #1f2328);
    animation: bf-spin .7s linear infinite;
    pointer-events: none;
}
/* Primary green (.issues-new-submit) and indigo (.btn-primary) keep
   their own white spinner. */
.issues-new-submit.is-submitting::after,
.btn-primary.is-submitting::after,
.btn-success.is-submitting::after { color: #fff; -webkit-text-fill-color: #fff; }

/* In-flight override for primary buttons. The submit-spinner JS
   sets `disabled = true` after locking the form, which lights up
   the .btn-primary:disabled rule above (background -> surface-2,
   color -> muted) and produces a near-white pill against which
   the white spinner is invisible. The user-reported "+ Invite"
   regression on /Teams: button looked blank during processing.
   While submitting, keep the primary's normal indigo so the
   white spinner reads as "this action is in progress" instead
   of "the button silently broke." Pinned by
   SubmitSpinnerStandardTests.SpinnerCss_PrimaryButtonKeepsIndigoBackground_WhileSubmittingAndDisabled. */
.btn-primary.is-submitting,
.btn-primary.is-submitting:disabled,
.btn-primary.is-submitting.disabled,
.btn-primary.is-submitting.is-disabled {
    background: var(--text) !important;
    border-color: var(--primary) !important;
    /* Hide the label so only the white ::after spinner reads on
       the indigo surface. The base `button.is-submitting` rule
       sets `color: transparent !important`, but the disabled-state
       override above has higher selector-specificity than the
       generic submitting rule and wins, so we must restate
       transparent here for the in-flight + disabled compound state.
       Without this, the underlying label remains visible while
       the spinner spins ON TOP of it - reading as a flickering
       smear rather than a clean "in progress" affordance. */
    color: transparent !important;
    -webkit-text-fill-color: transparent !important;
    cursor: progress;
}
.btn-success.is-submitting,
.btn-success.is-submitting:disabled,
.btn-success.is-submitting.disabled {
    /* Match the primary contract for the green variant so the
       same in-flight visual works on success-styled buttons. */
    cursor: progress;
}

@keyframes bf-spin { to { transform: rotate(360deg); } }

/* ── Mail compose send-button: premium "Sending…" state ──────────
   The default in-flight rendering (transparent label + thin grey
   spinner inside a faded button) made the Send action almost
   invisible - the user reported "I can't tell if it's sending".
   For the mail send specifically we keep the button at full
   primary color, show an explicit "Sending…" label, position the
   spinner alongside it, and add a soft expanding ring so the
   activity reads at a glance.

   Scoped to the two mail compose submits via their stable data
   attributes: `[data-compose-new-submit]` (new mail) and
   `[data-compose-submit]` (reply). Other primary buttons keep
   their existing minimal spinner. */
/* Scroll-margin gutter used by the mail-compose controller's
   `keepComposeFooterInView` (mirror of the markdown editor's
   submit-gutter). When the contenteditable body editor grows past
   the visible scroll area and we call
   `submitButton.scrollIntoView({block:'nearest'})`, the browser
   honours this margin and leaves a 24px breathing band beneath
   Send / Save as draft so the buttons never sit flush against
   the bottom edge of the pane. Pinned indirectly via the
   controller's call-site comment. */
[data-compose-submit],
[data-compose-new-submit] {
    scroll-margin-bottom: 24px;
}
[data-compose-new-submit].is-submitting,
[data-compose-submit].is-submitting,
[data-compose-new-submit].is-submitting:disabled,
[data-compose-submit].is-submitting:disabled {
    background: var(--text) !important;
    border-color: var(--text) !important;
    color: transparent !important;
    box-shadow: 0 1px 2px rgba(15, 23, 42, .08) !important;
    opacity: 1 !important;
    cursor: progress !important;
    /* Reserve enough room for "Sending…" + spinner without making
       the button jump width when it transitions in. min-width is
       ALSO set inline by submit-spinner.js (to the captured idle
       width); we max() against a sensible lower bound here so a
       narrow original button still has room for the new content. */
    min-width: 120px !important;
    /* Subtle breathing ring - communicates "live process" without
       the noise of a continuous bar animation. */
    animation: bf-send-pulse 1.6s ease-out infinite;
}
/* "Sending…" label, painted via ::before so the original button
   content (SVG + Send text) stays hidden by the global
   `is-submitting` rules above. */
[data-compose-new-submit].is-submitting::before,
[data-compose-submit].is-submitting::before {
    content: "Sending…";
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    /* Clear the spinner (left:14px + 14px wide ends at ~28px) AND add a
       gap so the label doesn't crowd the glyph. Centered text shifts
       right by half the padding increase, so this opens a visible band
       between spinner and "Sending…". */
    padding-left: 38px;
    color: #fff !important;
    -webkit-text-fill-color: #fff !important;
    font: inherit;
    font-weight: 500;
    letter-spacing: .01em;
    pointer-events: none;
    /* The base `is-submitting` rule sets the parent's color to
       transparent; explicit color above wins because the cascade
       resolves on the pseudo-element specifically. */
}
/* Spinner relocated to the LEFT of the label. We override the
   global ::after centering (inset:0 + margin:auto) with an
   explicit top/left so it lines up next to the Sending… text.
   Note on centering: we pre-compute the vertical offset
   (`calc(50% - 7px)` where 7px is half the spinner height)
   instead of `top: 50%; transform: translateY(-50%)` because the
   rotation animation later replaces the entire `transform`
   value. With translateY in the transform, every animation
   frame would clobber the vertical centering and the spinner
   would jitter up + down once per rotation. Pre-computing keeps
   `transform` free for rotation alone. */
[data-compose-new-submit].is-submitting::after,
[data-compose-submit].is-submitting::after {
    inset: auto;
    top: calc(50% - 7px);
    left: 14px;
    transform: none;
    margin: 0;
    width: 14px;
    height: 14px;
    border: 2px solid #fff;
    border-top-color: transparent;
    color: #fff;
    -webkit-text-fill-color: #fff;
    /* Slightly slower than the default .7s - easier on the eye for
       a longer-running send (mailservers can take a few seconds). */
    animation: bf-spin 0.85s linear infinite;
}
@keyframes bf-send-pulse {
    0%   { box-shadow: 0 0 0 0   rgba(79, 70, 229, .45), 0 1px 2px rgba(15, 23, 42, .08); }
    70%  { box-shadow: 0 0 0 8px rgba(79, 70, 229, 0),    0 1px 2px rgba(15, 23, 42, .08); }
    100% { box-shadow: 0 0 0 0   rgba(79, 70, 229, 0),    0 1px 2px rgba(15, 23, 42, .08); }
}
/* Reduce-motion safety: keep the spinner (it's the affordance
   that tells the user "in flight"), drop the pulsing ring. */
@media (prefers-reduced-motion: reduce) {
    [data-compose-new-submit].is-submitting,
    [data-compose-submit].is-submitting {
        animation: none;
    }
}

/* ── Mention pill (rendered inline in comment bodies) ───────────────
   Replaces the raw email @-mention with avatar + display name pill;
   clickable, opens the shared profile-card popover via
   data-user-profile-id. vertical-align: middle keeps the pill
   aligned with the surrounding text baseline; small horizontal
   margin gives breathing room from neighbouring words. */
.issues-mention {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 0 9px 0 2px;
    margin: 0 2px;
    /* Pin the pill to the surrounding text's effective line box so
       it doesn't push extra leading. The avatar is sized within the
       pill height, not above it. */
    height: 1.4em;
    line-height: 1.4em;
    background: color-mix(in srgb, var(--gh-accent-fg, #1f6feb) 12%, transparent);
    color: var(--gh-accent-fg, #1f6feb);
    border-radius: 999px;
    font-weight: 500;
    text-decoration: none;
    cursor: pointer;
    /* user-select:none so click-and-hold inside the chip doesn't
       start a text selection. Without it, browsers flip the cursor
       to the I-beam mid-click and the chip reads as a text input
       instead of a button. The chip IS clickable (role="button" /
       <a>); selecting its label is not a meaningful interaction. */
    user-select: none;
    -webkit-user-select: none;
    /* text-bottom aligns the pill's bottom with the surrounding
       prose's descent line. middle drifted above the x-height
       leaving the pill visibly floating; baseline put it too low.
       text-bottom is the closest match to the eye. */
    vertical-align: text-bottom;
    white-space: nowrap;
}
.issues-mention:hover {
    background: color-mix(in srgb, var(--gh-accent-fg, #1f6feb) 22%, transparent);
}
/* Scoped to .issues-mention so it doesn't collide with the unrelated
   .issues-mention-avatar rule used by the @-mention popover above.
   Avatar matches the pill's inner height (text-line minus the 2px
   left padding) so it reads as part of the pill, not floating in. */
.issues-mention > .issues-mention-avatar {
    width: 1.2em; height: 1.2em;
    border-radius: 50%;
    object-fit: cover;
    flex-shrink: 0;
}
/* Letter-bubble fallback when the mentioned user has no uploaded
   photo. Routes through `var(--avatar-bg)` so the canonical helper's
   `--avatar-hue` controls the colour - same person, same colour
   everywhere. */
.issues-mention > .issues-mention-avatar-letter {
    /* LAYOUT + size only. Fill / hue / weight / color come from
       the canonical `.bf-avatar-letter` rule. */
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 10px;
    line-height: 1;
}
.issues-mention .issues-mention-name {
    font-size: inherit;
}

/* ── Team-mention chip (rendered by IssueMarkdownRenderer's
   ExpandTeamMentionPills post-processor) ─────────────────────────
   Mirrors the user-mention pill geometry but reads as "different
   kind" via the muted-canvas pill background and the squared glyph
   (the workspace-icon paint, not a circular avatar). Inline so it
   sits in prose without breaking the line. */
.issues-team-mention {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    /* The SAME pill as a person's mention (owner, 2026-09-13, a team
       mention rendered as a 60px card in the comment preview: "very bad
       appearance... we need the same kind of layout as for user, with
       better alignment"): the text line's height, the face tucked at the
       left edge, the words after it. What says "team" is the face - a
       rounded square in the team's own paint, never a circle - and the
       quiet neutral tint in place of the person's accent tint. */
    height: 1.4em;
    line-height: 1.4em;
    padding: 0 9px 0 2px;
    margin: 0 2px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--text, #1f2328) 7%, transparent);
    color: var(--text, #1f2328);
    text-decoration: none;
    vertical-align: text-bottom;
    white-space: nowrap;
    transition: background-color 80ms;
    cursor: pointer;
    /* user-select:none - same rationale as .issues-mention above:
       the chip is clickable (it's an anchor); a click-and-hold
       inside the label shouldn't flip the cursor to the I-beam or
       start a text selection. */
    user-select: none;
    -webkit-user-select: none;
}
.issues-team-mention:hover {
    background: color-mix(in srgb, var(--text, #1f2328) 12%, transparent);
    text-decoration: none;
}
/* The team's face inside the pill: the canonical workspace-icon paint
   (.bf-icon), whose EVERY inner size - the letter's !important font-size,
   the svg's 0.83 fill, the emoji - derives from --bf-icon-size. Setting
   width and height alone left that variable at its 56px default, so the
   glyph drew at 46px inside an 18px box and the pill grew into a card.
   The variable is the one knob; the radius stays a rounded square. */
.issues-team-mention .issues-mention-team-glyph.bf-icon {
    --bf-icon-size: 18px;
    --bf-icon-radius: 4px;
    width: 18px;
    height: 18px;
    flex: 0 0 auto;
}
.issues-team-mention-name {
    font-weight: 500;
}

/* ── Digested internal-link chip ────────────────────────────────────
   Rendered by IssueMarkdownRenderer.ExpandInternalLinks (Issues / Wiki /
   Documents comments) and mirrored by the mail-notes JS. A pasted
   Burofolk URL - an issue, a mail message, a wiki page, a contact, a
   document, or a workspace home - becomes this compact typed chip
   instead of a raw platform URI. entity-ref.js hydrates a rich hover
   card from the data-* attributes; the chip itself is stable (no layout
   shift). Neutral surface + squared, type-tinted glyph = the same
   "reference" language as the team-mention chip, one notch quieter. */
.bf-entity-ref {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 1px 7px 1px 5px;
    margin: 0 1px;
    max-width: 22em;
    border-radius: 6px;
    border: 1px solid var(--gh-border, #d0d7de);
    background: var(--gh-canvas-subtle, #f6f8fa);
    color: var(--gh-fg-default, #1f2328);
    font-weight: 500;
    line-height: 1.5;
    text-decoration: none;
    white-space: nowrap;
    vertical-align: baseline;
    cursor: pointer;
    user-select: none;
    -webkit-user-select: none;
    transition: background-color 80ms ease, border-color 80ms ease;
}
.bf-entity-ref:hover {
    background: color-mix(in srgb, var(--gh-canvas-subtle, #f6f8fa) 70%, var(--gh-fg-muted, #57606a) 12%);
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
    text-decoration: none;
}
.bf-entity-ref-icon {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    color: var(--primary);
}
.bf-entity-ref-label {
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
}
/* Per-kind icon accent so the type reads at a glance without shouting. */
.bf-entity-ref-issue .bf-entity-ref-icon { color: #1f883d; }
.bf-entity-ref-mailmessage .bf-entity-ref-icon { color: #0969da; }
.bf-entity-ref-wikipage .bf-entity-ref-icon { color: #0891b2; }
.bf-entity-ref-contact .bf-entity-ref-icon,
.bf-entity-ref-company .bf-entity-ref-icon { color: #bf3989; }
.bf-entity-ref-document .bf-entity-ref-icon { color: #9a6700; }
/* No-access state (entity-ref.js adds .is-locked once the hover fetch
   reports the viewer can't see the target workspace). */
.bf-entity-ref.is-locked {
    color: var(--gh-fg-muted, #57606a);
    cursor: default;
}
.bf-entity-ref.is-locked .bf-entity-ref-icon { color: var(--gh-fg-muted, #8b949e); }
html[data-theme="dark"] .bf-entity-ref {
    border-color: rgba(255, 255, 255, .14);
    background: rgba(255, 255, 255, .04);
}
html[data-theme="dark"] .bf-entity-ref:hover {
    background: rgba(255, 255, 255, .09);
}

/* Hover card for an entity-ref chip (built by entity-ref.js). Fixed /
   viewport-positioned, informational only (pointer-events: none so it
   never traps the cursor), flips + clamps at the edges in JS. */
.bf-entity-pop {
    position: fixed;
    z-index: 3000;
    max-width: 320px;
    min-width: 150px;
    padding: 10px 12px;
    border-radius: 10px;
    border: 1px solid var(--gh-border, #d0d7de);
    background: var(--gh-canvas, #fff);
    box-shadow: 0 8px 28px rgba(15, 23, 42, .16);
    pointer-events: none;
    font-size: 13px;
    line-height: 1.45;
    color: var(--gh-fg-default, #1f2328);
}
.bf-entity-pop-head {
    display: flex;
    align-items: flex-start;
    gap: 7px;
    font-weight: 650;
}
.bf-entity-pop-icon {
    flex: 0 0 auto;
    display: inline-flex;
    margin-top: 1px;
    color: var(--primary);
}
.bf-entity-pop-issue .bf-entity-pop-icon { color: #1f883d; }
.bf-entity-pop-mailmessage .bf-entity-pop-icon { color: #0969da; }
.bf-entity-pop-wikipage .bf-entity-pop-icon { color: #0891b2; }
.bf-entity-pop-contact .bf-entity-pop-icon,
.bf-entity-pop-company .bf-entity-pop-icon { color: #bf3989; }
.bf-entity-pop-document .bf-entity-pop-icon { color: #9a6700; }
.bf-entity-pop-head.is-locked .bf-entity-pop-icon { color: var(--gh-fg-muted, #8b949e); }
.bf-entity-pop-title {
    min-width: 0;
    overflow: hidden;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}
.bf-entity-pop-sub {
    margin-top: 3px;
    color: var(--gh-fg-muted, #57606a);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.bf-entity-pop-meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 6px;
    font-size: 12px;
    color: var(--gh-fg-muted, #57606a);
}
.bf-entity-pop-status {
    padding: 0 7px;
    line-height: 1.5;
    border: 1px solid var(--gh-border, #d0d7de);
    border-radius: 999px;
    font-weight: 550;
}
.bf-entity-pop-locked {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-top: 6px;
    color: var(--gh-fg-muted, #57606a);
    font-size: 12px;
}
html[data-theme="dark"] .bf-entity-pop {
    background: var(--gh-canvas, #161b22);
    border-color: rgba(255, 255, 255, .14);
    box-shadow: 0 10px 32px rgba(0, 0, 0, .5);
}

/* ── Issue activity timeline event row ──────────────────────────────
   Rendered between comment bubbles inside .issues-thread. One-line,
   muted, no avatar - keeps the conversation visually dominant while
   the event still shows in time order. */
.issues-thread-event {
    /* Read-only timeline prose - I-beam would imply the row is
       editable. Default cursor + still-selectable text matches
       the pattern on .note-text / .message-body / .email-body. */
    cursor: default;
    user-select: text;
    position: relative;
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 6px 0 6px 56px;
    color: var(--gh-fg-muted);
    font-size: 13px;
    line-height: 1.4;
    z-index: 1;
}

/* Actor avatar in the timeline event column. Sits in the same
   spot the dot would have - a smaller round avatar so the
   conversation column still reads as the dominant surface. The
   inner <img>/letter bubble paints via the canonical .bf-avatar
   rule (single avatar source of truth). */
.issues-thread-event-avatar {
    position: absolute;
    left: 12px;
    width: 20px; height: 20px;
    border-radius: 50%;
    overflow: hidden;
    background: var(--gh-canvas);
    border: 2px solid var(--gh-canvas);
    box-sizing: content-box;
    cursor: pointer;
    flex: 0 0 auto;
}
.issues-thread-event-avatar-img,
.issues-thread-event-avatar img,
.issues-thread-event-avatar .bf-avatar-letter {
    width: 100%; height: 100%;
    border-radius: 50%;
    display: block;
    font-size: 11px;
    line-height: 20px;
}
/* Admin-mode automation rows: the dedicated org "Admin" shield fills the same
   20px slot as a user avatar. The bubble's fill / glyph come from the canonical
   `.bf-icon` engine (AdminAvatar); `--bf-icon-size` scales the glyph to the
   slot. The wrapper stays a click target for the org-automation identity card. */
.issues-thread-event-avatar-admin { cursor: pointer; }
.issues-thread-event-avatar .issues-thread-event-admin-icon {
    width: 100%; height: 100%;
    --bf-icon-size: 20px;
    border-radius: 50%;
    display: block;
}
.issues-thread-event-admin-name { cursor: pointer; }
.issues-thread-event-admin-name:hover {
    text-decoration: underline;
    text-underline-offset: 2px;
}
.issues-thread-event-text {
    display: inline-flex;
    align-items: baseline;
    gap: 6px;
    flex-wrap: wrap;
    min-width: 0;
}
.issues-thread-event-text strong {
    color: var(--gh-fg-default);
    font-weight: 600;
}
.issues-thread-event-text strong[data-user-profile-id] { cursor: pointer; }
.issues-thread-event-text strong[data-user-profile-id]:hover {
    text-decoration: underline;
    text-underline-offset: 2px;
}
.issues-thread-event-pill {
    background: var(--gh-canvas-subtle);
    border: 1px solid var(--gh-border-muted);
    border-radius: 4px;
    padding: 1px 6px;
    font-family: inherit;
    font-size: 12px;
    color: var(--gh-fg-default);
}
/* LinkChanged rows: the other issue's #ref as a live link (issue #265). */
.issues-thread-event-ref {
    color: var(--gh-accent-fg);
    text-decoration: none;
    font-variant-numeric: tabular-nums;
}
.issues-thread-event-ref:hover { text-decoration: underline; }
/* A ref into a project this reader cannot open: same slot, but muted and
   inert - it never pretends to be a destination. */
.issues-thread-event-ref.is-unreachable {
    color: var(--gh-fg-muted);
    cursor: default;
}
.issues-thread-event-ref.is-unreachable:hover { text-decoration: none; }
/* Inline label swatch in the timeline - smaller than sidebar
   chips so the row stays one tight line, but keeps the same
   colour wash as everywhere else. */
.issues-thread-event-label {
    font-size: 12px;
    line-height: 1;
    padding: 2px 8px;
    height: auto;
    align-self: center;
}
.issues-thread-event-time {
    color: var(--gh-fg-muted);
}

/* ── Inline About-this-board editor on workspace detail ────────────
   Lives between the Workspace header and Access & permissions.
   Shows the markdown source in a textarea for editors; renders the
   blurb read-only otherwise. */
.ws-detail-about-form { display: flex; flex-direction: column; gap: 10px; }
.ws-detail-about-textarea {
    width: 100%;
    min-height: 110px;
    padding: 10px 12px;
    font-family: inherit;
    font-size: 14px;
    line-height: 1.55;
    color: var(--text);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    resize: vertical;
    overflow-wrap: anywhere;
    word-break: break-word;
}
.ws-detail-about-textarea:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 25%, transparent);
}
.ws-detail-about-actions {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
}
.ws-detail-about-hint { color: var(--text-muted); font-style: normal; font-size: 13px; }
.ws-detail-about-readonly {
    padding: 12px 14px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    overflow-wrap: anywhere;
    word-break: break-word;
}
.ws-detail-about-readonly > :first-child { margin-top: 0; }
.ws-detail-about-readonly > :last-child { margin-bottom: 0; }

/* ── Documents grouped settings on workspace detail ────────────────
   One hairline card per settings group (Search & AI, Sharing & guests,
   Upload rules, Retention & compliance), each a stack of feature rows:
   name + one-line consequence on the left, the control (canonical
   notif-prefs-toggle switch, content-sized bf-select, or text input)
   on the right. Every control auto-saves on change (settings-autosave.js
   + data-ajax-form) - no per-row Save buttons. */
.ws-feature-rows { display: flex; flex-direction: column; }
/* A multi-row config form must not break the row stack. */
.ws-feature-config { display: contents; }
.ws-feature-row {
    display: flex;
    align-items: center;
    gap: 14px;
    min-height: 44px;
    padding: 4px 2px;
    /* Narrow frames: the row STACKS rather than shredding the label into one
       word per line or pushing the control past the card edge. Paired with the
       text column's basis below, the control drops WHOLE to the next line -
       the charter's rule - and stays content-sized there, never stretched. */
    flex-wrap: wrap;
}
.ws-feature-row + .ws-feature-row,
.ws-feature-config + .ws-feature-row,
.ws-feature-row + .ws-feature-config > .ws-feature-row:first-child,
.ws-feature-config + .ws-feature-config > .ws-feature-row:first-child,
.ws-feature-config .ws-feature-row + .ws-feature-row {
    border-top: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
}
/* The label keeps a readable floor (15rem): the row wraps the control away
   before the text is ever squeezed into a one-word-per-line column. */
/* ONE line per row: the label, its ? (the note lives in the tooltip), the
   control. A row with no control says its fact as a value at the right edge. */
.ws-feature-text { display: flex; align-items: center; min-width: 0; flex: 1 1 15rem; }
.ws-feature-text strong { font-size: 14px; font-weight: 600; color: var(--text); }
.ws-feature-text .pc-help { flex: 0 0 auto; }
.ws-feature-value { margin-left: auto; font-size: 13px; color: var(--text-muted); text-align: right; }
.ws-feature-text em { font-style: normal; font-size: 13px; color: var(--text-muted); }
.ws-feature-form { flex: 0 0 auto; display: inline-flex; }
.ws-feature-row.is-unavailable .ws-feature-text { opacity: .6; }
/* A dependent SUB-permission row (e.g. "Members can share" under "External
   share links"): indented so its parent-child relation reads at a glance. */
.ws-feature-subrow { padding-left: 22px; }
.notif-prefs-toggle.is-disabled { opacity: .45; cursor: not-allowed; }
/* Controls size to content, never the row (premium dropdown rule) - but they
   never outgrow the frame either: the 12rem floor yields when the row is
   narrower, and the trigger label ellipsizes instead of spilling. */
.ws-feature-row .bf-select { flex: 0 0 auto; min-width: 0; max-width: 100%; }
.ws-feature-row .bf-select-btn { width: auto; min-width: min(12rem, 100%); max-width: 100%; }
.ws-feature-form { max-width: 100%; }
.ws-settings-select {
    padding: 8px 10px;
    font-family: inherit;
    font-size: 14px;
    color: var(--text);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
}
.ws-settings-input {
    flex: 0 0 auto;
    width: 16rem;
    max-width: 45%;
    padding: 8px 10px;
    font-family: inherit;
    font-size: 14px;
    color: var(--text);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
}
.ws-settings-input:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 25%, transparent);
}
/* Retention-lock banner: quiet frosted note with the lock glyph - the
   state signal for the frozen rows below it. */
.ws-retention-lock-note {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 9px 12px;
    margin-bottom: 6px;
    font-size: 13px;
    color: var(--text-muted);
    border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
    border-radius: 8px;
}
.ws-retention-lock-note svg { width: 14px; height: 14px; flex: 0 0 auto; }
/* Storage & connection card (a Drive-backed space only): the feature-row grammar - the fact's
   name in ink at the left, its value at the right edge, one hairline between
   rows (owner, 2026-09-05: the label/value table read as loose text). */
.ws-storage-lines { display: flex; flex-direction: column; }
.ws-storage-line {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px 14px;
    min-height: 44px;
    padding: 4px 2px;
}
.ws-storage-line + .ws-storage-line { border-top: 1px solid color-mix(in srgb, var(--border) 60%, transparent); }
.ws-storage-line-label { flex: 1 1 12rem; min-width: 0; font-size: 14px; font-weight: 600; color: var(--text); }
.ws-storage-line-value { margin-left: auto; font-size: 13px; color: var(--text-muted); text-align: right; }

/* Google Drive connection block inside the Storage & connection card:
   sync status rides a flat dot + plain text (house rule - never a pill);
   disconnect / reconnect follow as small buttons with the reversibility
   note. The dot classes are shared with the hero-state health line. */
.ws-drive-conn { margin-top: 4px; border-top: 1px solid color-mix(in srgb, var(--border) 60%, transparent); }
.ws-drive-conn-status { font-weight: 600; }
.ws-drive-conn-status.is-ok { color: var(--success, #16a34a); }
.ws-drive-conn-status.is-warn { color: var(--warning, #b45309); }
.ws-drive-conn-status.is-off { color: var(--text-subtle); }
.ws-drive-conn-detail { margin: 2px 0 6px; font-size: 13px; color: var(--text-muted); line-height: 1.5; }
.ws-drive-conn-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 10px 2px 2px; }
.ws-drive-conn-note { flex-basis: 100%; margin: 2px 0 0; font-size: 13px; color: var(--text-muted); line-height: 1.5; }
.ws-drive-conn-hero { display: inline-flex; align-items: center; gap: 7px; }

/* ── Viewer-permission defaults on workspace detail ──
   Read-only per-type defaults summary rendered inside the shared
   "Access & permissions" section (the AccessExtraPartial slot), below
   the collaborator list. State reads from text + weight only - no
   filled wash. */
.ws-detail-access-defaults {
    margin-top: 14px;
    padding-top: 14px;
    border-top: 1px solid var(--border);
}
.ws-detail-access-defaults-title {
    margin: 0 0 6px;
    font-size: 10px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .08em;
    color: var(--text-muted);
}
.ws-detail-access-list { list-style: none; margin: 0; padding: 0; }
.ws-detail-access-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 11px 2px;
}
.ws-detail-access-row + .ws-detail-access-row { border-top: 1px solid var(--border); }
.ws-detail-access-label { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.ws-detail-access-label strong { font-size: 14px; color: var(--text); }
.ws-detail-access-label em { font-style: normal; font-size: 13px; color: var(--text-muted); }
.ws-detail-access-state { flex: 0 0 auto; font-size: 13px; font-weight: 600; }
.ws-detail-access-state.is-on { color: var(--text); }
.ws-detail-access-state.is-off { color: var(--text-muted); font-weight: 500; }

/* ── Closed-issue banner ───────────────────────────────────────────
   Replaces the comment composer when the issue is in Closed state.
   Avatar column intentionally left empty (the banner is a system
   message, not a person's reply); the rest of the row aligns with
   the comment timeline so the page reads continuously. */
.issues-thread-closed-banner {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 14px 16px;
    background: var(--gh-canvas-subtle);
    border: 1px solid var(--gh-border-muted);
    border-radius: 8px;
    color: var(--gh-fg-muted);
    margin-left: 56px; /* lines up after the avatar column */
}
.issues-thread-closed-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px; height: 28px;
    background: color-mix(in srgb, #0891b2 18%, transparent);
    color: #0891b2;
    border-radius: 50%;
    flex-shrink: 0;
}
.issues-thread-closed-text {
    flex: 1; min-width: 0;
    display: flex; flex-direction: column; gap: 2px;
}
.issues-thread-closed-text strong { color: var(--gh-fg-default); }
.issues-thread-closed-sub { font-size: 13px; }
.issues-thread-closed-action { margin: 0; }

/* ── Confirm dialog (in-app, no native confirm()) ─────────────────
   Generic styled overlay used by [data-confirm-submit] forms. JS
   shows it before re-firing the actual submit. Same pattern can
   be used for any future destructive form. */
.bf-confirm-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(15, 23, 42, .35);
    display: flex; align-items: center; justify-content: center;
    z-index: 2050;
    animation: bf-confirm-fade .12s ease;
}
.bf-confirm-dialog {
    background: var(--surface);
    border-radius: 12px;
    box-shadow: 0 20px 60px rgba(15, 23, 42, .25);
    width: 92%;
    max-width: 420px;
    padding: 24px;
    color: var(--text);
    animation: bf-confirm-pop .14s cubic-bezier(.4, 1.4, .6, 1);
}
.bf-confirm-title {
    font-size: 16px;
    font-weight: 600;
    margin: 0 0 8px;
    color: var(--text);
}
.bf-confirm-body {
    font-size: 14px;
    color: var(--text-muted);
    margin: 0 0 20px;
    line-height: 1.5;
}
.bf-confirm-actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
}
/* The confirm's ONE opt-in (bfConfirm `option`): a bounded row - glyph plate,
   the word, the house switch at its end - OFF at open. */
.bf-confirm-option {
    position: relative;
    display: flex;
    align-items: center;
    gap: 10px;
    margin: -4px 0 20px;
    padding: 10px 12px;
    border: 1px solid var(--border);
    border-radius: 10px;
    cursor: pointer;
    transition: background .12s;
}
.bf-confirm-option:hover { background: color-mix(in srgb, var(--text) 3%, transparent); }
.bf-confirm-option:has(.bf-confirm-option-input:focus-visible) { outline: 2px solid var(--text); outline-offset: 2px; }
.bf-confirm-option-glyph {
    flex: 0 0 auto;
    width: 28px;
    height: 28px;
    display: inline-grid;
    place-items: center;
    border-radius: 8px;
    background: color-mix(in srgb, var(--text) 6%, transparent);
}
.bf-confirm-option-word { flex: 1 1 auto; min-width: 0; font-size: 14px; font-weight: 500; color: var(--text); }
.bf-confirm-option-input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
.bf-confirm-option-switch {
    flex: 0 0 auto;
    position: relative;
    width: 34px;
    height: 20px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--text) 18%, transparent);
    transition: background .15s;
}
.bf-confirm-option-switch::after {
    content: "";
    position: absolute;
    top: 2px;
    left: 2px;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: var(--surface);
    box-shadow: 0 1px 2px rgba(0, 0, 0, .25);
    transition: transform .15s;
}
.bf-confirm-option-input:checked ~ .bf-confirm-option-switch { background: var(--text); }
.bf-confirm-option-input:checked ~ .bf-confirm-option-switch::after { transform: translateX(14px); }
@media (prefers-reduced-motion: reduce) {
    .bf-confirm-option-switch, .bf-confirm-option-switch::after { transition: none; }
}
@keyframes bf-confirm-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes bf-confirm-pop  { from { transform: scale(.94); opacity: 0; } to { transform: scale(1); opacity: 1; } }

/* Activity row preview block for Issues: title strong + snippet
   muted-italic on the next line. Matches the existing activity-
   quote shape so the row reads like the other category quotes. */
.activity-issue { display: flex; flex-direction: column; gap: 2px; }
.activity-issue-snippet { color: var(--text-muted); font-style: italic; font-size: 13px; }

/* ── Issue reaction chips + picker ─────────────────────────────────
   Mirror of the notes-side reaction UI (Pages/Mail/Message.cshtml +
   .note-reaction-* CSS) so the user's muscle memory carries across
   surfaces. Single .issues-reaction-picker positioned by JS on the
   add-reaction click; chips render below each comment / issue body. */

.issues-reactions {
    display: flex; flex-wrap: wrap; align-items: center;
    gap: 6px;
    padding: 8px 16px 12px;
    background: var(--gh-canvas);
}
.issues-reactions-empty {
    /* Empty state: just the add-reaction button. Hide it until
       the bubble is hovered/focused so the resting state is clean. */
    padding: 4px 16px 8px;
    opacity: 0;
    transition: opacity .12s ease;
}
/* BOTH bubble shapes reveal it: a root comment (.issues-thread-bubble)
   and a thread reply (.issues-thread-reply-bubble). The replies block is
   a SIBLING of the root comment's <article>, never a descendant, so the
   root selector alone leaves the trigger on every reply at opacity 0 -
   the markup ships, the reaction is unreachable. */
.issues-thread-bubble:hover .issues-reactions-empty,
.issues-thread-reply-bubble:hover .issues-reactions-empty,
.issues-reactions-empty:focus-within { opacity: 1; }
/* Touch never hovers. Hiding the trigger behind an event that cannot
   fire would make reactions a desktop-only feature, so it stands
   revealed there. */
@media (hover: none) {
    .issues-reactions-empty { opacity: 1; }
}
/* A reply bubble pads its own content (8px 12px), so the row inside it
   keeps its vertical rhythm only - the row's own 16px would push the
   chips out of the reply's text column. */
.issues-thread-reply-bubble > .issues-reactions {
    padding: 6px 0 0;
    background: transparent;
}
.issues-thread-reply-bubble > .issues-reactions-empty { padding: 4px 0 0; }

.issues-reaction-form { margin: 0; display: inline-flex; }

.issues-reaction-chip {
    display: inline-flex; align-items: center; gap: 4px;
    padding: 1px 8px;
    height: 24px;
    background: var(--gh-canvas-subtle);
    border: 1px solid var(--gh-border-muted);
    border-radius: 999px;
    cursor: pointer;
    font-size: 13px;
    line-height: 1;
    color: var(--gh-fg-default);
    transition: background .12s ease, border-color .12s ease;
}
.issues-reaction-chip:hover {
    background: color-mix(in srgb, var(--gh-accent-fg, #1f6feb) 10%, var(--gh-canvas-subtle));
    border-color: color-mix(in srgb, var(--gh-accent-fg, #1f6feb) 35%, var(--gh-border));
}
.issues-reaction-chip.is-mine {
    /* Highlighted border + tint when the current user reacted with
       this emoji - same affordance as the notes chips. */
    background: color-mix(in srgb, var(--gh-accent-fg, #1f6feb) 14%, var(--gh-canvas-subtle));
    border-color: color-mix(in srgb, var(--gh-accent-fg, #1f6feb) 50%, var(--gh-border));
    color: var(--gh-accent-fg, #1f6feb);
}
.issues-reaction-emoji { font-size: 13px; line-height: 1; }
.issues-reaction-count { font-weight: 500; font-variant-numeric: tabular-nums; }

.issues-reaction-add {
    display: inline-flex; align-items: center; gap: 2px;
    padding: 1px 8px;
    height: 24px;
    background: transparent;
    border: 1px dashed var(--gh-border-muted);
    border-radius: 999px;
    cursor: pointer;
    color: var(--gh-fg-muted);
    transition: background .12s ease, color .12s ease, border-color .12s ease;
}
.issues-reaction-add:hover {
    background: var(--gh-canvas-subtle);
    color: var(--gh-fg-default);
    border-color: var(--gh-border);
    border-style: solid;
}
.issues-reaction-add-face { display: block; }
.issues-reaction-add-plus {
    font-size: 11px; font-weight: 700; line-height: 1;
    margin-left: 1px;
}

/* The shared picker popover. JS positions it next to the trigger.
   Renders all StandardReactionModel.Palette emojis as a wrapped
   grid with hover highlight. */
.issues-reaction-picker {
    /* VIEWPORT space, per the anchored-popover contract: the JS
       places it with position:fixed at open, flips/clamps into the
       viewport, and closes it on scroll/resize. overflow-y lets the
       grid scroll internally when the viewport cap bites on short
       screens. Pinned by IssuesReactionPickerStaysOnScreenTests. */
    position: fixed;
    z-index: 1100;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    box-shadow: 0 12px 32px rgba(15, 23, 42, .18);
    padding: 8px;
    display: grid;
    grid-template-columns: repeat(7, 32px);
    gap: 4px;
    overflow-y: auto;
}
/* The [hidden] attribute must always beat this rule's own display -
   without the guard, a closed panel stays painted (display:grid wins
   over the UA's [hidden] default) and swallows the next click. Same
   belt-and-braces the sibling .note-reaction-picker carries. */
.issues-reaction-picker[hidden] { display: none; }
.issues-reaction-picker .issues-reaction-form { margin: 0; }
.issues-reaction-pick {
    width: 32px; height: 32px;
    background: transparent; border: 0;
    border-radius: 8px;
    font-size: 18px; line-height: 1;
    cursor: pointer;
    display: inline-flex; align-items: center; justify-content: center;
    transition: background .12s ease, transform .08s ease;
}
.issues-reaction-pick:hover {
    background: var(--gh-canvas-subtle);
    transform: scale(1.1);
}

/* The issue-title vote control is the canonical .bf-vote widget
   (vote-control.css + vote-control.js + Html.VoteControl). Its
   appearance + behaviour live there; placement on the detail h1 is in
   vote-control.css (.issues-detail-title .bf-vote). */

/* ── Legacy vertical vote stack (unused, kept for reference) ───── */
.issues-vote-stack {
    width: 72px;
    margin: 8px 16px 16px 0;
    padding: 12px 8px;
    background: var(--gh-canvas-subtle, #f6f8fa);
    border: 1px solid var(--gh-border-muted, #d8dee4);
    border-radius: 12px;
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    float: left;
}
.issues-vote-stack .issues-vote-form { margin: 0; display: block; }
.issues-vote-arrow {
    width: 44px;
    height: 36px;
    border: 0;
    background: transparent;
    border-radius: 8px;
    color: var(--gh-fg-muted, #656d76);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: background .12s ease, color .12s ease, transform .08s ease;
}
.issues-vote-arrow:hover {
    background: var(--gh-canvas, #ffffff);
    color: var(--gh-fg-default, #1f2328);
    transform: scale(1.05);
}
.issues-vote-arrow-up.is-mine {
    background: color-mix(in srgb, var(--gh-success-emphasis) 22%, transparent);
    color: var(--gh-success-emphasis);
}
.issues-vote-arrow-up.is-mine:hover {
    background: color-mix(in srgb, var(--gh-success-emphasis) 30%, transparent);
}
.issues-vote-arrow-down.is-mine {
    background: color-mix(in srgb, #cf222e 22%, transparent);
    color: #cf222e;
}
.issues-vote-arrow-down.is-mine:hover {
    background: color-mix(in srgb, #cf222e 30%, transparent);
}
.issues-vote-score {
    font-size: 22px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--gh-fg-default, #1f2328);
    line-height: 1;
    margin-top: 2px;
}
.issues-vote-score.is-positive { color: var(--gh-success-emphasis); }
.issues-vote-score.is-negative { color: #cf222e; }
.issues-vote-label {
    font-size: 11px;
    color: var(--gh-fg-muted, #656d76);
    margin-bottom: 2px;
    text-transform: uppercase;
    letter-spacing: .08em;
}
@media (max-width: 720px) {
    .issues-vote-stack {
        width: 56px;
        padding: 8px 6px;
        margin-right: 12px;
    }
    .issues-vote-arrow { width: 36px; height: 30px; }
    .issues-vote-score { font-size: 18px; }
}

/* ── Issue hover preview popover ────────────────────────────────────
   GitHub-style hover card on issue links. Renders the issue's
   title + state + author + snippet without leaving the current
   page. The JS in issues-ui.js positions it next to the anchor;
   the popover handles its own enter/leave to stay open while the
   cursor is inside it. */
.issues-hover-pop {
    background: var(--surface, #ffffff);
    border: 1px solid var(--border, #d0d7de);
    border-radius: 12px;
    box-shadow: 0 14px 40px rgba(15, 23, 42, .14);
    padding: 0;
    width: 540px;
    max-width: 92vw;
    color: var(--text, #1f2328);
    font-size: 13px;
    line-height: 1.5;
    pointer-events: auto;
    overflow: hidden;
    display: flex;
    flex-direction: column;
}
.issues-hover-pop-body {
    padding: 16px 18px 12px;
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.issues-hover-pop-head {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    color: var(--text-muted, #656d76);
    font-size: 12px;
    line-height: 1.2;
}
.issues-hover-pop-ws { font-weight: 500; color: var(--gh-fg-default, #1f2328); }
.issues-hover-pop-num { color: var(--text-muted, #656d76); font-variant-numeric: tabular-nums; }
.issues-hover-pop-title {
    font-weight: 600;
    font-size: 14px;
    color: var(--gh-fg-default, #1f2328);
    line-height: 1.35;
}
.issues-hover-pop-meta {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-size: 12px;
}
.issues-hover-pop-state {
    display: inline-flex;
    align-items: center;
    padding: 2px 8px;
    border-radius: 999px;
    font-weight: 600;
    border: 1px solid color-mix(in srgb, currentColor 35%, transparent);
    font-size: 11px;
    letter-spacing: .01em;
}
.issues-hover-pop-state.is-open    { color: var(--gh-success-fg); }
.issues-hover-pop-state.is-closed  { color: var(--gh-fg-muted); }
.issues-hover-pop-author { color: var(--text-muted, #656d76); }
.issues-hover-pop-snippet {
    color: var(--text-muted, #656d76);
    font-size: 13px;
    line-height: 1.55;
    overflow-wrap: anywhere;
    word-break: break-word;
    margin-top: 4px;
    /* Cap to a reasonable preview length so the popover doesn't
       turn into a wall of text. */
    max-height: 8em;
    overflow: hidden;
}
/* Footer with the two action buttons (Open / Open in new tab).
   Lives at the bottom of the card, separated by a hairline border.
   Buttons sit on the right; primary action is the right-most so
   the eye naturally lands on "Open in new tab" first (matches
   typical dual-action dialog convention). */
.issues-hover-pop-foot {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 8px;
    padding: 10px 16px;
    border-top: 1px solid var(--border, rgba(15, 23, 42, .08));
    background: rgba(15, 23, 42, .015);
}
.issues-hover-pop-action {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 5px 10px;
    border-radius: 6px;
    font-size: 12px;
    font-weight: 500;
    color: var(--bf-text, #1f2328);
    background: transparent;
    border: 1px solid rgba(15, 23, 42, .15);
    text-decoration: none;
    transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
}
.issues-hover-pop-action:hover {
    background: color-mix(in srgb, var(--text) 5%, transparent);
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
    color: var(--text);
}
.issues-hover-pop-action.is-primary {
    background: var(--text);
    border-color: var(--text);
    color: var(--surface, #fff);
}
.issues-hover-pop-action.is-primary:hover {
    background: color-mix(in srgb, var(--text) 84%, var(--surface));
    border-color: transparent;
    color: var(--surface, #fff);
}
/* Destructive variant for the Dismiss button. Pushed to the LEFT
   edge via margin-right: auto so it visually separates from the
   navigate actions on the right (positive/safe actions remain
   anchored to the right edge). */
.issues-hover-pop-action.is-dismiss {
    margin-right: auto;
    color: var(--text-muted, #656d76);
    border-color: rgba(15, 23, 42, .12);
    background: transparent;
}
.issues-hover-pop-action.is-dismiss:hover {
    background: rgba(239, 68, 68, .10);
    border-color: rgba(239, 68, 68, .45);
    color: #dc2626;
}
html[data-theme="dark"] .issues-hover-pop-action.is-dismiss {
    color: rgba(255, 255, 255, .65);
    border-color: rgba(255, 255, 255, .15);
}
html[data-theme="dark"] .issues-hover-pop-action.is-dismiss:hover {
    background: rgba(239, 68, 68, .18);
    border-color: rgba(239, 68, 68, .50);
    color: #fca5a5;
}
.issues-hover-pop-action svg { flex-shrink: 0; }
html[data-theme="dark"] .issues-hover-pop-foot {
    background: rgba(255, 255, 255, .02);
    border-top-color: rgba(255, 255, 255, .08);
}
html[data-theme="dark"] .issues-hover-pop-action {
    color: rgba(255, 255, 255, .85);
    border-color: rgba(255, 255, 255, .15);
}
html[data-theme="dark"] .issues-hover-pop-action:hover {
    background: color-mix(in srgb, var(--text) 5%, transparent);
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
    color: var(--text);
}
/* The fallback popover carried the same defect as the unified one: the
   dark rule above repaints every action's label light, and the filled
   primary's slab IS light in dark mode - so "color: #fff" here painted
   white on white. Same fix, same grammar: dark label on the light
   slab. */
html[data-theme="dark"] .issues-hover-pop-action.is-primary,
html[data-theme="dark"] .issues-hover-pop-action.is-primary:hover {
    background: color-mix(in srgb, var(--text) 86%, var(--surface));
    border-color: transparent;
    color: var(--surface, #0f172a);
}

/* ── Auto-hiding overlay scrollbars ─────────────────────────────────
   macOS / MAUI-style: track stays invisible, thumb fades in on
   hover or scroll, fades out when idle. Apply to any element that
   should scroll without the bar shouting for attention.
   Use via class="bf-scroll-quiet" or by chaining into a custom
   selector. The .issues-detail-main rule above duplicates the
   recipe inline so the issue page doesn't depend on the class
   being applied. */
.bf-scroll-quiet {
    transition: scrollbar-color .25s ease;
    padding-right: 14px;
}
.bf-scroll-quiet:hover,
.bf-scroll-quiet:focus-within {
}
.bf-scroll-quiet::-webkit-scrollbar { width: 10px; height: 10px; }
.bf-scroll-quiet::-webkit-scrollbar-track { background: transparent; }
.bf-scroll-quiet::-webkit-scrollbar-thumb {
    border-radius: 999px;
    border: 2px solid transparent;
    transition: background-color .25s ease;
}
.bf-scroll-quiet:hover::-webkit-scrollbar-thumb,
.bf-scroll-quiet:focus-within::-webkit-scrollbar-thumb {
}
.bf-scroll-quiet::-webkit-scrollbar-thumb:hover {
    background-color: rgba(140, 149, 159, .85);
}

/* ── Vote score chip on issue list rows (prominent) ─────────────────
   Sits next to the comment-count chip. Green for net positive,
   red for net negative; we never render a chip for 0 (the row
   stays clean). Bigger + bolder than other meta chips so
   community demand is visible at a glance. */
.issues-list-row-score {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 4px 10px;
    height: 28px;
    border-radius: 999px;
    font-size: 13px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    flex-shrink: 0;
    border: 1px solid transparent;
}
.issues-list-row-score.is-positive {
    background: color-mix(in srgb, var(--gh-success-emphasis) 18%, transparent);
    color: var(--gh-success-emphasis);
    border-color: color-mix(in srgb, var(--gh-success-emphasis) 35%, transparent);
}
.issues-list-row-score.is-negative {
    background: color-mix(in srgb, #cf222e 18%, transparent);
    color: #cf222e;
    border-color: color-mix(in srgb, #cf222e 35%, transparent);
}

/* ───────────────────────── Image viewer (lightbox) ─────────────────────────
   Any container marked with [data-image-zoom] gets every <img> inside it
   (over 64x64) made clickable: image-viewer.js opens it in THE document
   viewer's lightbox (attachment-viewer.js / .css) - no overlay of its own.
*/

/* Cursor cue on opt-in images so users see they're clickable. The
   selector targets <img> inside an opt-in container, excluding
   explicit opt-outs and avatars. cursor: zoom-in is the standard
   browser affordance for "click to enlarge". */
[data-image-zoom] img:not(.avatar-img):not([data-no-zoom]) {
    cursor: zoom-in;
}

/* Body scroll lock while the viewer is up - the JS toggles this
   class so the page behind the dim backdrop can't scroll out from
   under the user. */
/* Scroll lock: via Burofolk.ScrollLock (bf-scroll-locked). */

/* ── The ajax-form FREEZE veil (data-ajax-freeze, ajax-form.js) ─────────────
   A long mutation (bulk trash) locks the whole page under this veil with the
   form's own progress wording until the swap lands. Above the confirm dialog
   (2050) - the action often starts from one - below the toasts (2400), so the
   outcome toast is never covered. The card opens at its final size. */
.bf-ajax-freeze {
    position: fixed;
    inset: 0;
    z-index: 2200;
    display: flex;
    align-items: center;
    justify-content: center;
    background: color-mix(in srgb, var(--bg, #f8fafc) 55%, transparent);
    backdrop-filter: blur(1.5px);
    cursor: progress;
}
.bf-ajax-freeze-card {
    display: inline-flex;
    align-items: center;
    gap: 0.65rem;
    padding: 0.7rem 1.1rem;
    background: var(--surface, #fff);
    border: 1px solid var(--border, #e2e8f0);
    border-radius: 12px;
    box-shadow: 0 18px 48px rgba(15, 23, 42, 0.18);
    font-size: 12px;
    font-weight: 550;
    color: var(--text, #0f172a);
}
.bf-ajax-freeze-spinner {
    flex: 0 0 auto;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    border: 2px solid color-mix(in srgb, var(--text, #0f172a) 18%, transparent);
    border-top-color: var(--primary);
    animation: bf-ajax-freeze-spin 0.7s linear infinite;
}
@keyframes bf-ajax-freeze-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
    .bf-ajax-freeze-spinner { animation-duration: 1.6s; }
}

/* Team rail rows. The avatar sits where a folder glyph would, so the
   rail scans as one column with the folders above it rather than as a
   separate widget bolted on. */
.mail-sidebar .sb-team-avatar {
    /* Layout only - the bubble paint (gradient, letter color, weight)
       is the canonical .bf-avatar-letter rule; the markup renders via
       Html.UserAvatar. AvatarSingleSourceOfTruthTests. */
    width: 18px; height: 18px;
    min-width: 18px; max-width: 18px;
    border-radius: 50%;
    flex: 0 0 auto;
    object-fit: cover;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 9px;
}
img.mail-sidebar .sb-team-avatar,
.mail-sidebar img.sb-team-avatar { display: block; }

/* ── Workspace purpose mark (#277): the quiet ink signature telling an
   asset library from a document vault - typography + glyph, never a pill. ── */
.ws-purpose-mark {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    flex: 0 0 auto;
    color: var(--text-subtle, #64748b);
    vertical-align: middle;
}
.ws-purpose-mark svg { width: 13px; height: 13px; display: block; }
.ws-purpose-mark-label {
    font-size: 10px;
    font-weight: 650;
    letter-spacing: .08em;
    text-transform: uppercase;
    line-height: 1;
    white-space: nowrap;
}
.ws-purpose-mark.is-compact { gap: 0; }
.ws-purpose-mark.is-compact svg { width: 12px; height: 12px; }
/* The PROXY TILE (Finder grammar): the room's kind fused to its name - a
   hairline glyph tile, never a filled pill, tooltip carries the full purpose. */
.ws-purpose-mark.is-tile {
    width: 22px;
    height: 22px;
    justify-content: center;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: transparent;
}
.ws-purpose-mark.is-tile svg { width: 13px; height: 13px; }
.documents-repo-crumbs .ws-purpose-mark.is-tile { margin-inline-end: 6px; }
.app-archive-email .ws-purpose-mark { position: relative; top: 1px; }

/* Stop-word chips on the Search & AI card (issue #358): each word is a chip with its own remove
   cross, the add field sits inline as the last item; the group wraps whole chips, never a sliver. */
.ws-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; max-width: 100%; min-width: 0; }
.ws-chip {
    display: inline-flex; align-items: center; gap: 4px; padding: 3px 4px 3px 9px;
    border: 1px solid var(--border); border-radius: 999px; font-size: 13px; color: var(--text);
    background: var(--surface); line-height: 1.3;
}
.ws-chip-remove {
    appearance: none; border: 0; background: transparent; color: var(--text-muted); cursor: pointer;
    width: 20px; height: 20px; border-radius: 999px; font-size: 15px; line-height: 20px; padding: 0;
}
.ws-chip-remove:hover { color: var(--text); background: color-mix(in srgb, var(--border) 60%, transparent); }
.ws-chips-add { display: inline-flex; align-items: stretch; gap: 6px; }
.ws-chips-input {
    width: 9rem; max-width: 100%; padding: 6px 10px; font-family: inherit; font-size: 13px; color: var(--text);
    background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
}
.ws-chips-input:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.ws-chips-add-btn {
    appearance: none; padding: 6px 10px; font-family: inherit; font-size: 13px; color: var(--text);
    background: transparent; border: 1px solid var(--border); border-radius: 8px; cursor: pointer;
}
.ws-chips-add-btn:hover { background: color-mix(in srgb, var(--border) 40%, transparent); }


/* ── People & access: the role control on every roster row (issue #414) ──
   One bf-select per person, team or pending invitation, sized to the row
   (32px), wearing the role's rank glyph; Owner is a value in the same
   grammar. The invite bar's copy keeps the bar's 39px height. */
.ws-role-cell { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
.ws-role-cell form { margin: 0; }
.ws-role-cell .ws-role-form { display: inline-flex; }
.ws-role-cell .bf-select { display: inline-block; }
.ws-role-cell .bf-select-btn {
    padding-top: 5px;
    padding-bottom: 5px;
    font-size: 12px;
    font-weight: 600;
    line-height: 20px;
    white-space: nowrap;
}
.ws-role-glyph { display: inline-flex; align-items: center; justify-content: center; width: 14px; height: 14px; flex: 0 0 auto; }
.ws-role-glyph svg { width: 14px; height: 14px; }
.ws-role-value {
    display: inline-flex; align-items: center; gap: 6px;
    height: 32px; padding: 0 10px;
    font-size: 12px; font-weight: 700; line-height: 1;
    color: var(--text); white-space: nowrap;
}
/* A pending invitation's seat: the role it will land, in the pending tone. */
.ws-role-cell.is-pending .bf-select-btn { color: #b45309; border-style: dashed; }
html[data-theme="dark"] .ws-role-cell.is-pending .bf-select-btn { color: var(--warning); }
.ws-access-invite .bf-select,
.ws-access-invite .ws-access-invite-role { flex: 0 0 auto; }
.ws-access-invite .bf-select-btn { white-space: nowrap; }

/* Zero layout shift for the role controls (owner, 2026-09-10: "0 CLS on the
   config panel"). The select component swaps the native <select> for its
   trigger after first paint; the two must occupy the SAME box, so the native
   control below carries the trigger's compact metrics and both carry one fixed
   width - which also lines every seat up in one column. Long own-role names
   clip inside the label with an ellipsis rather than widening the seat. */
.ws-role-cell select[data-bf-select]:not(.bf-select-native),
.ws-role-cell .bf-select-btn {
    padding-top: 5px;
    padding-bottom: 5px;
    font-size: 12px;
    font-weight: 600;
    line-height: 20px;
    height: 32px;
    box-sizing: border-box;
}
.ws-role-cell select[data-bf-select]:not(.bf-select-native),
.ws-role-cell .bf-select { width: 148px; min-width: 148px; max-width: 148px; display: inline-block; vertical-align: middle; }
.ws-role-cell .bf-select-btn { width: 100%; }
.ws-role-cell .bf-select-label { min-width: 0; overflow: hidden; }
.ws-role-cell .bf-select-label-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ws-access-invite select.ws-access-invite-role,
.ws-access-invite .ws-access-invite-role + .bf-select,
.ws-access-invite .bf-select:has(> select.ws-access-invite-role) { width: 168px; min-width: 168px; max-width: 168px; }
.ws-access-invite select.ws-access-invite-role,
.ws-access-invite .bf-select:has(> select.ws-access-invite-role) .bf-select-btn { height: 39px; box-sizing: border-box; }

/* ── People & access, second pass (issue #414): the bar and the dense roster ──
   Three controls of ONE height above the list: find a person, narrow to a
   role, add people (the ink verb). Rows are a list one scans: 30px avatar,
   tight padding, the two text lines kept, the seat control at 32px. */
.ws-roster-bar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
.ws-roster-search { position: relative; flex: 0 1 260px; min-width: 180px; display: block; margin: 0; }
.ws-roster-search > svg { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); width: 14px; height: 14px; color: var(--text-muted); pointer-events: none; }
.ws-roster-search-input {
    width: 100%; height: 36px; box-sizing: border-box;
    padding: 0 10px 0 30px;
    border: 1px solid var(--border); border-radius: 10px;
    background: var(--surface); color: var(--text); font-size: 13px; font-family: inherit;
}
.ws-roster-search-input::placeholder { color: var(--text-subtle); }
.ws-roster-search-input:focus { outline: none; border-color: var(--text-subtle); }
.ws-roster-search-input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
/* The role filter and its native twin share one box: zero shift on enhance. */
.ws-roster-bar select[data-bf-select]:not(.bf-select-native),
.ws-roster-bar .bf-select { width: 176px; min-width: 176px; max-width: 176px; display: inline-block; vertical-align: middle; }
.ws-roster-bar select[data-bf-select]:not(.bf-select-native),
.ws-roster-bar .bf-select-btn { height: 36px; box-sizing: border-box; padding-top: 6px; padding-bottom: 6px; font-size: 13px; line-height: 22px; }
.ws-roster-bar .bf-select-btn { width: 100%; }
.ws-roster-add { margin-left: auto; height: 36px; display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.ws-roster-add svg { width: 14px; height: 14px; }
.ws-roster-empty { padding: 18px 12px; font-size: 13px; color: var(--text-muted); text-align: center; }
@media (max-width: 640px) {
    .ws-roster-search { flex: 1 1 100%; }
    .ws-roster-add { margin-left: 0; }
}

.collab-grid--dense .coworker-row { padding: 6px 12px; gap: 10px; }
.collab-grid--dense .coworker-avatar { width: 30px; height: 30px; font-size: 11px; }
.collab-grid--dense .coworker-text { gap: 0; }
.collab-grid--dense .coworker-name { font-size: 13px; }
.collab-grid--dense .coworker-email { font-size: 12px; }
.collab-grid--dense .coworker-meta { font-size: 11px; }
.collab-grid--dense .collab-row-gutter { gap: 8px; }
.collab-grid--dense .collab-row-presence { width: 9px; height: 9px; }

/* The Add-people assistant: the picker sits in the frame like a field; the
   role tiles are the house grid; the narration slot keeps its height. */
.ws-peopleassist-chips { min-height: 44px; }
/* The seat a newcomer takes (owner, 2026-10-23): the guest's term stands under the Guest row only while it is the
   answer, and the roles a guest may not hold stand down - both decided by the radios themselves (:has). */
.ws-peopleassist-guestterm { flex-direction: row; align-items: center; flex-wrap: wrap; gap: 10px; }
.ws-peopleassist-seats > .ws-peopleassist-guestterm { display: none; }
.ws-peopleassist-seats:has(> [data-ws-peopleassist-seat="guest"] > input:checked) > .ws-peopleassist-guestterm { display: flex; }
.ws-peopleassist-guestterm-cap { font-size: 13px; color: var(--text-muted); line-height: 1.4; }
.ws-peopleassist-form:has(input[name="seat"][value="guest"]:checked) [data-ws-peopleassist-tile][data-guest-ok="0"] { display: none; }
.documents-incassist-chip .ws-role-glyph { margin-right: 5px; vertical-align: -2px; }

/* ── The person sheet (issue #414): "what can this person do here, and why" ── */
.ws-roster-sheet-open { width: 32px; height: 32px; padding: 0; display: inline-flex; align-items: center; justify-content: center; color: var(--text-muted); }
.ws-roster-sheet-open svg { width: 15px; height: 15px; }
.ws-roster-sheet-open:hover { color: var(--text); }
.ws-personsheet-card { width: 480px; max-width: calc(100vw - 32px); padding: 0; position: relative; }
.ws-personsheet-close { position: absolute; top: 12px; right: 12px; }
.ws-personsheet-body[aria-busy="true"] { min-height: 240px; }
.ws-person { display: flex; flex-direction: column; }
.ws-person-head { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; padding: 20px 56px 16px 20px; }
.ws-person-avatar { width: 40px; height: 40px; }
.ws-person-id { flex: 1 1 180px; min-width: 0; }
.ws-person-name { font-size: 14px; font-weight: 600; color: var(--text); line-height: 1.3; }
.ws-person-email { font-size: 12px; color: var(--text-muted); line-height: 1.4; overflow-wrap: anywhere; }
.ws-person-seat { flex: 0 0 auto; margin-left: auto; }
.ws-person-sec { padding: 12px 20px 16px; border-top: 1px solid var(--border); }
.ws-person-lab { margin: 0 0 10px; font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted); }
.ws-person-src { display: flex; align-items: center; gap: 8px; padding: 6px 0; font-size: 13px; color: var(--text-muted); }
.ws-person-src + .ws-person-src { border-top: 1px solid color-mix(in srgb, var(--border) 60%, transparent); }
.ws-person-src.is-on { color: var(--text); }
.ws-person-src-role { font-weight: 600; }
.ws-person-src-how { color: var(--text-muted); font-size: 12px; }
.ws-person-fam { display: grid; grid-template-columns: 80px 1fr; gap: 6px 12px; align-items: start; margin-bottom: 8px; }
.ws-person-fam-name { font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--text-subtle); padding-top: 6px; }
.ws-person-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.ws-person-quiet { margin: 0; font-size: 13px; color: var(--text-muted); }
.ws-person-exc-row { display: flex; align-items: center; gap: 10px; padding: 7px 0; }
.ws-person-exc-row + .ws-person-exc-row { border-top: 1px solid color-mix(in srgb, var(--border) 60%, transparent); }
.ws-person-exc-glyph { width: 28px; height: 28px; display: inline-flex; align-items: center; justify-content: center; color: var(--text-muted); flex: 0 0 auto; }
.ws-person-exc-glyph svg { width: 16px; height: 16px; }
.ws-person-exc-name { flex: 1 1 auto; min-width: 0; font-size: 13px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ws-person-exc-answer { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--text); white-space: nowrap; }
.ws-person-exc-answer.is-none { color: var(--text-muted); }
.ws-person-exc-answer svg { width: 14px; height: 14px; }
.ws-person-exc-door { width: 32px; height: 32px; padding: 0; display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; }
.ws-person-exc-door svg { width: 14px; height: 14px; }

/* ═══ Workspace roles (issue #300, lifted to every kind by #414 phase 3): the
   Roles card of the Permissions chapter - the matrix, the assistant's seed
   tiles, the holder figures. Moved here from documents-portal.css so the
   Documents card and the Issues card are styled by the ONE sheet every
   detail page loads in its head (zero flash, zero shift). ═══ */
/* ── Access roles (issue #300, settings-window pass 2026-09-05): ONE still
   MATRIX - a column per role (glyph plate + name), a row per verb, an ink
   check where the role grants it. Family rows title the verbs the way the
   vocabulary is documented. Nothing on the card moves: no peek slot, no
   reveal; composing happens in the assistant window. Row hover is a wash so
   a long row reads across; the header cell of an own role carries its
   pencil and trash. ── */
.ws-rolemx-wrap { min-width: 0; }
.ws-rolemx { width: 100%; border-collapse: collapse; table-layout: auto; font-size: 13px; }
.ws-rolemx th, .ws-rolemx td { padding: 0 6px; text-align: center; vertical-align: middle; font-weight: 500; }
.ws-rolemx thead th, .ws-rolemx thead td { vertical-align: top; padding: 6px 2px 10px; border-bottom: 1px solid var(--border, #e2e8f0); }
.ws-rolemx-role { width: 84px; min-width: 80px; }
.ws-rolemx-glyph {
    display: inline-flex; align-items: center; justify-content: center;
    width: 30px; height: 30px;
    border: 1px solid var(--border, #e2e8f0); border-radius: 8px;
    color: var(--text-muted, #64748b);
}
.ws-rolemx-glyph svg { width: 15px; height: 15px; }
.ws-rolemx-role.is-own .ws-rolemx-glyph { color: var(--text, #0f172a); border-color: var(--text-subtle, #94a3b8); }
.ws-rolemx-name {
    display: block; margin-top: 5px;
    font-size: 11px; font-weight: 600; line-height: 1.25; color: var(--text, #0f172a);
    overflow-wrap: anywhere; hyphens: manual;
}
.ws-rolemx-tools { display: inline-flex; align-items: center; justify-content: center; margin-top: 2px; }
.ws-rolemx-tools .ws-rolemx-tool { width: 24px; height: 24px; }
.ws-rolemx-tools .ws-rolemx-tool svg { width: 12px; height: 12px; }
/* A verb's words wrap when the table stands near its least room (MatrixWidthNeeded counts the verb
   column at its 150px floor); with room they hold one line on their own. */
.ws-rolemx-row th { text-align: left; color: var(--text, #0f172a); white-space: normal; min-width: 150px; }
.ws-rolemx-row th, .ws-rolemx-row td { height: 30px; }
.ws-rolemx-row + .ws-rolemx-row th, .ws-rolemx-row + .ws-rolemx-row td,
.ws-rolemx-family + .ws-rolemx-row th, .ws-rolemx-family + .ws-rolemx-row td {
    border-top: 1px solid color-mix(in srgb, var(--border, #e2e8f0) 55%, transparent);
}
.ws-rolemx-row:hover th, .ws-rolemx-row:hover td { background: var(--surface-2, #f1f5f9); }
.ws-rolemx-row td.is-on { color: var(--text, #0f172a); }
.ws-rolemx-row td svg { display: block; width: 13px; height: 13px; margin: 0 auto; }
.ws-rolemx-family th {
    text-align: left; padding: 14px 6px 4px;
    font-size: 10px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
    color: var(--text-muted, #64748b);
}
.ws-rolemx-row:first-child th, .ws-rolemx-row:first-child td { border-top: 0; }
/* ── Access roles: room (the roles matrix never scrolls sideways). The partial stamps the width
   its table needs, rounded up to a 40px step (WorkspaceRolesCardViewModel.MatrixFitStep); while
   the card is narrower, the same data stands as ONE CARD PER ROLE. Decided by the container before
   first paint: nothing shifts, nothing is bitten, one vertical scroller. ── */
.ws-rolemx-host { container: rolemx / inline-size; }
.ws-rolemx-cards { display: none; list-style: none; margin: 0; padding: 0; grid-template-columns: repeat(auto-fill, minmax(232px, 1fr)); align-items: start; gap: 12px; }
.ws-rolemx-host[data-rolemx-fit="always"] > .ws-rolemx-wrap { display: none; }
.ws-rolemx-host[data-rolemx-fit="always"] > .ws-rolemx-cards { display: grid; }
@container rolemx (width < 200px) { .ws-rolemx-host[data-rolemx-fit="200"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="200"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 240px) { .ws-rolemx-host[data-rolemx-fit="240"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="240"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 280px) { .ws-rolemx-host[data-rolemx-fit="280"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="280"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 320px) { .ws-rolemx-host[data-rolemx-fit="320"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="320"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 360px) { .ws-rolemx-host[data-rolemx-fit="360"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="360"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 400px) { .ws-rolemx-host[data-rolemx-fit="400"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="400"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 440px) { .ws-rolemx-host[data-rolemx-fit="440"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="440"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 480px) { .ws-rolemx-host[data-rolemx-fit="480"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="480"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 520px) { .ws-rolemx-host[data-rolemx-fit="520"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="520"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 560px) { .ws-rolemx-host[data-rolemx-fit="560"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="560"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 600px) { .ws-rolemx-host[data-rolemx-fit="600"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="600"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 640px) { .ws-rolemx-host[data-rolemx-fit="640"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="640"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 680px) { .ws-rolemx-host[data-rolemx-fit="680"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="680"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 720px) { .ws-rolemx-host[data-rolemx-fit="720"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="720"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 760px) { .ws-rolemx-host[data-rolemx-fit="760"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="760"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 800px) { .ws-rolemx-host[data-rolemx-fit="800"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="800"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 840px) { .ws-rolemx-host[data-rolemx-fit="840"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="840"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 880px) { .ws-rolemx-host[data-rolemx-fit="880"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="880"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 920px) { .ws-rolemx-host[data-rolemx-fit="920"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="920"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 960px) { .ws-rolemx-host[data-rolemx-fit="960"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="960"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 1000px) { .ws-rolemx-host[data-rolemx-fit="1000"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="1000"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 1040px) { .ws-rolemx-host[data-rolemx-fit="1040"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="1040"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 1080px) { .ws-rolemx-host[data-rolemx-fit="1080"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="1080"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 1120px) { .ws-rolemx-host[data-rolemx-fit="1120"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="1120"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 1160px) { .ws-rolemx-host[data-rolemx-fit="1160"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="1160"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 1200px) { .ws-rolemx-host[data-rolemx-fit="1200"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="1200"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 1240px) { .ws-rolemx-host[data-rolemx-fit="1240"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="1240"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 1280px) { .ws-rolemx-host[data-rolemx-fit="1280"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="1280"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 1320px) { .ws-rolemx-host[data-rolemx-fit="1320"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="1320"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 1360px) { .ws-rolemx-host[data-rolemx-fit="1360"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="1360"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 1400px) { .ws-rolemx-host[data-rolemx-fit="1400"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="1400"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 1440px) { .ws-rolemx-host[data-rolemx-fit="1440"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="1440"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 1480px) { .ws-rolemx-host[data-rolemx-fit="1480"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="1480"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 1520px) { .ws-rolemx-host[data-rolemx-fit="1520"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="1520"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 1560px) { .ws-rolemx-host[data-rolemx-fit="1560"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="1560"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 1600px) { .ws-rolemx-host[data-rolemx-fit="1600"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="1600"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 1640px) { .ws-rolemx-host[data-rolemx-fit="1640"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="1640"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 1680px) { .ws-rolemx-host[data-rolemx-fit="1680"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="1680"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 1720px) { .ws-rolemx-host[data-rolemx-fit="1720"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="1720"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 1760px) { .ws-rolemx-host[data-rolemx-fit="1760"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="1760"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 1800px) { .ws-rolemx-host[data-rolemx-fit="1800"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="1800"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 1840px) { .ws-rolemx-host[data-rolemx-fit="1840"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="1840"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 1880px) { .ws-rolemx-host[data-rolemx-fit="1880"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="1880"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 1920px) { .ws-rolemx-host[data-rolemx-fit="1920"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="1920"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 1960px) { .ws-rolemx-host[data-rolemx-fit="1960"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="1960"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 2000px) { .ws-rolemx-host[data-rolemx-fit="2000"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="2000"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 2040px) { .ws-rolemx-host[data-rolemx-fit="2040"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="2040"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 2080px) { .ws-rolemx-host[data-rolemx-fit="2080"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="2080"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 2120px) { .ws-rolemx-host[data-rolemx-fit="2120"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="2120"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 2160px) { .ws-rolemx-host[data-rolemx-fit="2160"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="2160"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 2200px) { .ws-rolemx-host[data-rolemx-fit="2200"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="2200"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 2240px) { .ws-rolemx-host[data-rolemx-fit="2240"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="2240"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 2280px) { .ws-rolemx-host[data-rolemx-fit="2280"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="2280"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 2320px) { .ws-rolemx-host[data-rolemx-fit="2320"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="2320"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 2360px) { .ws-rolemx-host[data-rolemx-fit="2360"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="2360"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 2400px) { .ws-rolemx-host[data-rolemx-fit="2400"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="2400"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 2440px) { .ws-rolemx-host[data-rolemx-fit="2440"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="2440"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 2480px) { .ws-rolemx-host[data-rolemx-fit="2480"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="2480"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 2520px) { .ws-rolemx-host[data-rolemx-fit="2520"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="2520"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 2560px) { .ws-rolemx-host[data-rolemx-fit="2560"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="2560"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 2600px) { .ws-rolemx-host[data-rolemx-fit="2600"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="2600"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 2640px) { .ws-rolemx-host[data-rolemx-fit="2640"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="2640"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 2680px) { .ws-rolemx-host[data-rolemx-fit="2680"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="2680"] > .ws-rolemx-cards { display: grid; } }
@container rolemx (width < 2720px) { .ws-rolemx-host[data-rolemx-fit="2720"] > .ws-rolemx-wrap { display: none; } .ws-rolemx-host[data-rolemx-fit="2720"] > .ws-rolemx-cards { display: grid; } }
.ws-rolemx-card { min-width: 0; padding: 12px 14px 10px; border: 1px solid var(--border, #e2e8f0); border-radius: 12px; background: var(--surface, #fff); }
.ws-rolemx-cardhead {
    display: grid; grid-template-columns: 30px minmax(0, 1fr) auto; column-gap: 10px; align-items: center;
    padding-bottom: 10px; margin-bottom: 4px; border-bottom: 1px solid color-mix(in srgb, var(--border, #e2e8f0) 70%, transparent);
}
.ws-rolemx-cardhead .ws-rolemx-glyph { grid-row: 1 / span 2; }
.ws-rolemx-cardhead .ws-rolemx-name { grid-column: 2; margin-top: 0; font-size: 13px; overflow-wrap: anywhere; }
.ws-rolemx-cardhead .ws-rolemx-holders { grid-column: 2; margin-top: 1px; }
.ws-rolemx-cardhead .ws-rolemx-tools { grid-column: 3; grid-row: 1 / span 2; margin-top: 0; gap: 2px; }
.ws-rolemx-card.is-own .ws-rolemx-glyph { color: var(--text, #0f172a); border-color: var(--text-subtle, #94a3b8); }
.ws-rolemx-card.is-rel .ws-rolemx-glyph { border-style: dashed; }
.ws-rolemx-grantfamily { margin: 10px 0 2px; font-size: 10px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted, #64748b); }
.ws-rolemx-grants { list-style: none; margin: 0; padding: 0; }
.ws-rolemx-grant { display: flex; align-items: flex-start; gap: 8px; padding: 4px 0; font-size: 13px; line-height: 18px; color: var(--text, #0f172a); }
.ws-rolemx-grant svg { flex: 0 0 auto; width: 13px; height: 13px; margin-top: 2.5px; }
.ws-rolemx-grant span { min-width: 0; overflow-wrap: anywhere; }
/* A card is narrower than the table's header: the trash's confirm hangs from the CARD, inset on both
   sides under its head, so it never leaves the card it answers for. */
.ws-rolemx-card { position: relative; }
.ws-rolemx-card .ws-rolemx-remove { position: static; }
.ws-rolemx-card .ws-rolemx-confirm { inset-inline: 8px; top: 58px; min-width: 0; }

.ws-roles-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
    gap: 14px 20px;
    padding: 10px 0 4px;
}
.ws-roles-family { display: flex; flex-direction: column; gap: 7px; min-width: 0; }
.ws-roles-family-title {
    font-size: 10px;
    font-weight: 600;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--text-muted, #64748b);
}
.ws-roles-verb {
    display: flex;
    align-items: center;
    gap: 9px;
    cursor: pointer;
    min-width: 0;
}
/* The switch is this card's own, deliberately: borrowing another surface's
   primitive coupled the checked paint to a stylesheet that page may or may not
   load, and the ON state silently read as OFF. Same grammar - hidden native
   input keeps the form and a11y state, the track is the designed mark - built
   from the app's tokens so both themes follow. */
.ws-roles-verb > input[type="checkbox"] {
    position: absolute;
    opacity: 0;
    pointer-events: none;
    width: 0;
    height: 0;
}
.ws-roles-switch {
    position: relative;
    flex: 0 0 auto;
    width: 34px;
    height: 20px;
    border-radius: 999px;
    background: var(--surface-2, #f1f5f9);
    border: 1px solid var(--border, #e2e8f0);
    transition: background .15s ease, border-color .15s ease;
}
.ws-roles-switch::after {
    content: "";
    position: absolute;
    top: 2px;
    left: 2px;
    width: 14px;
    height: 14px;
    border-radius: 50%;
    /* The knob inverts against the ink track - the canonical pair, so dark
       theme flips with everything else instead of staying white on white. */
    background: var(--bg, #fff);
    box-shadow: 0 1px 3px rgba(0, 0, 0, .18);
    transition: transform .15s ease;
}
.ws-roles-verb > input[type="checkbox"]:checked ~ .ws-roles-switch {
    background: var(--primary);
    border-color: var(--primary);
}
.ws-roles-verb > input[type="checkbox"]:checked ~ .ws-roles-switch::after {
    transform: translateX(14px);
}
.ws-roles-verb > input[type="checkbox"]:focus-visible ~ .ws-roles-switch {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}
.ws-roles-verb-label { font-size: 12px; min-width: 0; }

@media (prefers-reduced-motion: reduce) {
    .ws-roles-switch, .ws-roles-switch::after { transition: none; }
}


/* The assistant's "start from" tiles: a built-in's glyph + name; one click
   seeds the switches, the ring marks the set they currently equal. */
.ws-roleassist-seeds { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 0 0 12px; }
.ws-roleassist-seeds-label {
    flex: 0 0 100%; margin-bottom: 2px;
    font-size: 10px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
    color: var(--text-muted, #64748b);
}
.ws-roleassist-seed {
    display: inline-flex; align-items: center; gap: 6px;
    height: 30px; padding: 0 10px 0 6px;
    border: 1px solid var(--border, #e2e8f0); border-radius: 8px; background: transparent;
    font: inherit; font-size: 12px; font-weight: 600; color: var(--text, #0f172a);
    cursor: pointer; white-space: nowrap;
}
.ws-roleassist-seed:hover { border-color: var(--text-subtle, #94a3b8); }
.ws-roleassist-seed.is-on { border-color: var(--text, #0f172a); box-shadow: inset 0 0 0 1px var(--text, #0f172a); }
.ws-roleassist-seed:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.ws-roleassist-seed-glyph { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; color: var(--text-muted, #64748b); }
.ws-roleassist-seed-glyph svg { width: 13px; height: 13px; }
.ws-roleassist-seed.is-on .ws-roleassist-seed-glyph { color: var(--text, #0f172a); }
.ws-roles-newrow { display: flex; justify-content: flex-end; padding: 12px 0 4px; }
button.ws-rolemx-tool { appearance: none; -webkit-appearance: none; border: 0; background: transparent; }
.ws-roleassist-name { width: min(100%, 320px); }

/* Who holds each role (issue #414): the figure under a column is a door to the
   roster narrowed to that role; "nobody yet" is the honest empty. */
.ws-rolemx-holders {
    display: block; margin-top: 3px;
    font-size: 11px; font-weight: 500; line-height: 1.3;
    color: var(--text-muted, #64748b); text-decoration: none; white-space: normal;
}
a.ws-rolemx-holders:hover, a.ws-rolemx-holders:focus-visible { color: var(--text, #0f172a); text-decoration: underline; text-underline-offset: 2px; }
.ws-rolemx-holders.is-none { color: var(--text-subtle, #94a3b8); }
/* The matrix's own controls (the pencil, the trash, its confirm popover, the
   New role verb, the cap note): the same grammar the portal's config rows use,
   under roles-scoped names so the card needs nothing from the portal sheet. */
.ws-rolemx-remove { position: relative; flex: 0 0 auto; display: inline-block; }
.ws-rolemx-tool {
    list-style: none; width: 26px; height: 26px; border: 0; background: transparent; padding: 0;
    border-radius: 8px; display: inline-flex; align-items: center; justify-content: center;
    color: var(--text-subtle, #64748b); cursor: pointer;
}
.ws-rolemx-tool::-webkit-details-marker { display: none; }
.ws-rolemx-tool svg { width: 14px; height: 14px; }
.ws-rolemx-tool:hover { color: var(--danger, #b42318); background: color-mix(in srgb, var(--danger, #b42318) 8%, transparent); }
.ws-rolemx-tool.is-neutral:hover { color: var(--text, #0f172a); background: color-mix(in srgb, var(--text, #0f172a) 6%, transparent); }
.ws-rolemx-remove[open] .ws-rolemx-tool { color: var(--text, #0f172a); background: color-mix(in srgb, var(--text, #0f172a) 6%, transparent); }
.ws-rolemx-confirm {
    position: absolute; inset-inline-end: 0; top: calc(100% + 6px); z-index: 40; min-width: 280px;
    display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    background: var(--surface, #fff); border: 1px solid var(--border, #e2e8f0); border-radius: 10px;
    box-shadow: 0 12px 32px rgba(15, 23, 42, 0.16); padding: 12px; text-align: start;
}
.ws-rolemx-confirm p { margin: 0; font-size: 13px; color: var(--text-muted); line-height: 1.5; }
.ws-roles-createbtn { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 6px; }
.ws-roles-createbtn svg { width: 12px; height: 12px; }
.ws-roles-note { margin: 8px 2px 4px; font-size: 12px; color: var(--text-muted, #64748b); }

/* ── How this board works (issue #450): the ONE discipline renderer ──
   Glyph-and-word facts, one strong line and one muted line each; for an
   admin every fact is a door (a hairline row with a chevron), for a reader a
   still row. Never a paragraph, never a plate around the glyph. */
.bf-discipline { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 12px; }
.bf-discipline.is-compact { grid-template-columns: minmax(0, 1fr); gap: 4px; }
.bf-discipline-fact { min-width: 0; }
.bf-discipline-door { display: grid; grid-template-columns: 18px minmax(0, 1fr) auto; align-items: start; gap: 10px; padding: 8px 10px; border-radius: 10px; color: inherit; text-decoration: none; min-height: 44px; }
.bf-discipline-door.is-still { grid-template-columns: 18px minmax(0, 1fr); }
a.bf-discipline-door { border: 1px solid var(--border); }
a.bf-discipline-door:hover { background: color-mix(in srgb, var(--text) 4%, transparent); text-decoration: none; }
.bf-discipline-glyph { color: var(--text); margin-top: 2px; flex: none; }
.bf-discipline-words { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.bf-discipline-words strong { font-weight: 600; font-size: 13px; line-height: 1.35; color: var(--text); }
.bf-discipline-words span { font-size: 12px; line-height: 1.35; color: var(--text-muted); }
.bf-discipline-chevron { color: var(--text-muted); align-self: center; }
@media (max-width: 720px) { .bf-discipline { grid-template-columns: minmax(0, 1fr); } }
/* Compact (the ticket page's "How this board works" popover): flat rows under
   hairlines, 8px inside the popover's border - never boxed cards that could sit
   against it (owner, 2026-09-12: "don't stuck rounded UI element to any border").
   A door keeps its chevron and its wash; a still row has neither. */
.bf-discipline.is-compact { gap: 0; }
.bf-discipline.is-compact .bf-discipline-fact + .bf-discipline-fact { border-top: 1px solid var(--border); }
.bf-discipline.is-compact .bf-discipline-door { padding: 8px 8px; min-height: 44px; border-radius: 8px; }
.bf-discipline.is-compact a.bf-discipline-door { border: 0; }

/* ── The ownership-mode switch recap (issue #450) ──────────────────
   Under the mode cards: glyph + word facts, the tickets as hairline chips,
   an ink Apply beside a hairline Cancel. Rendered by issues-ownership-mode.js
   only after a pick, so nothing shifts at first paint. */
.ws-feature-row-stack { padding-bottom: 4px; }
.ws-issues-mode .bf-mode-cards { margin: 6px 0 4px; }
.ws-switch-recap { margin-top: 10px; padding: 12px 14px; border: 1px solid var(--border); border-radius: 10px; display: flex; flex-direction: column; gap: 10px; }
.ws-switch-recap-facts { display: flex; flex-direction: column; gap: 6px; }
.ws-switch-recap-fact { display: flex; align-items: center; gap: 10px; font-size: 13px; color: var(--text); }
.ws-switch-recap-fact svg { flex: none; color: var(--text); }
.ws-switch-recap-fact span { color: var(--text-muted); }
.ws-switch-recap-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.ws-switch-recap-chip { display: inline-flex; align-items: center; gap: 6px; height: 26px; padding: 0 9px; border: 1px solid var(--border-strong, rgba(15, 23, 42, .18)); border-radius: 999px; font-size: 12px; color: var(--text); max-width: 100%; }
.ws-switch-recap-chip strong { font-weight: 600; }
.ws-switch-recap-chip span { color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 160px; }
.ws-switch-recap-chip.is-more { color: var(--text-muted); border-style: dashed; }
.ws-switch-recap-verbs { display: flex; justify-content: flex-end; gap: 8px; }
.ws-feature-door { white-space: nowrap; }

/* Relationship columns of the roles matrix (issue #450): what the reporter
   and the owner of a ticket may do on it - derived, read-only. They stand
   apart from the roles by ROOM alone (a wider gutter before the group),
   their cells exactly the roles' cells: a hairline stitched down the table
   and a grey wash on every empty cell read as a broken column (owner,
   2026-09-12: "I believe we have a design problem here"; the charter: never
   stitch elements with lines, nothing ever touches). */
.ws-rolemx th.ws-rolemx-role.is-rel,
.ws-rolemx td.is-rel { padding-left: 22px; }
.ws-rolemx th.ws-rolemx-role.is-rel .ws-rolemx-glyph { border-style: dashed; }
.ws-rolemx th.ws-rolemx-role.is-rel .ws-rolemx-holders { color: var(--text-muted); }

/* When you're away (#537): a standing plan is ONE row - the deputy's avatar and name, the days as a
   chip, End; without one, ONE row of controls of one height that wraps whole. */
.profile-away-plan { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 12px; margin-top: 14px; }
.profile-away-avatar { width: 28px; height: 28px; flex: 0 0 auto; }
.profile-away-name { font-size: 14px; font-weight: 600; color: var(--text); }
.profile-away-days {
    display: inline-flex; align-items: center; height: 26px; padding: 0 10px;
    border: 1px solid var(--border); border-radius: 8px; font-size: 12px; color: var(--text-muted); white-space: nowrap;
}
.profile-away-end { margin: 0 0 0 auto; }
.profile-away-form { display: flex; flex-direction: column; gap: 12px; margin-top: 14px; }
.profile-away-row { display: flex; align-items: flex-end; flex-wrap: wrap; gap: 12px; }
.profile-away-field { display: flex; flex-direction: column; gap: 6px; min-width: 180px; }
.profile-away-field .form-label { margin: 0; font-size: 12px; color: var(--text-muted); }
/* The button shares the fields' height (one row, one height). */
.profile-away-save { white-space: nowrap; height: 39px; padding-inline: 14px; }
.profile-away-none { margin: 14px 0 0; font-size: 13px; color: var(--text-muted); }

/* A popover whose start would leave the window turns to its trigger's END
   (app-shell.js, the ONE popover rule): its right edge meets the trigger's. */
.bf-popover.is-edge-end,
[role="menu"].is-edge-end,
.cockpit-views-panel.is-edge-end {
    left: auto !important;
    right: 0 !important;
    transform: none !important;
    transform-origin: top right;
}

/* Paused strip (owner, 2026-10-15): a paused mailbox receives nothing, and
   the list says so at its top - the pause glyph in the warning tone, ONE
   strong line, who and since when muted beside it, Resume in ink. Rendered
   with the page (no shift), it leaves in place once resumed. */
.mail-paused-strip {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px 12px;
    margin: 10px 12px 4px;
    padding: 10px 12px;
    border: 1px solid color-mix(in srgb, var(--warning) 45%, transparent);
    border-radius: 10px;
    background: color-mix(in srgb, var(--warning) 9%, var(--surface));
}
.mail-paused-glyph {
    display: inline-flex;
    flex: 0 0 auto;
    color: var(--warning);
}
.mail-paused-text {
    display: flex;
    flex: 1 1 200px;
    min-width: 0;
    flex-direction: column;
    gap: 2px;
}
.mail-paused-title {
    font-weight: 600;
    font-size: 13px;
    color: var(--text);
}
.mail-paused-meta,
.mail-paused-ask {
    font-size: 12px;
    color: var(--text-muted);
}
.mail-paused-resume {
    flex: 0 0 auto;
    height: 30px;
    padding: 0 14px;
    border: 0;
    border-radius: 8px;
    background: var(--text);
    color: var(--bg);
    font-size: 13px;
    font-weight: 600;
    white-space: nowrap;
    cursor: pointer;
}
.mail-paused-resume:hover { background: color-mix(in srgb, var(--text) 86%, var(--bg)); }
.mail-paused-resume:disabled { opacity: .6; cursor: default; }
