/* =============================================================
   ez-platform brand theme
   Scope: [data-theme-brand="ez-platform"] on <html>
   Light mode vars applied at attribute specificity (0,1,0).
   Dark overrides at (0,2,0) - always win over Bootstrap's [data-bs-theme="dark"].
   ============================================================= */

/* ── Structure - brand-independent, set once ────────────────── */
/* Layout, spacing, radius, and typography tokens that never change
   regardless of which brand theme is active. */
:root {
    --side-w:               17.5rem;
    --radius-xs:            0.25rem;
    --radius-sm:            0.5rem;
    --radius:               0.625rem;
    --radius-lg:            0.875rem;
    --radius-pill:          999rem;

    --app-max-w:            90rem;
    --app-gutter:           1.5rem;
    --app-gutter-y:         1rem;
    --app-radius:           1rem;

    --bs-body-font-size:    0.875rem;
}

/* ── Light palette ──────────────────────────────────────────── */
[data-theme-brand="ez-platform"] {

    /* ── Elevation scale (overrides Bootstrap's tiers) ─────────────
       Every raised surface (dropdowns, menus, popovers, toasts, modals,
       lightboxes, drag ghosts) references ONE of these three tokens - never
       a hand-rolled box-shadow. Values are the brand's softer, flat-friendly
       take on Bootstrap's defaults, re-declared per theme because a dark
       surface needs a heavier shadow than a light one. To flatten the whole
       app, set all three to `none` here. */
    --bs-box-shadow-sm: 0 2px 8px rgba(0,0,0,.08);
    --bs-box-shadow:    0 8px 24px rgba(0,0,0,.12), 0 2px 8px rgba(0,0,0,.08);
    --bs-box-shadow-lg: 0 12px 40px rgba(0,0,0,.18), 0 2px 8px rgba(0,0,0,.08);

    /* ── Focus ring (overrides Bootstrap's tokens) ─────────────────
       Bootstrap's stock --bs-focus-ring-color is a literal blue at 0.25rem;
       we point it at the brand primary at 3px so EVERY focus ring - Bootstrap's
       own :focus-visible / .focus-ring, our form fields, pagination, input
       groups - is one brand-consistent halo. --ez-focus-ring is the box-shadow
       alias for rules that apply the ring directly. */
    --bs-focus-ring-width:   0.1875rem;
    --bs-focus-ring-opacity: 0.25;
    --bs-focus-ring-color:   rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity));
    --ez-focus-ring:         0 0 0 var(--bs-focus-ring-width) var(--bs-focus-ring-color);

    /* ── No Bootstrap equivalent (keep as custom) ─────────────── */
    --border-strong:   #a99dd0;
    --ez-shadow-knob:  0 1px 3px rgba(0, 0, 0, .3);

    /* ── Bootstrap color tokens ────────────────────────────────── */
    --bs-primary:          #af85ff;
    --bs-primary-rgb:      175, 133, 255;
    --bs-secondary:        #6c757d;
    --bs-secondary-rgb:    108, 117, 125;
    --bs-success:          #198754;
    --bs-success-rgb:       25, 135,  84;
    --bs-warning:          #ffc107;
    --bs-warning-rgb:      255, 193,   7;
    --bs-danger:           #dc3545;
    --bs-danger-rgb:       220,  53,  69;
    --bs-info:             #0dcaf0;
    --bs-info-rgb:          13, 202, 240;

    /* ── Bootstrap text tokens ─────────────────────────────────── */
    --bs-body-color:       #0c1a2e;
    --bs-secondary-color:  #5a4a7a;
    --bs-tertiary-color:   #3a2d60;

    /* ── Bootstrap surface tokens ──────────────────────────────── */
    --bs-body-bg:              #ffffff;
    --bs-body-bg-rgb:          255, 255, 255;
    --bs-secondary-bg:         #ede9f7;
    --bs-secondary-bg-rgb:     237, 233, 247;
    --bs-tertiary-bg:          #e4dff2;
    --bs-tertiary-bg-rgb:      228, 223, 242;

    /* ── Brand "subtle" surface tokens ─────────────────────────────
       Bootstrap ships these as literals derived from ITS default blue, so rebranding
       --bs-primary alone leaves .bg-primary-subtle / .border-primary-subtle /
       .text-primary-emphasis (and .alert-primary, .accordion active) off-brand. Deriving
       them from the brand token keeps every subtle surface on-brand, and a derived app
       brand that retints --bs-primary gets matching surfaces for free.
       Only primary needs this: success/warning/danger/info keep Bootstrap's own hues, so
       Bootstrap's own subtle values for those still match. */
    --bs-primary-bg-subtle:     color-mix(in srgb, var(--bs-primary) 14%, var(--bs-body-bg));
    --bs-primary-border-subtle: color-mix(in srgb, var(--bs-primary) 34%, var(--bs-body-bg));
    --bs-primary-text-emphasis: color-mix(in srgb, var(--bs-primary) 70%, #000);

    /* ── Bootstrap border / link tokens ────────────────────────── */
    --bs-border-color:       #ccc5e8;
    /* Links derive from the single brand token, darkened for readable contrast on the
       light (white) surface; hover darkens further for affordance. */
    --bs-link-color:         color-mix(in srgb, var(--bs-primary) 70%, #000);
    --bs-link-color-rgb:     var(--bs-primary-rgb);
    --bs-link-hover-color:   color-mix(in srgb, var(--bs-primary) 50%, #000);
    --bs-code-color:         var(--bs-primary);

    /* ── Bootstrap component tokens ────────────────────────────── */
    --bs-card-bg:            var(--bs-body-bg);
    --bs-card-border-color:  var(--bs-border-color);
    --bs-card-cap-bg:        transparent;
    --bs-modal-bg:           var(--bs-body-bg);
    --bs-modal-border-color: var(--border-strong);
    --bs-body-font-family: 'Inter', system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    /* Single monospace stack for the whole app (code blocks, diffs, shortcuts, IDs).
       Bootstrap consumes this token for <code>/<pre>; custom rules use var(--bs-font-monospace). */
    --bs-font-monospace: 'JetBrains Mono', SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;

    color-scheme: light;
}

/* ── Dark palette override ──────────────────────────────────── */
/* Deliberately carries the FULL token set - including tokens whose values equal
   the base block - so this block is a complete, self-contained template that a
   derived app brand can copy and retheme token-by-token without cross-referencing
   the light block. Do not trim the duplicates. */
[data-theme-brand="ez-platform"][data-bs-theme="dark"] {

    /* ── Elevation scale (dark values; see light block) ──────────── */
    --bs-box-shadow-sm: 0 2px 10px rgba(0,0,0,.4);
    --bs-box-shadow:    0 8px 28px rgba(0,0,0,.65), 0 2px 10px rgba(0,0,0,.4);
    --bs-box-shadow-lg: 0 14px 44px rgba(0,0,0,.6), 0 3px 10px rgba(0,0,0,.4);

    /* ── Focus ring (dark - a touch stronger; see light block) ───── */
    --bs-focus-ring-width:   0.1875rem;
    --bs-focus-ring-opacity: 0.35;
    --bs-focus-ring-color:   rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity));
    --ez-focus-ring:         0 0 0 var(--bs-focus-ring-width) var(--bs-focus-ring-color);

    /* ── No Bootstrap equivalent ───────────────────────────────── */
    --border-strong:   #42424d;
    --ez-shadow-knob:  0 1px 3px rgba(0, 0, 0, .55);

    /* ── Bootstrap color tokens ────────────────────────────────── */
    --bs-primary:          #af85ff;
    --bs-primary-rgb:      175, 133, 255;
    --bs-secondary:        #6c757d;
    --bs-secondary-rgb:    108, 117, 125;
    --bs-success:          #198754;
    --bs-success-rgb:       25, 135,  84;
    --bs-warning:          #ffc107;
    --bs-warning-rgb:      255, 193,   7;
    --bs-danger:           #dc3545;
    --bs-danger-rgb:       220,  53,  69;
    --bs-info:             #0dcaf0;
    --bs-info-rgb:          13, 202, 240;

    /* ── Bootstrap text tokens ─────────────────────────────────── */
    --bs-body-color:       #fafafa;
    --bs-secondary-color:  #a1a1aa;
    --bs-tertiary-color:   #d4d4d8;
    /* Bootstrap uses --bs-emphasis-color-rgb for table text, hover/stripe overlays.
       [data-bs-theme=dark] flips these to white; we must mirror that here. */
    --bs-emphasis-color:     #fff;
    --bs-emphasis-color-rgb: 255, 255, 255;

    /* ── Bootstrap surface tokens ──────────────────────────────── */
    --bs-body-bg:              #141416;
    --bs-body-bg-rgb:          20, 20, 22;
    --bs-secondary-bg:         #0d0d0f;
    --bs-secondary-bg-rgb:     13, 13, 15;
    --bs-tertiary-bg:          #09090b;
    --bs-tertiary-bg-rgb:      9,  9,  11;

    /* ── Brand "subtle" surface tokens (dark; see light block) ──── */
    /* Slightly less brand and a darker ground than the light block, so the result is a
       muted "blacky purple" that lifts off the card without glowing. */
    --bs-primary-bg-subtle:     color-mix(in srgb, var(--bs-primary) 12%, var(--bs-body-bg));
    --bs-primary-border-subtle: color-mix(in srgb, var(--bs-primary) 28%, var(--bs-body-bg));
    --bs-primary-text-emphasis: color-mix(in srgb, var(--bs-primary) 70%, #fff);

    /* ── Bootstrap border / link tokens ────────────────────────── */
    --bs-border-color:       #2e2e35;
    /* On the dark surface the brand token reads well as-is; hover lightens for affordance. */
    --bs-link-color:         var(--bs-primary);
    --bs-link-color-rgb:     var(--bs-primary-rgb);
    --bs-link-hover-color:   color-mix(in srgb, var(--bs-primary) 70%, #fff);
    --bs-code-color:         var(--bs-primary);

    /* ── Bootstrap component tokens ────────────────────────────── */
    --bs-card-bg:            var(--bs-body-bg);
    --bs-card-border-color:  var(--bs-border-color);
    --bs-card-cap-bg:        transparent;
    --bs-modal-bg:           var(--bs-body-bg);
    --bs-modal-border-color: var(--border-strong);

    color-scheme: dark;
}

body { 
    background: var(--bs-tertiary-bg);
    color: var(--bs-body-color);
}

/* ── Base link styles ─────────────────────────────────────────── */
a {
    color: var(--bs-link-color);
    transition: color 0.2s ease;
}
a:hover {
    color: var(--bs-link-hover-color);
}

/* ── Logo sizing ──────────────────────────────────────────────── */
/* EzLogo renders two <img> elements (light + dark). These rules are global because
   child-component output gets no scoped-CSS attribute. The admin-configured height arrives
   as the --ez-logo-max-h custom property set by EzLogo on its anchor; here we keep the
   logos responsive and aspect-correct. */
.brand-logo {
    width: auto;
    max-width: 100%;
    height: auto;
    max-height: var(--ez-logo-max-h, 2.5rem);
}

/* Show only the image matching the active colour mode (the [data-bs-theme] attribute is set
   on <html> by the theme switcher, and forced to "dark" on the auth hero). */
.brand-logo--light { display: block; }
.brand-logo--dark  { display: none; }
[data-bs-theme="dark"] .brand-logo--light { display: none; }
[data-bs-theme="dark"] .brand-logo--dark  { display: block; }

/* Bootstrap's .btn-close is a black SVG; invert it in dark mode so the modal X stays
   visible. Kept explicitly even though Bootstrap also inverts it under [data-bs-theme=dark]. */
[data-bs-theme="dark"] .btn-close {
    filter: invert(1) grayscale(100%) brightness(200%);
}

/* Sidebar brand row: push the mobile close button to the far right (replaces the old flex:1). */
.brand-wordmark {
    min-width: 0;
    margin-right: auto;
    object-fit: contain;
    object-position: left center;
}

/* Logo upload previews on the admin settings page - each on its matching backdrop. */
.logo-preview {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 5.25rem;
    padding: 0.75rem 1rem;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius, 0.375rem);
}
.logo-preview img { max-height: 3.75rem; width: auto; max-width: 100%; }
.logo-preview--light { background: #ffffff; }
.logo-preview--dark  { background: #0c1a2e; }

/* ── App shell (flex-based sidebar layout) ──────────────────── */

/* App shell. Base = the full-bleed mobile surface: edge-to-edge with no card border/margin, and
   transparent so the page ground (--bs-tertiary-bg = #09090b, the marketing site's ground colour)
   shows through the transparent header and behind the #141416 content cards - exactly the site's
   surface. The boxed card (centred max-width, border, radius, outer gutter) is added only at >=xl
   in the min-width block below, where the persistent side-menu appears. display:flex and
   overflow:hidden are global: flex drives the sidebar+main row, and overflow:hidden guards against
   stray horizontal overflow on mobile and clips the rounded card corners on desktop. */
.layout {
    display: flex;
    min-height: 100vh;
    overflow: hidden;
}

/* Google's PlaceAutocompleteElement (address autocomplete) renders its suggestion list
   inside the component's shadow DOM, absolutely positioned - so any overflow-clipping
   ancestor cuts the list off. EzActionMenu escapes clipping via CSS anchor positioning,
   but that cannot reach inside a third-party shadow root; instead, un-clip the shell
   only while it actually hosts an address autocomplete. */
.layout:has(gmp-place-autocomplete) { overflow: visible; }

/* At >=xl the app becomes a boxed shell: a centred card (border, radius, outer gutter) sitting
   next to the persistent side-menu. Below xl none of this applies, so the layout is full-bleed. */
@media (min-width: 1200px) {
    .layout {
        min-height: calc(100vh - var(--app-gutter-y, 0) * 2);
        max-width: min(var(--app-max-w), calc(100vw - var(--app-gutter) * 2));
        margin-inline: auto;
        margin-block: var(--app-gutter-y, 0);
        margin-bottom: 2rem;
        background: var(--bs-body-bg);
        border: 1px solid var(--bs-border-color);
        border-radius: var(--app-radius, 0);
    }
}

/* Main content column.

   Bottom padding = the surface's own gutter PLUS a shared 1.5rem (Bootstrap spacer 4) of breathing
   room, so a page's last card never sits hard against the bottom edge of the screen. The extra rides
   a custom property rather than being baked into each breakpoint's value so a page can opt out in
   ONE place: add .ez-page-flush-bottom to the page root (see the rule under the media blocks below).
   Opting out is for full-bleed interactive surfaces whose own layout already reaches the bottom -
   VCR's styling studios are the only ones today. */
.main {
    flex: 1;
    min-width: 0;
    padding: 0.875rem 1.5rem calc(1.75rem + var(--ez-page-bottom-extra, 1.5rem));
}

/* Page content area */
.page { margin-top: 1rem; }

/* Page header */
.page-header {
    display: flex;
    align-items: center;
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: var(--radius-lg);
    padding: 0.625rem;
    min-height: 3.875rem;
}

/* Page title / breadcrumb row */
.page-title {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 0.3rem;
    flex: 1;
    min-width: 0;
    overflow: hidden;
    padding: 0 0.5rem;
}
/* EzBreadcrumb renders h1 as a direct flex child of .page-title.
   Typography comes from .ez-title-1 on the h1; this rule keeps only
   the flex/truncation behavior. */
.page-title h1 {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    flex: 1;
}
/* Underline only the label text, not the arrow icon or the gap between them
   (a default anchor underline spans every inline descendant, including whitespace). */
.breadcrumb-back { text-decoration: none; }
.breadcrumb-back span { text-decoration: underline; }

/* Brand logo shown in the header on mobile/tablet only (see LayoutHeader). No fixed cap: it uses
   the app's configured (sidebar) logo height via .brand-logo's --ez-logo-max-h, so the app's
   mobile header logo matches the marketing site's header logo. */
.header-logo { display: inline-flex; align-items: center; flex: none; }

/* Responsive: collapse sidebar and adjust layout - Bootstrap < xl */
@media (max-width: 1199.98px) {
    /* Content gutter: match the marketing site (its .vm-shell uses 1.25rem) - the single source
       of truth for the app's mobile side padding. No top padding so the header bar sits flush at
       the top like the site's sticky nav. */
    .main { padding: 0 1.25rem calc(1.5rem + var(--ez-page-bottom-extra, 1.5rem)); }

    /* Mobile/tablet header = an EXACT clone of the marketing site's nav bar (source of truth):
       a borderless, card-less row (hamburger + logo + actions) that is 7.5rem tall with its
       content vertically centred, a 1.5rem hamburger<->logo gap, and a divider flush at the bar's
       bottom. Only the actions differ from the site (credit badge / bell / avatar vs its CTA).
       - column-gap 1.5rem = the site's .vm-nav-inner gap; row-gap 0 keeps the divider flush.
       - padding 0: the row content aligns to .main's 1.25rem gutter, like the site's .vm-shell. */
    .page-header {
        flex-wrap: wrap;
        align-items: center;
        background: transparent;
        border: 0;
        border-radius: 0;
        padding: 0;
        min-height: 0;
        column-gap: 1.5rem;
        row-gap: 0;
    }
    /* The logo carries the bar's 7.5rem height (matching the site's .vm-nav-inner min-height): it
       makes the first flex line 7.5rem tall and, being inline-flex + centred, sits the 70px logo
       dead centre - exactly the site's bar. Doing it here (not on .page-header) keeps the wrapped
       breadcrumb line OUT of the bar height, so the bar is a true fixed-height row like the site. */
    .header-logo { min-height: 7.5rem; }
    /* Breadcrumb / page title drops onto its own line under the bar, separated by a full-bleed
       divider flush at the bar's bottom (mirrors the site nav's border-bottom). The negative inline
       margin cancels .main's 1.25rem gutter so the rule spans edge-to-edge; the matching inline
       padding re-aligns the heading text back to the gutter. */
    .page-title {
        flex-basis: 100%;
        order: 5;
        /* The 7.5rem bar is taller than the logo's ink, which leaves ~0.8rem of dead space at its
           bottom. The studio pages reclaim exactly that (their content pulls up against the ink),
           so pulling this row up by the same amount lands the divider at the SAME height on every
           page - otherwise studio pages draw their line at ~107px and everything else at 120px. */
        margin: -0.8125rem -1.25rem 0;
        /* Tightened from 1rem so the header block sits as close to the studio pages' as it can
           while still carrying a title (they hide theirs, so their bar is the 7.5rem row alone). */
        padding: 0.5rem 1.25rem 0;
        /* ONE divider, and a faded one: a hairline drawn as a background gradient that dissolves
           toward both screen edges, instead of a solid grey rule ending abruptly at them. Painted
           rather than bordered so nothing changes the row's box or spacing. */
        border-top: 0;
        background-image: linear-gradient(to right,
            transparent 0%,
            var(--bs-border-color) 14%,
            var(--bs-border-color) 86%,
            transparent 100%);
        background-repeat: no-repeat;
        background-size: 100% 1px;
        background-position: top center;
    }
}

/* Bootstrap < md */
@media (max-width: 767.98px) {
    .page { margin-top: 1.125rem; }
}

/* Phone in landscape: wide but very short (a 410px-tall viewport is typical). The 7.5rem logo bar
   is sized to match the marketing site's nav, which is ~29% of the screen here - so the branding is
   scaled down for this orientation only, giving the page back roughly 64px of the little height it
   has. Keyed on height + orientation, NOT width, so a genuine tablet or desktop keeps the full bar. */
@media (orientation: landscape) and (max-height: 32rem) {
    .header-logo { min-height: 3.5rem; }
    .header-logo .brand-logo { --ez-logo-max-h: 2.25rem; }
    /* The title row's pull is calibrated to the tall bar's dead space; the short bar has far less. */
    .page-title { margin-top: -0.25rem; padding-top: 0.35rem; }
}

/* Bootstrap .btn is display:inline-block with no gap - override to flex so
   icon + label spacing is intentional, not dependent on whitespace text nodes.
   Value matches gap-2 (0.5rem) for consistency with the rest of the UI.
   Centring, height and the leading icon/spinner slot live in ez-platform-buttons.css
   alongside the .btn-ez-* tiers. */
.btn {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    position: relative;
    overflow: visible;
}

/* .ez-btn-basic is gone. It existed to mark <button> elements that could not be an
   <EzButton> because they carry complex child content - the user-menu toggle rows and
   the user card. EzButton takes ChildContent now, so there is nothing left to mark and
   no second set of button metrics to keep in step with the first. */

/* Bootstrap < sm */
@media (max-width: 575.98px) {
    /* Edge-to-edge is already applied from the < xl block above; here only the bottom padding
       tightens on phones. The borderless header-bar + full-bleed divider rules carry through. */
    .main { padding: 0 1.25rem calc(1.25rem + var(--ez-page-bottom-extra, 1.5rem)); }
    .page-title { gap: 0.375rem; }
    .page-title h1 { font-size: 1.125rem; }
}

/* Opt out of the shared bottom breathing room (see .main above). One declaration covers every
   breakpoint because each one adds the same custom property. Declared after the media blocks so it
   is not tangled up in them - the property is read by .main's own padding at whatever width. */
.main:has(.ez-page-flush-bottom) { --ez-page-bottom-extra: 0rem; }

/* ── Component exceptions: things Bootstrap cannot express natively ──── */

/* EzDragHandle: the six-dot grip marking the draggable spot on a sortable row.
   Lifted from the form builder, which had the original treatment - deliberately very faint, so
   the row reads as content with one grabbable corner rather than as a control. The grab/grabbing
   cursor is the whole point of the component, and it is also why the handle is a focusable div
   rather than a <button>: Bootstrap's reboot sets cursor:pointer on button at a specificity the
   grab cursor cannot beat. */
.ez-drag-handle {
    display: inline-flex;
    align-items: center;
    padding: 0;
    line-height: 1;
    font-size: 0.8125rem;
    color: var(--bs-border-color);
    cursor: grab;
}
/* While dragging: SortableJS marks the row .sortable-chosen, the form builder canvas marks it
   .fb-field--chosen. Both need the cursor to follow the pointer, not just the handle. */
.ez-drag-handle:active,
.sortable-chosen .ez-drag-handle,
.fb-field--chosen .ez-drag-handle { cursor: grabbing; }

/* EzButtonBadge: count badge overlay on icon buttons
   Uses Bootstrap badge classes (text-bg-*) for color variants.
   .btn prefix + !important to win over Bootstrap .badge layout. */
.btn .ez-btn-badge,
.ez-btn-badge {
    position: absolute !important;
    top: 0.125rem !important;
    right: 0.125rem !important;
    left: auto !important;
    bottom: auto !important;
    min-width: 0.875rem !important;
    height: 0.875rem !important;
    padding: 0 0.1875rem !important;
    border-radius: 0.4375rem !important;
    font-size: 0.5625rem !important;
    font-weight: 700;
    line-height: 0.875rem !important;
    text-align: center;
    pointer-events: none;
    z-index: 10;
}

/* EzCard: remove header/footer tinted background and internal dividers.
   Bootstrap defines --bs-card-cap-bg on .card itself, not :root, so the
   inherited token override above has no effect. Must override here. */
.card { --bs-card-cap-bg: transparent; --bs-card-border-color: var(--bs-border-color); }
/* No divider line between cap and body, so the header's bottom padding and the body's
   top padding (likewise body-bottom + footer-top) would stack into a doubled gap.
   Zero the inner cap padding and let the body's spacer provide a single, consistent
   1rem rhythm above and below the body content. */
.card-header { border-bottom: 0; padding-top: var(--bs-card-spacer-y); padding-bottom: 0; }
.card-footer { border-top: 0; padding-top: 0; padding-bottom: var(--bs-card-spacer-y); }

/* Three-level heading scale. Role-based (not tied to h1-h6), so a page always shows
   a clear title -> card/table title -> in-card sub-section hierarchy instead of every
   level rendering at the same size. Variant cards keep their text-* colour, which wins
   via Bootstrap's !important utilities. */

/* Level 1: page title (EzPageTop / EzBreadcrumb h1). One per page, the largest. */
.ez-title-1 {
    margin-bottom: 0;
    font-size: 1.25rem;
    font-weight: 700;
    letter-spacing: -0.05em;
    text-transform: uppercase;
    color: var(--bs-emphasis-color);
}

/* Level 2: card / table panel title (EzCard, EzTable). One step down from the page
   title so a card never competes with it. */
.ez-title-2 {
    margin-bottom: 0;
    font-size: 1rem;
    font-weight: 700;
    letter-spacing: -0.03em;
    text-transform: uppercase;
    color: var(--bs-emphasis-color);
}

/* Level 3: in-card sub-group heading - a labelled cluster of fields nested inside a
   Level 1 section. Rendered by EzCardSection with Level="2" (see docs/agents/ui-razor.md).
   No uppercase - signals "still inside the same card/panel", not a new panel of its own. */
.ez-title-3 {
    margin-bottom: 0;
    font-size: 0.9375rem;
    font-weight: 600;
    color: var(--bs-emphasis-color);
}

/* EzCardSection spacing - the whitespace that separates one form section from the
   previous one. Owned here rather than by padding/margin utilities in the component so a
   single rhythm governs every form. Fields inside a section are already 1rem apart
   (each field's own mb-3); when a section break was worth roughly the same, a long
   settings form read as one undifferentiated stack of inputs with headings dropped in.

   A Level 1 section stacks its gap ABOVE the divider rule (on top of the preceding
   field's mb-3, so ~2.5rem of clear space before the line) and repeats it below, so the
   heading is never crowded by the rule. Level 2 is a sub-group inside a section, so it
   takes the smaller gap - still comfortably past 1rem once the preceding field's mb-3 is
   counted, but visibly tighter than a full section break, which is what keeps the two
   levels readable as a hierarchy. The panel variant has no rule and takes the gap as a
   plain top margin. A section that opens its card has nothing above it to separate from,
   so it drops the leading space entirely. */
:root {
    --ez-card-section-gap: 1.5rem;
    --ez-card-section-gap-sub: 1rem;
}

.ez-card-section { margin-top: var(--ez-card-section-gap); }
.ez-card-section.border-top { padding-top: var(--ez-card-section-gap); }
.ez-card-section-sub { margin-top: var(--ez-card-section-gap-sub); }
.ez-card-section:first-child { margin-top: 0; }

/* btn-outline-secondary uses Bootstrap's defaults - secondary
   border and text. Do not re-add a card-border override; the search box border and icon
   are matched to this via var(--bs-secondary). */

/* The pressed/expanded state moved to .btn-ez-outline[aria-pressed] in
   ez-platform-buttons.css, and became a subtle fill rather than a solid brand one - a
   loud fill shouted beside the quiet neighbours it now sits with. Nothing renders bare
   .btn-outline-secondary any more, so the old rule is gone rather than left to rot. */

/* btn-primary / btn-outline-primary follow the brand token. Bootstrap bakes a literal
   #0d6efd into these classes, so without this override solid primary buttons stay blue
   even though --bs-primary is the lavender brand. Solid fill uses white text (consistent
   with the other solid variants); hover/active darken the fill via color-mix. Outline
   text matches its border (the brand colour) and fills to the brand with white text on
   hover/active. The other solid variants (secondary/success/warning/danger/info) need no
   override - their tokens already equal Bootstrap's defaults. */
.btn-primary {
    --bs-btn-bg:                 var(--bs-primary);
    --bs-btn-border-color:       var(--bs-primary);
    --bs-btn-color:              #fff;
    --bs-btn-hover-bg:           color-mix(in srgb, var(--bs-primary) 88%, #000);
    --bs-btn-hover-border-color: color-mix(in srgb, var(--bs-primary) 85%, #000);
    --bs-btn-hover-color:        #fff;
    --bs-btn-active-bg:          color-mix(in srgb, var(--bs-primary) 80%, #000);
    --bs-btn-active-border-color:color-mix(in srgb, var(--bs-primary) 78%, #000);
    --bs-btn-active-color:       #fff;
    --bs-btn-disabled-bg:        var(--bs-primary);
    --bs-btn-disabled-border-color: var(--bs-primary);
    --bs-btn-disabled-color:     #fff;
}
.btn-outline-primary {
    --bs-btn-color:              var(--bs-primary);
    --bs-btn-border-color:       var(--bs-primary);
    --bs-btn-hover-bg:           var(--bs-primary);
    --bs-btn-hover-border-color: var(--bs-primary);
    --bs-btn-hover-color:        #fff;
    --bs-btn-active-bg:          var(--bs-primary);
    --bs-btn-active-border-color:var(--bs-primary);
    --bs-btn-active-color:       #fff;
}

/* Form hint text: italic to visually distinguish from field labels */
.form-text { font-style: italic; }

/* EzPagination: map Bootstrap pagination tokens to design tokens */
.pagination {
    --bs-pagination-color:                 var(--bs-tertiary-color);
    --bs-pagination-bg:                    var(--bs-body-bg);
    --bs-pagination-border-color:          var(--bs-border-color);
    --bs-pagination-hover-color:           var(--bs-primary);
    --bs-pagination-hover-bg:              rgba(var(--bs-primary-rgb), .08);
    --bs-pagination-hover-border-color:    var(--bs-border-color);
    --bs-pagination-focus-color:           var(--bs-primary);
    --bs-pagination-focus-bg:              rgba(var(--bs-primary-rgb), .10);
    --bs-pagination-focus-box-shadow:      var(--ez-focus-ring);
    --bs-pagination-active-color:          var(--bs-primary);
    --bs-pagination-active-bg:             rgba(var(--bs-primary-rgb), .12);
    --bs-pagination-active-border-color:   var(--bs-primary);
    --bs-pagination-disabled-color:        var(--bs-secondary-color);
    --bs-pagination-disabled-bg:           var(--bs-body-bg);
    --bs-pagination-disabled-border-color: var(--bs-border-color);
}

/* EzPagination: hide browser spinners on the jump-to-page number input */
#ez-jump-page::-webkit-inner-spin-button,
#ez-jump-page::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
#ez-jump-page[type=number] { -moz-appearance: textfield; }

/* ── Accordion (Bootstrap .accordion) ─────────────────────────────────────
   Bootstrap's accordion is the one common component that is still hard-wired to
   ITS defaults in three places, so a rebranded theme leaves it looking foreign:
   the focus ring is a literal blue rgba(13,110,253,.25), and BOTH chevrons are
   SVG data URIs with baked-in hex fills (#212529 / #052c65 in light, #6ea8fe in
   the [data-bs-theme=dark] override). Everything else is already token-driven -
   including the expanded header, which rides --bs-primary-bg-subtle /
   --bs-primary-text-emphasis and therefore follows the brand automatically
   because those two are derived from --bs-primary at the top of this file.
   The mapping below finishes the job: brand focus ring, app radius, and the
   compact padding the 0.875rem body font asks for. */
.accordion {
    --bs-accordion-color:               var(--bs-body-color);
    --bs-accordion-bg:                  var(--bs-body-bg);
    --bs-accordion-border-color:        var(--bs-border-color);
    --bs-accordion-border-radius:       var(--radius);
    --bs-accordion-inner-border-radius: calc(var(--radius) - var(--bs-border-width));
    --bs-accordion-btn-color:           var(--bs-body-color);
    /* Header and body share the card's 1rem horizontal rhythm; the header is a touch
       tighter vertically so a closed accordion reads as a control, not a panel. */
    --bs-accordion-btn-padding-x:        1rem;
    --bs-accordion-btn-padding-y:        0.75rem;
    --bs-accordion-body-padding-x:       1rem;
    --bs-accordion-body-padding-y:       1rem;
    --bs-accordion-btn-focus-box-shadow: var(--ez-focus-ring);
    --bs-accordion-active-color:         var(--bs-primary-text-emphasis);
    --bs-accordion-active-bg:            var(--bs-primary-bg-subtle);
}

/* The header is a heading, not body copy - Bootstrap leaves it at the inherited weight. */
.accordion-button { font-weight: 600; }

/* Hover affordance (Bootstrap gives the button no hover state at all). Closed lifts off
   the card with the same faint brand wash used by ghost buttons and pagination; open
   deepens its existing subtle tint rather than fighting it. */
.accordion-button.collapsed:hover { background-color: rgba(var(--bs-primary-rgb), .06); }
.accordion-button:not(.collapsed):hover {
    background-color: color-mix(in srgb, var(--bs-primary) 20%, var(--bs-accordion-bg));
}

/* Chevron: drop Bootstrap's two hard-coded SVG fills and paint the glyph with
   currentColor through a mask instead. One declaration then covers every case -
   light, dark, closed (body colour) and open (brand emphasis colour) - and a
   rebrand needs no new icon. background-image is zeroed on BOTH selectors because
   .accordion-button:not(.collapsed)::after sets it at higher specificity, and the
   dark-mode override re-points the icon variables on the pseudo-element itself.
   The rotate-on-open transform is Bootstrap's and still applies. */
.accordion-button::after,
.accordion-button:not(.collapsed)::after {
    background-image: none;
    background-color: currentColor;
    -webkit-mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M3 6L8 11L13 6'/%3e%3c/svg%3e");
            mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M3 6L8 11L13 6'/%3e%3c/svg%3e");
    -webkit-mask-repeat: no-repeat;
            mask-repeat: no-repeat;
    -webkit-mask-position: center;
            mask-position: center;
    -webkit-mask-size: var(--bs-accordion-btn-icon-width);
            mask-size: var(--bs-accordion-btn-icon-width);
}

/* ── EzAnnouncementBanner ───────────────────────────────────── */
/* z-index above all content; Bootstrap utilities only reach z-3 */
.annc-group { position: relative; z-index: 1045; isolation: isolate; }
/* Clickable strip: pointer cursor and hover darkening (no Bootstrap utility) */
.annc--linked { cursor: pointer; }
.annc--linked:hover { filter: brightness(0.92); }
/* Fixed-colour themes with no Bootstrap text-bg-* equivalent (beta rides the brand primary) */
.annc--celebrate   { background-color: #be185d; }
.annc--new-feature { background-color: #059669; }
.annc--beta        { background-color: var(--bs-primary); }
/* Maintenance: muted panel that adapts to light and dark themes */
.annc--maintenance { background-color: var(--bs-secondary-bg); color: var(--bs-body-color); border-bottom-color: var(--bs-border-color); }
/* Body: width cap and normalise HTML content authored by admins */
.annc-body { max-width: 64ch; text-align: center; }
.annc-body :is(p, ul, ol) { margin: 0; }
.annc-body a { color: inherit; font-weight: 600; text-decoration: underline; text-underline-offset: 2px; }
.annc-body strong, .annc-body b { font-weight: 700; }
.annc-body code { padding: 0.1em 0.35em; background: rgba(255,255,255,.22); border-radius: 0.25rem; }
/* Announcement dismiss uses btn-close btn-close-white (Bootstrap default close button). */

/* ── KPI tile (rendered inside EzCard) ─────────────────────── */
/* Value-forward metric tile. Use a standalone tile as:
     <EzCard Variant="success" Class="ez-kpi-card"><div class="ez-kpi">...</div></EzCard>
   or drop the <div class="ez-kpi"> straight into an existing card body.
   The icon colour is driven by the EzCard variant via its border-* class. */
.ez-kpi-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 0.5rem;
}
.ez-kpi-label { font-size: 0.8125rem; color: var(--bs-secondary-color); }
.ez-kpi-icon  { font-size: 1.125rem; color: var(--bs-secondary-color); flex-shrink: 0; }
.ez-kpi-value {
    margin-top: 0.25rem;
    font-size: 1.5rem;
    font-weight: 700;
    line-height: 1.15;
    color: var(--bs-emphasis-color);
}
.ez-kpi-trend { font-size: 0.75rem; margin-left: 0.35rem; }
.ez-kpi-trend--up   { color: var(--bs-success); }
.ez-kpi-trend--down { color: var(--bs-warning); }
/* Icon tint follows the EzCard variant (border-* class it already emits). */
.ez-kpi-card.border-primary   .ez-kpi-icon { color: var(--bs-primary); }
.ez-kpi-card.border-secondary .ez-kpi-icon { color: var(--bs-secondary); }
.ez-kpi-card.border-success   .ez-kpi-icon { color: var(--bs-success); }
.ez-kpi-card.border-warning   .ez-kpi-icon { color: var(--bs-warning); }
.ez-kpi-card.border-danger    .ez-kpi-icon { color: var(--bs-danger); }
.ez-kpi-card.border-info      .ez-kpi-icon { color: var(--bs-info); }

/* ── Color palette demo (UI Examples > Colors) ─────────────── */
/* Swatch chip beside each hex value in the comparison table. The per-swatch
   background colour is set inline because it is dynamic demo data. */
.color-swatch {
    display: inline-block;
    width: 1.25rem;
    height: 1.25rem;
    border-radius: var(--radius-xs);
    border: 1px solid var(--bs-border-color);
    vertical-align: -0.25rem;
    margin-right: 0.5rem;
}
/* Native colour picker used in the Tweak column - sized to match the swatch row. */
.color-tweak {
    width: 2.5rem;
    height: 1.75rem;
    padding: 0;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--radius-xs);
    background: var(--bs-body-bg);
    cursor: pointer;
}

/* ── EzImpersonationBanner ─────────────────────────────────── */

.ez-impersonation-banner {
    position: sticky;
    top: 0;
    z-index: 1050;
    background: #fbbf24;
    border-bottom: 2px solid color-mix(in srgb, #fbbf24 72%, #000);
}

.ez-imp-inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.4375rem 1.25rem;
}

.ez-imp-left {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    min-width: 0;
}

.ez-imp-icon {
    color: rgba(0, 0, 0, 0.75);
    flex-shrink: 0;
}

.ez-imp-text {
    font-weight: 500;
    color: rgba(0, 0, 0, 0.85);
    display: flex;
    align-items: center;
    gap: 0.375rem;
    min-width: 0;
}

.ez-imp-user {
    font-weight: 700;
    background: rgba(0, 0, 0, 0.12);
    color: rgba(0, 0, 0, 0.9);
    padding: 0.125rem 0.5rem;
    border-radius: var(--radius-sm);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 20rem;
}

.ez-imp-end-btn {
    flex-shrink: 0;
    background: #dc2626 !important;
    border-color: transparent !important;
    color: #fff !important;
}

.ez-imp-end-btn:hover {
    background: color-mix(in srgb, #dc2626 85%, #fff) !important;
}

/* ── Table utilities ───────────────────────────────────────── */

/* Header cells never wrap: keep the label and any sort icon on one line.
   Body cells keep their default wrapping behaviour. */
.table thead th { white-space: nowrap; }

/* Body cells are vertically centered so mixed-height content (icons, badges,
   action menus, avatars) lines up with adjacent single-line text. Bootstrap's
   default is vertical-align: inherit -> baseline, which leaves rows looking
   top-heavy. Tables that deliberately want top alignment (the Error Log detail
   row, the permission matrix) override this with a more specific selector. */
.table > tbody > tr > td { vertical-align: middle; }

/* Sortable headers: clickable, with the sort icon held inline beside the label. */
.table thead th.sortable { cursor: pointer; -webkit-user-select: none; user-select: none; }
.table thead th.sortable .sort-icon { margin-left: 0.35em; opacity: 0.5; }
.table thead th.sortable.is-sorted .sort-icon,
.table thead th.sortable:hover .sort-icon { opacity: 1; }

/* Actions column: shrink to content width, no wrap, no trailing padding */
.col-actions { width: 1px; white-space: nowrap; padding-right: 0 !important; }

/* Selection column (EzTable Selectable): shrink to the checkbox, centered. */
.col-select { width: 1px; white-space: nowrap; text-align: center; }
.col-select .form-check-input { margin: 0; cursor: pointer; }

/* Channel columns (In-App / Push / Email / SMS on messaging and automation lists):
   one fixed, equal width so the channel checks line up across every table. */
.col-channel { width: 5.5rem; text-align: center; }

/* Archived rows: dim and strike through all cells except the actions column */
.row-archived td:not(.col-actions) { opacity: 0.5; text-decoration: line-through; }
.row-archived td:not(.col-actions) .badge { text-decoration: none; }

/* Single-line cells that clip overflow to an ellipsis. Pairs with a
   fixed-layout table (or a cell max-width) so the column width is honoured
   and long, unbroken content (URLs, stack frames) can't stretch the row. */
.cell-truncate {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Auto-layout variant of .cell-truncate: carries its own width cap, so a long
   unbroken value (an email address, a business name, preview text) cannot
   stretch its column and push the whole table into a wide horizontal scroll
   on phones. Pair with title="..." so the full value stays reachable. */
.td-clamp {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 20rem;
}
@media (max-width: 767.98px) {
    .td-clamp { max-width: 12rem; }
}

/* ── Error log table ─────────────────────────────────────────────────────
   Fixed layout pins every column to its declared width so the borderless
   Message column flexes into the remaining space and truncates, rather than
   a long JS exception URL forcing the column wide and ballooning row height. */
.errors-table-container .table { table-layout: fixed; width: 100%; }
.errors-table-container .col-select      { width: 2.5rem; }
.errors-table-container .col-actions     { width: 3rem; }
.errors-table-container .col-w-status    { width: 7rem; }
.errors-table-container .col-w-source    { width: 7.5rem; }
.errors-table-container .col-w-occurred  { width: 11rem; }
.errors-table-container .col-w-exception { width: 12rem; }
.errors-table-container .col-w-path      { width: 14rem; }

/* Bulk action bar shown above the error table when rows are selected. */
.bulk-action-bar {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    margin: 0 0 0.75rem;
    padding: 0.5rem 0.875rem;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    background: var(--bs-tertiary-bg);
}
.bulk-action-count { font-weight: 600; color: var(--bs-body-color); }

/* Summary-row cells stay on a single line; only the message row below wraps. */
.errors-table-container thead th,
.errors-table-container tbody tr:not(.ez-row-detail) > td { white-space: nowrap; }

/* ── Error log triage summary chips ─────────────────────────────────────────── */
.error-summary {
    display: flex;
    flex-wrap: wrap;
    gap: 0.625rem;
    margin: 0 0 1rem;
}
.error-summary-chip {
    display: inline-flex;
    align-items: baseline;
    gap: 0.4rem;
    padding: 0.4rem 0.75rem;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    background: var(--bs-body-bg);
    color: var(--bs-body-color);
    cursor: pointer;
    transition: border-color 0.12s ease, background-color 0.12s ease;
}
.error-summary-chip:hover { background: var(--bs-tertiary-bg); }
.error-summary-chip .error-summary-count { font-size: 1.05rem; font-weight: 700; line-height: 1; }
.error-summary-chip .error-summary-label {
    font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.04em;
    color: var(--bs-secondary-color);
}
.error-summary-chip.is-critical { border-color: var(--bs-danger); }
.error-summary-chip.is-critical .error-summary-count { color: var(--bs-danger); }
.error-summary-chip.is-error { border-color: var(--bs-warning); }
.error-summary-chip.is-error .error-summary-count { color: var(--bs-warning); }
.error-summary-chip.is-warning { border-color: var(--bs-info); }
.error-summary-chip.is-warning .error-summary-count { color: var(--bs-info); }

/* Full-width message row sitting directly beneath each error's summary row.
   The summary row drops its bottom border so the pair reads as one unit. */
.errors-table-container tbody tr:has(+ .ez-row-detail) > td { border-bottom: 0; }
.errors-table-container .ez-row-detail > td {
    border-top: 0;
    padding-top: 0.25rem;
    padding-bottom: 0.875rem;
    vertical-align: top;
}
.errors-table-container .ez-row-detail .error-message-spacer { padding: 0; }

/* Keep the summary row and its message row highlighted together on hover. */
.errors-table-container tbody tr:hover + .ez-row-detail > td,
.errors-table-container tbody tr:has(+ .ez-row-detail:hover) > td {
    background-color: var(--bs-table-hover-bg);
}

/* ============================================================
   .ez-code-block - reusable inline code/output panel
   Renders monospace text (messages, stack traces, payloads, IDs)
   in a bordered, tinted panel that wraps long unbroken strings.
   Use on a <pre> to preserve original line breaks.
   ============================================================ */
.ez-code-block {
    margin: 0;
    padding: 0.5rem 0.75rem;
    font-family: var(--bs-font-monospace);
    font-size: 0.8125rem;
    line-height: 1.5;
    color: var(--bs-body-color);
    background-color: var(--bs-tertiary-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    white-space: pre-wrap;
    overflow-wrap: anywhere;   /* wrap long unbroken URLs/stack frames */
}

/* ============================================================
   Audit / error detail modal - metadata grid + before/after diff
   Shared by the Audit Log and Error Log "View details" modals.
   ============================================================ */

/* Two-column definition grid for entry metadata. */
.audit-detail-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.75rem 1.5rem;
    margin: 0 0 1.25rem;
}
.audit-detail-grid > div { min-width: 0; }
.audit-detail-grid .audit-detail-full { grid-column: 1 / -1; }
.audit-detail-grid dt {
    margin: 0 0 0.125rem;
    font-size: 0.6875rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--bs-secondary-color);
}
.audit-detail-grid dd {
    margin: 0;
    color: var(--bs-body-color);
    overflow-wrap: anywhere;
}
.audit-detail-reason { display: block; margin-top: 0.25rem; color: var(--bs-danger); }

/* Section heading above a diff or stack trace. */
.audit-diff-heading {
    margin: 1.25rem 0 0.5rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--bs-secondary-color);
}

/* ── Unified before/after field diff (GitHub-style) ──────────────────────── */
.audit-diff-block {
    overflow: hidden;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    font-family: var(--bs-font-monospace);
    font-size: 0.8125rem;
    line-height: 1.6;
}
.diff-line {
    display: flex;
    gap: 0.5rem;
    padding: 0.0625rem 0.75rem;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}
.diff-gutter {
    flex: none;
    width: 1ch;
    text-align: center;
    color: var(--bs-secondary-color);
    -webkit-user-select: none;
    user-select: none;
}
.diff-body { min-width: 0; }
.diff-field-name { color: var(--bs-secondary-color); }

.diff-line--removed { background-color: rgba(var(--bs-danger-rgb), 0.12); }
.diff-line--removed .diff-gutter { color: var(--bs-danger); }
.diff-line--added { background-color: rgba(var(--bs-success-rgb), 0.12); }
.diff-line--added .diff-gutter { color: var(--bs-success); }
.diff-line--context { color: var(--bs-secondary-color); }

/* Revertable hint icon shown beside the result badge in the table. */
.audit-revert-hint { color: var(--bs-warning); opacity: 0.85; }

/* Inline link-styled button (e.g. the navigable correlation id in the detail modal). */
.btn-link-inline {
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: var(--bs-link-color);
    cursor: pointer;
    text-decoration: underline;
    text-underline-offset: 2px;
}
.btn-link-inline:hover { color: var(--bs-link-hover-color); }

/* "Changed only" toggle above the field diff. */
.audit-diff-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    color: var(--bs-secondary-color);
    cursor: pointer;
    -webkit-user-select: none;
    user-select: none;
}
.audit-diff-toggle input { cursor: pointer; }

/* ============================================================
   EzActionBar
   ============================================================ */

/* Reusable 1px vertical rule for the .page-actions flex row.
   Place <span class="page-actions-divider"> between any two flex children.
   Shown automatically when the immediately preceding sibling has content. */
.page-actions-divider {
    display: none;
    width: 1px;
    align-self: stretch;
    background: var(--bs-border-color);
    flex-shrink: 0;
    margin: 0 0.25rem;
}
.page-actions-search:has(*) + .page-actions-divider {
    display: block;
}
/* Divider placed directly inside the buttons zone */
.page-actions-buttons .page-actions-divider {
    display: block;
}

/* The row's top spacing, and the ONLY source of it - EzActionBar.razor deliberately carries no mt-*
   utility, because a utility class cannot be conditional and the spacing has to disappear along with
   the bar. Both zones collapse to display:none when their SectionOutlet is empty, so on a page with
   no search and no actions (every form / edit / detail page) the row is zero-height; an
   unconditional margin there is 1rem of dead space above the content with nothing to separate it
   from. 1rem matches the mt-3 this replaces, so a page that HAS a toolbar is unchanged.

   `> *` and NOT a nested `:has(*)`: :has() may not contain another :has(). Such a selector is a
   syntax error that browsers drop silently, taking the whole rule with it - which is exactly what
   happened to the previous version of this rule, so it never applied on any page. */
.page-actions:has(.page-actions-search > *, .page-actions-buttons > *) {
    margin-top: 1rem; /* Bootstrap spacer 3 */
}

/* Left zone: flex-fill utility on element handles flex growth.
   min-width: 0 prevents overflow in flex containers - no Bootstrap equivalent.
   display:none default is overridden by :has(*). */
.page-actions-search {
    display: none;
    min-width: 0;
}
.page-actions-search:has(*) { display: flex; }

/* Right zone: hidden until content arrives via SectionOutlet. */
.page-actions-buttons { display: none; }
.page-actions-buttons:has(*) { display: flex; }

/* Active filter chips row: gap-2 and layout utilities on element.
   display:none cannot use d-none - Bootstrap d-none is !important and blocks :has() override. */
.page-current-filters { display: none; }
.page-current-filters:has(*) { display: flex; }

/* Overflow trigger zone: the icon-only button that collapses the page's action buttons on small
   screens. Hidden outright at >= md and whenever the page supplied no actions at all. */
.page-actions-toggle-zone { display: none; }

/* Bootstrap < md: the action buttons stop wrapping onto their own line and hide behind the trigger,
   which sits immediately beside the search bar. The buttons zone becomes a panel dropping from the
   row's right edge - directly under the trigger, since the zone leaves the flex flow and the trigger
   is then the last visible item in the row. */
@media (max-width: 767.98px) {
    /* Only worth a trigger when the page actually pushed actions into the outlet.
       `> *` and NOT a nested `:has(*)`: :has() may not contain another :has(), and a selector that
       does is a syntax error the browser drops silently - taking the whole rule with it. */
    .page-actions:has(.page-actions-buttons > *) .page-actions-toggle-zone {
        display: flex;
        align-items: center;
        flex: none;
    }

    /* Anchor for the absolutely positioned panel below. */
    .page-actions { position: relative; }

    /* Beats the base `.page-actions-buttons:has(*) { display: flex }` (equal specificity, declared
       later); .is-open then beats this one on specificity. */
    .page-actions-buttons:has(*) {
        display: none;
        position: absolute;
        top: 100%;
        right: 0;
        z-index: 1050; /* above .user-menu-backdrop (1049) */
        min-width: 12rem;
        max-width: min(20rem, calc(100vw - 2.5rem));
        margin-top: 0.375rem;
        padding: 0.375rem;
        background: var(--bs-body-bg);
        border: 1px solid var(--border-strong);
        border-radius: var(--radius-lg);
    }
    .page-actions-buttons.is-open:has(*) {
        display: flex;
        flex-direction: column;
        align-items: stretch;
    }
    /* Stacked full-width rows so the panel reads as one list of actions. Variant colours are kept:
       the primary action should still stand out here exactly as it does in the header. */
    .page-actions-buttons.is-open .btn {
        width: 100%;
        justify-content: flex-start;
    }
}

/* Bootstrap < sm */
@media (max-width: 575.98px) {
    .page-actions-divider { display: none !important; }
}

/* Placeholder text: dim it below Bootstrap's default so it reads clearly as a hint/mask
   rather than as entered content. Applies to every .form-control (inputs + textareas). */
.form-control::placeholder {
    color: var(--bs-tertiary-color);
    opacity: 0.55;
}

/* Google PlaceAutocompleteElement (address autocomplete): its input + suggestion list live
   in a closed shadow DOM styled with Google's Material defaults. The --gmp-* custom properties
   theme the surfaces, text and dropdown panel, but the input's *border* is NOT one of them: it
   is a hard-coded `border: 1px solid black` on the shadow's `:host` rule (confirmed by dumping
   the shadow stylesheet). An outer-document rule on the host element outranks the shadow's
   `:host` rule, so we restyle the resting + hover border here to track .form-control exactly
   (grey at rest, brand-mix on hover). The FOCUS treatment cannot live here: Chrome does not
   apply a `:focus-within` rule to this host because focus is inside a CLOSED shadow tree (tested
   - it never paints, even with !important), so the brand focus border + halo are injected onto
   Google's own `.focus-ring` element in customFieldAddress.js instead. */
gmp-place-autocomplete {
    --gmp-mat-color-surface: var(--bs-body-bg);
    --gmp-mat-color-surface-container: var(--bs-body-bg);
    --gmp-mat-color-on-surface: var(--bs-body-color);
    --gmp-mat-color-on-surface-variant: var(--bs-secondary-color);
    --gmp-mat-color-primary: var(--bs-primary);
    --gmp-mat-font-family: var(--bs-body-font-family);
    --gmp-card-border-radius: var(--radius-sm);

    background-color: var(--bs-body-bg);
    border: var(--bs-border-width, 1px) solid var(--bs-border-color);
    border-radius: var(--radius-sm);
    transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
gmp-place-autocomplete:hover:not(:focus-within) {
    border-color: var(--ez-field-border-hover, var(--border-strong));
}

/* EzSearchInput: Bootstrap input-group with prepended icon */
/* Hide native browser 'x' on search inputs */
.ez-search-input input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; }
/* No border override here any more: the search box is matched to the OUTLINE TIER, whose
   border is the --bs-border-color hairline that .form-control already carries. The old
   --bs-secondary pinning existed to match the heavier btn-outline-secondary and now would
   make the search box louder than the Filter button beside it. */
/* The [data-theme-forms="on"] resting/hover overrides for this component live in
   ez-platform-forms.css, next to the rest of that theme's rules - see the "Grouped
   fields" section there for why they're needed. */
/* Clear button: Bootstrap resets cursor on input-group-text buttons */
.ez-search-input__clear { cursor: pointer; }
.ez-search-input__clear:hover { color: var(--bs-body-color) !important; }

/* Focus ring on ALL input groups: match a standalone field's focus (the brand ring via
   --bs-focus-ring-*), but draw ONE ring around the whole .input-group instead of each
   element, so a grouped field (icon + input) reads identically to a standalone field. */
.input-group:focus-within .form-control:focus,
.input-group:focus-within .form-select:focus { box-shadow: none; }
.input-group:focus-within {
    border-radius: var(--bs-border-radius);
    box-shadow: 0 0 0 var(--bs-focus-ring-width) var(--bs-focus-ring-color);
}
.input-group:focus-within .input-group-text,
.input-group:focus-within .form-control,
.input-group:focus-within .form-select { border-color: var(--bs-primary); } /* brand focused-input border (matches standalone fields) */

/* EzSearchSelect dropdown: mirror the .ez-action-menu panel + primary-tint hover
   (same design tokens) so result items stand out and match the app's other menus. */
.ez-search-select__menu {
    margin-top: 0.375rem;
    padding: 0.375rem;
    background: var(--bs-body-bg);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-lg);
    box-shadow: var(--bs-box-shadow);
    max-height: 18.75rem; /* keep long result lists inside short viewports */
    overflow-y: auto;
}
.ez-search-select__menu .dropdown-item {
    padding: 0.4375rem 0.625rem;
    border-radius: var(--radius-sm);
    white-space: normal;
}
.ez-search-select__menu .dropdown-item > span { font-weight: 500; }
.ez-search-select__menu .dropdown-item:hover,
.ez-search-select__menu .dropdown-item:focus {
    background: rgba(var(--bs-primary-rgb), .08);
    color: var(--bs-primary);
}

/* EzPagination jump-to-page input: fixed compact width (was an inline style). */
.ez-pagination-jump { width: 4.5rem; }

/* ============================================================
   Blazor form validation
   ============================================================ */

/* Blazor adds .valid/.invalid directly to <input> - cannot use Bootstrap utilities. */
.valid.modified:not([type=checkbox]) { outline: 1px solid var(--bs-success); }
.invalid { outline: 1px solid var(--bs-danger); }

/* <ValidationMessage> output: icon prepended via ::before, align-items:baseline
   has no Bootstrap utility equivalent. */
.validation-message {
    color: var(--bs-danger);
    font-size: 0.875em;
    font-weight: 500;
    margin-top: 0.25rem;
    display: flex;
    align-items: baseline;
    gap: 0.3em;
}
.validation-message::before {
    content: '\f071'; /* fa-triangle-exclamation */
    font-family: "Font Awesome 7 Pro";
    font-weight: 400;
    font-style: normal;
    flex-shrink: 0;
}

/* ============================================================
   Shared overlay backdrop
   All overlays that dim and blur the page behind them.
   Transparent click-capture backdrops (action-menu, user-menu)
   carry no background or blur - they are not overlays.
   ============================================================ */

.ez-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, .5);
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
    animation: ez-backdrop-in .2s ease both;
}
@keyframes ez-backdrop-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}

/* ============================================================
   EzFilterDrawer
   Slide-out right panel for page-level filters.
   ============================================================ */

/* Backdrop: extends .ez-backdrop with z-index */
.ez-filter-backdrop { z-index: 1030; }

/* Drawer panel */
.ez-filter-drawer {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    width: min(22rem, 94vw);
    background: var(--bs-body-bg);
    border-left: 1px solid var(--border-strong);
    box-shadow: var(--bs-box-shadow-lg);
    z-index: 1031;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    animation: ez-filter-slide-in .28s cubic-bezier(0.32, 0.72, 0, 1) both;
}
@keyframes ez-filter-slide-in {
    from { transform: translateX(100%); }
    to   { transform: translateX(0); }
}

/* Body: thin scrollbar - no Bootstrap equivalent */
.ez-filter-body {
    overflow-y: auto;
    scrollbar-color: var(--border-strong) transparent;
    scrollbar-width: thin;
}

/* Filter section: each filter control group in the drawer */
.ez-filter-section {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

/* Active filter count indicator inside the drawer header */
.ez-filter-count {
    font-weight: 500;
    color: var(--bs-primary);
    background: rgba(var(--bs-primary-rgb), 0.1);
    border-radius: 0.75rem;
    padding: 0.125rem 0.5rem;
}

/* Section heading label */
.ez-filter-label {
    font-size: 0.875em;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--bs-secondary-color);
}

.ez-filter-hint {
    font-size: 0.875em;
    color: var(--bs-secondary-color);
    margin: 0;
}

/* ── EzFilterChip ─────────────────────────────────────────────────────────── */

.ez-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    padding: 0.25rem 0.625rem;
    border-radius: var(--radius-sm);
    border: 1px solid var(--bs-primary);
    background: rgba(var(--bs-primary-rgb), 0.08);
    color: var(--bs-primary);
    font-weight: 500;
    cursor: pointer;
    line-height: 1.4;
    text-decoration: none;
    transition: background 0.15s, transform 0.15s;
}

.ez-chip:hover {
    background: var(--bs-primary);
    border-color: var(--bs-primary);
    color: #fff;
    transform: translateY(-1px);
}

/* ============================================================
   Bootstrap modal backdrop override
   EzModal renders Bootstrap's .modal-backdrop; override it to
   match the shared .ez-backdrop (dim + blur).
   ============================================================ */

.modal-backdrop {
    --bs-backdrop-bg: rgba(0, 0, 0, .5);
    --bs-backdrop-opacity: 1;
    background: rgba(0, 0, 0, .5);
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
    animation: ez-backdrop-in .2s ease both;
}

/* Body scroll lock while an EzModal is open. Bootstrap does this from its own modal JS, which we
   do not load (EzModal is Blazor/ezModal.js driven), and Bootstrap 5 ships no .modal-open rule -
   so the class is ours. Set by ezModal.js lockScroll(), which reference-counts so an inner modal
   closing cannot unlock the page under an outer one. --ez-scrollbar-gap is the width the vanished
   scrollbar leaves behind: padding it back keeps the page from jumping sideways on open (0 on
   overlay-scrollbar platforms, and on a modal opened over an already unscrollable page). */
body[data-ez-scroll-lock] {
    overflow: hidden;
    padding-right: var(--ez-scrollbar-gap, 0);
}

/* Blazor error UI (#blazor-error-ui): all rules live in ez-platform-error-ui.css
   (single source - do not redeclare them here). */

/* ============================================================
   Blazor reconnect modal (#components-reconnect-modal)
   Shown by Blazor JS when the SignalR circuit is lost.
   ============================================================ */

/* Visibility helpers - Blazor toggles classes on the modal element */
.components-reconnect-first-attempt-visible,
.components-reconnect-repeated-attempt-visible,
.components-reconnect-failed-visible,
.components-pause-visible,
.components-resume-failed-visible,
.components-rejoining-animation { display: none; }

#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
#components-reconnect-modal.components-reconnect-retrying,
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-failed,
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible { display: block; }

#components-reconnect-modal {
    background: var(--bs-body-bg);
    color: var(--bs-body-color);
    width: 22rem;
    max-width: calc(100vw - 2rem);
    margin: 20vh auto;
    padding: 1.75rem;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-lg);
    box-shadow: var(--bs-box-shadow);
    opacity: 0;
    transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
    animation: reconnect-modal-fade-out 0.5s both;
}

#components-reconnect-modal[open] {
    animation: reconnect-modal-slide-up 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s,
               reconnect-modal-fade-in 0.5s ease-in-out 0.3s;
    animation-fill-mode: both;
}

#components-reconnect-modal::backdrop {
    background: rgba(0, 0, 0, .5);
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
    animation: ez-backdrop-in .2s ease both;
}

@keyframes reconnect-modal-slide-up {
    0%   { transform: translateY(30px) scale(0.95); }
    100% { transform: translateY(0); }
}
@keyframes reconnect-modal-fade-in {
    0%   { opacity: 0; }
    100% { opacity: 1; }
}
@keyframes reconnect-modal-fade-out {
    0%   { opacity: 1; }
    100% { opacity: 0; }
}

.components-reconnect-container {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1rem;
}

#components-reconnect-modal p {
    margin: 0;
    text-align: center;
    color: var(--bs-secondary-color);
}

/* Reconnect modal buttons use Bootstrap btn-primary. */

/* Ripple animation shown while reconnecting */
.components-rejoining-animation {
    position: relative;
    width: 5rem;
    height: 5rem;
}
.components-rejoining-animation div {
    position: absolute;
    border: 3px solid var(--bs-primary);
    opacity: 1;
    border-radius: 50%;
    animation: reconnect-ripple 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.components-rejoining-animation div:nth-child(2) { animation-delay: -0.5s; }

@keyframes reconnect-ripple {
    0%   { top: 2.5rem; left: 2.5rem; width: 0;    height: 0;    opacity: 0; }
    4.9% { top: 2.5rem; left: 2.5rem; width: 0;    height: 0;    opacity: 0; }
    5%   { top: 2.5rem; left: 2.5rem; width: 0;    height: 0;    opacity: 1; }
    100% { top: 0;      left: 0;      width: 5rem;  height: 5rem; opacity: 0; }
}

/* ── EzToastHost - fixed toast notification stack ───────────────
   Restored after the host-shell migration (was lost when the old
   consolidated app.css was dropped). Mapped onto the current --bs-*
   theme tokens. */
.ez-toast-host {
    position: fixed;
    bottom: 1rem;
    right: 1rem;
    z-index: 9999;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}
.ez-toast {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    padding: 0.6875rem 0.875rem;
    border-radius: var(--radius);
    border: 1px solid var(--border-strong);
    background: var(--bs-tertiary-bg);
    color: var(--bs-body-color);
    font-size: 0.8125rem;
    min-width: 16rem;
    max-width: 22rem;
    box-shadow: var(--bs-box-shadow);
    animation: ez-toast-in .18s ease;
}
@keyframes ez-toast-in {
    from { opacity: 0; transform: translateY(0.375rem); }
    to   { opacity: 1; transform: translateY(0); }
}
.ez-toast__icon { flex-shrink: 0; }
.ez-toast__msg  { flex: 1; line-height: 1.4; }
.ez-toast--success { border-color: rgba(var(--bs-success-rgb), .3); }
.ez-toast--success .ez-toast__icon { color: var(--bs-success); }
/* Errors look like every other toast - panel background, tinted border and icon - and auto-dismiss
   like them too (see ToastService). Filled-red plus sticky made routine, recoverable errors ("that
   name is taken") read as an incident and left the stack to be cleared by hand. A message that must
   not be missed belongs in a page-level EzAlert. */
.ez-toast--error { border-color: rgba(var(--bs-danger-rgb), .3); }
.ez-toast--error .ez-toast__icon { color: var(--bs-danger); }
/* Warnings stay FILLED: the ones we raise (out of credits, action blocked) stop the user doing what
   they came for, and on a dark phone screen the panel style camouflaged them. */
.ez-toast--warning {
    background: var(--bs-warning);
    border-color: var(--bs-warning);
    color: #1a1a1a;
}
.ez-toast--warning .ez-toast__icon { color: #1a1a1a; }
.ez-toast--info    { border-color: rgba(var(--bs-primary-rgb), .3); }
.ez-toast--info    .ez-toast__icon { color: var(--bs-link-hover-color); }

/* Phones: toasts move to the TOP, full width - bottom-right corner toasts sit behind the thumb,
   fight the bottom tab bar / sticky CTAs, and are easy to miss entirely. Slightly larger type,
   and the entrance slides down from the top edge instead of rising. */
@media (max-width: 575.98px) {
    .ez-toast-host {
        top: calc(0.75rem + env(safe-area-inset-top));
        bottom: auto;
        left: 0.75rem;
        right: 0.75rem;
        /* Newest toast nearest the anchored edge, matching the desktop stack. Both sizes lay toasts
           out in DOM order but hang off opposite edges, so without this the phone stack anchors at
           the top and grows DOWNWARD - the newest one lands in a different place depending on how
           many are already up. A toast that arrives unprompted (server-pushed, announcing work that
           finished long after the click) has to appear somewhere predictable. */
        flex-direction: column-reverse;
    }
    /* The push-notification banner occupies this same top strip, and it is the one that cannot move
       (drawn hanging off the top edge - square top corners, no top border), so the toasts give way.
       The anchor() line is exact and wins wherever anchor positioning is supported; the calc above
       it is the fallback, sized for a two-line banner. Only ever evaluated while a banner is in the
       DOM, so anchor() always has its anchor. */
    body:has(.notif-banner) .ez-toast-host {
        top: calc(0.75rem + env(safe-area-inset-top) + 6.5rem);
        top: calc(anchor(--ez-notif-banner bottom) + 0.75rem);
    }
    .ez-toast {
        width: 100%;
        min-width: 0;
        max-width: none;
        font-size: 0.875rem;
        animation-name: ez-toast-in-top;
    }
}
@keyframes ez-toast-in-top {
    from { opacity: 0; transform: translateY(-0.375rem); }
    to   { opacity: 1; transform: translateY(0); }
}

/* ============================================================
   Support ticket views (TicketPageBody / TicketThread / reply toggle)
   Restored after the host-shell migration (was lost when the old
   consolidated app.css was dropped, same as EzToastHost above).
   Originally per-component scoped CSS; mapped onto the current
   --bs-* theme tokens (--panel→--bs-body-bg, --panel-sub→--bs-secondary-bg,
   --border→--bs-border-color, --text→--bs-body-color,
   --text-muted→--bs-secondary-color, --text-subtle→--bs-tertiary-color).
   ============================================================ */

/* ── Ticket page header ─────────────────────────────────────────────── */
.ticket-head {
    margin-bottom: 1.25rem;
    padding-bottom: 1rem;
    border-bottom: 1px solid var(--bs-border-color);
}
.ticket-head-label {
    margin: 0 0 0.25rem;
    font-size: 0.6875rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--bs-tertiary-color);
}
.ticket-head-title {
    margin: 0 0 0.5rem;
    font-size: 1.625rem;
    font-weight: 700;
    line-height: 1.25;
    color: var(--bs-primary);
    text-transform: uppercase;
    word-wrap: break-word;
}
/* ── Sidebar details list (compact two-column) ──────────────────────── */
.ticket-details {
    display: grid;
    grid-template-columns: max-content 1fr;
    column-gap: 0.875rem;
    row-gap: 0.4375rem;
    font-size: 0.8125rem;
    margin: 0;
}
.ticket-details dt {
    color: var(--bs-secondary-color);
    font-weight: 500;
}
.ticket-details dd {
    margin: 0;
    color: var(--bs-body-color);
    overflow-wrap: anywhere;
}

/* ─── Ticket Thread: GitHub/Linear-style vertical timeline ───────────────── */
.thread {
    list-style: none;
    margin: 0;
    padding: 0;
    position: relative;
}

/* Vertical rail running the full length of the thread */
.thread::before {
    content: '';
    position: absolute;
    left: 1.1875rem;          /* center of the 2.375rem avatar column */
    top: 0.5rem;
    bottom: 0.5rem;
    width: 2px;
    background: var(--bs-border-color);
    border-radius: 1px;
}

.thread-row {
    position: relative;
    display: flex;
    align-items: flex-start;
    gap: 1rem;
    padding: 0.75rem 0;
}

/* ── Message rows ────────────────────────────────────────────────────────── */
.thread-row--msg + .thread-row--msg,
.thread-row--event + .thread-row--msg,
.thread-row--msg + .thread-row--event {
    margin-top: 0.25rem;
}

/* ── Card colour tokens ───────────────────────────────────────────────────── */
/* All card colours live here as CSS custom properties on the row element.    */
/* .thread-card and its children inherit and consume the tokens, so colour    */
/* variants (user, staff, private, archived) only need to override the vars.  */
.thread-row--msg {
    --tc-bg:           var(--bs-body-bg);
    --tc-head-bg:      var(--bs-secondary-bg);
    --tc-border:       var(--bs-border-color);
    --tc-arrow-outer:  var(--bs-border-color);
    --tc-arrow-inner:  var(--bs-secondary-bg);
    --tc-author-color: var(--bs-body-color);
}

.thread-avatar {
    flex: 0 0 2.375rem;
    width: 2.375rem;
    height: 2.375rem;
    border-radius: 50%;
    background: var(--bs-primary);
    color: #fff;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 0.8125rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    border: 3px solid var(--bs-body-bg);
    box-shadow: 0 0 0 1px var(--bs-border-color);
    z-index: 1;
}

.thread-row.is-staff .thread-avatar {
    background: var(--bs-info);
}

.thread-card {
    flex: 1 1 auto;
    min-width: 0;
    position: relative;
    background: var(--tc-bg);
    border: 1px solid var(--tc-border);
    border-radius: var(--radius);
    overflow: hidden;
}

/* Speech-bubble arrow pointing at the avatar */
.thread-card::before,
.thread-card::after {
    content: '';
    position: absolute;
    top: 0.875rem;
    width: 0;
    height: 0;
    border-style: solid;
}
.thread-card::before {
    left: -9px;
    border-width: 8px 9px 8px 0;
    border-color: transparent var(--tc-arrow-outer) transparent transparent;
}
.thread-card::after {
    left: -8px;
    border-width: 8px 9px 8px 0;
    border-color: transparent var(--tc-arrow-inner) transparent transparent;
}

.thread-card-head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.875rem;
    background: var(--tc-head-bg);
    border-bottom: 1px solid var(--tc-border);
    font-size: 0.8125rem;
}

.thread-card-author {
    font-weight: 600;
    color: var(--tc-author-color);
}

.thread-card-role {
    font-size: 0.6875rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    padding: 0.0625rem 0.4375rem;
    border-radius: var(--radius-pill);
    background: rgba(var(--bs-secondary-rgb), .14);
    color: var(--bs-info);
}

.thread-card-time {
    margin-left: auto;
    font-size: 0.75rem;
    color: var(--bs-secondary-color);
}

.thread-card-body {
    padding: 0.875rem;
    color: var(--bs-body-color);
    line-height: 1.55;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.5rem;
}

.thread-card-body-text {
    width: 100%;
    white-space: pre-wrap;
    word-wrap: break-word;
}

.thread-card-files {
    list-style: none;
    margin: 0;
    padding: 0 0.875rem 0.875rem;
    display: flex;
    flex-wrap: wrap;
    gap: 0.375rem;
}

.thread-card-files a {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    padding: 0.25rem 0.625rem;
    background: var(--bs-secondary-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: var(--radius-pill);
    font-size: 0.75rem;
    color: var(--bs-body-color);
    text-decoration: none;
    transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}
.thread-card-files a:hover {
    background: rgba(var(--bs-primary-rgb), .08);
    border-color: var(--bs-primary);
    color: var(--bs-primary);
}
.thread-card-files i { font-size: 0.6875rem; color: var(--bs-secondary-color); }
.thread-card-files a:hover i { color: var(--bs-primary); }

/* ── Event rows ──────────────────────────────────────────────────────────── */
.thread-row--event {
    align-items: flex-start;
}

/* Column-spacer that keeps the compact circle on the rail */
.thread-node {
    flex: 0 0 2.375rem;
    display: flex;
    justify-content: center;
    align-items: flex-start;
    z-index: 1;
}

/* The visual circle sits on the inner <i> so it can be compact */
.thread-node i {
    flex-shrink: 0;
    width: 1.375rem;
    height: 1.375rem;
    border-radius: 50%;
    background: var(--bs-body-bg);
    border: 2px solid var(--bs-border-color);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--bs-secondary-color);
    font-size: 0.5rem;
}

.thread-event {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    font-size: 0.8125rem;
    color: var(--bs-secondary-color);
    line-height: 1.5;
}

.thread-event-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
}

.thread-event-head strong {
    color: var(--bs-body-color);
    font-weight: 600;
}

.thread-event-change {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    flex-wrap: wrap;
}

.thread-event-change em {
    font-style: normal;
    font-weight: 500;
    color: var(--bs-body-color);
}

.thread-event-arrow {
    font-size: 0.625rem;
    color: var(--bs-tertiary-color);
    opacity: 0.7;
}

.thread-event-reason {
    margin: 0;
    color: var(--bs-secondary-color);
    font-style: italic;
    font-size: 0.8125rem;
}

.thread-event-time {
    flex-shrink: 0;
    font-size: 0.75rem;
    color: var(--bs-tertiary-color);
    white-space: nowrap;
}

/* ── Message colour variants ─────────────────────────────────────────────── */
/* Override the CSS tokens defined on .thread-row--msg for each message type. */
/* Values cascade into .thread-card and all descendants automatically.        */

/* User messages - primary tint */
.thread-row--msg.is-user {
    --tc-bg:          rgba(var(--bs-primary-rgb), 0.04);
    --tc-head-bg:     rgba(var(--bs-primary-rgb), 0.08);
    --tc-border:      rgba(var(--bs-primary-rgb), 0.2);
    --tc-arrow-outer: rgba(var(--bs-primary-rgb), 0.25);
    --tc-arrow-inner: rgba(var(--bs-primary-rgb), 0.08);
}

/* Staff messages (public) - info/blue tint */
.thread-row--msg.is-staff {
    --tc-bg:          rgba(var(--bs-info-rgb), 0.04);
    --tc-head-bg:     rgba(var(--bs-info-rgb), 0.10);
    --tc-border:      rgba(var(--bs-info-rgb), 0.2);
    --tc-arrow-outer: rgba(var(--bs-info-rgb), 0.25);
    --tc-arrow-inner: rgba(var(--bs-info-rgb), 0.04);
}

/* -- Private staff notes - amber tint ----------------------------------------- */
/* Specificity (0,3,0) beats .is-staff (0,2,0) so amber always wins.         */
.thread-row--msg.is-staff.thread-row--private {
    --tc-bg:           rgba(var(--bs-warning-rgb), 0.06);
    --tc-head-bg:      rgba(var(--bs-warning-rgb), 0.14);
    --tc-border:       rgba(var(--bs-warning-rgb), 0.25);
    --tc-arrow-outer:  rgba(var(--bs-warning-rgb), 0.35);
    --tc-arrow-inner:  rgba(var(--bs-warning-rgb), 0.14);
    --tc-author-color: var(--bs-warning);
}

.thread-card-private-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    font-size: 0.6875rem;
    font-weight: 600;
    color: var(--bs-danger);
    background: rgba(var(--bs-danger-rgb), 0.1);
    border: 1px solid rgba(var(--bs-danger-rgb), 0.35);
    border-radius: var(--radius);
    padding: 0.1rem 0.375rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.thread-row--private.is-blurred .thread-card-body,
.thread-row--private.is-blurred .thread-card-files {
    filter: blur(4px);
    -webkit-user-select: none;
    user-select: none;
}

/* ── Archived colour tokens ───────────────────────────────────────────────── */
/* Set on the row so they beat the type-specific token declarations above.    */
/* The action menu button lives inside the card but is not targeted here.     */

/* Archived non-private: specificity (0,3,0) beats user/staff (0,2,0) */
.thread-row--msg.has-archived:not(.thread-row--private) {
    --tc-bg:          color-mix(in srgb, var(--bs-body-bg) 50%, transparent);
    --tc-head-bg:     color-mix(in srgb, var(--bs-secondary-bg) 50%, transparent);
    --tc-border:      color-mix(in srgb, var(--bs-border-color) 50%, transparent);
    --tc-arrow-outer: color-mix(in srgb, var(--bs-border-color) 50%, transparent);
    --tc-arrow-inner: color-mix(in srgb, var(--bs-secondary-bg) 50%, transparent);
}

/* Archived private: specificity (0,4,0) beats private rule (0,3,0).         */
/* Keep the amber tint but halve each alpha so the scheme stays recognisable. */
.thread-row--msg.has-archived.is-staff.thread-row--private {
    --tc-bg:          rgba(var(--bs-warning-rgb), 0.03);
    --tc-head-bg:     rgba(var(--bs-warning-rgb), 0.07);
    --tc-border:      rgba(var(--bs-warning-rgb), 0.125);
    --tc-arrow-outer: rgba(var(--bs-warning-rgb), 0.175);
    --tc-arrow-inner: rgba(var(--bs-warning-rgb), 0.07);
}

/* ── Archived messages ───────────────────────────────────────── */
/* Elements targeted individually - opacity cannot be restored on a child    */
/* once set on a parent (it composites), so the action menu stays at 100%.   */
/* Matches the same opacity + strikethrough pattern as .row-archived.        */

.thread-card--archived .thread-card-role,
.thread-card--archived .thread-card-private-badge,
.thread-card--archived .thread-card-archived-label { opacity: 0.5; }

.thread-card--archived .thread-card-author,
.thread-card--archived .thread-card-time,
.thread-card--archived .thread-card-body-text { opacity: 0.5; text-decoration: line-through; }

.thread-card-footer {
    display: flex;
    justify-content: flex-end;
    padding: 0.375rem 0.75rem 0.5rem;
}

.thread-card-archived-label {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    font-size: 0.6875rem;
    font-weight: 600;
    color: var(--bs-secondary-color);
    background: rgba(var(--bs-secondary-rgb), 0.12);
    border: 1px solid var(--bs-border-color);
    border-radius: var(--radius);
    padding: 0.1rem 0.375rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

/* Toggle bar shown above the thread when private notes exist */
.thread-private-toggle {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 0.75rem;
    font-size: 0.8125rem;
    color: var(--bs-secondary-color);
}

.thread-private-toggle button {
    background: none;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--radius);
    padding: 0.15rem 0.5rem;
    font-size: 0.75rem;
    cursor: pointer;
    color: var(--bs-secondary-color);
    transition: color 0.15s, border-color 0.15s;
}

.thread-private-toggle button:hover {
    color: var(--bs-body-color);
    border-color: var(--bs-secondary-color);
}

/* ── Reply mode toggle (admin: public reply vs private note) ─────────────── */
/* Same pill-toggle design as .user-menu-toggle in ez-platform-nav.css - keep the
   two in sync if the toggle look ever changes. */
.reply-mode-toggle {
    width: 2.25rem;
    height: 1.25rem;
    border-radius: var(--radius-pill);
    background: var(--border-strong);
    position: relative;
    flex-shrink: 0;
    transition: background 0.2s;
}

.reply-mode-toggle::after {
    content: '';
    position: absolute;
    top: 0.1875rem;
    left: 0.1875rem;
    width: 0.875rem;
    height: 0.875rem;
    border-radius: 50%;
    background: #fff;
    transition: transform 0.2s;
    box-shadow: var(--ez-shadow-knob);
}

.reply-mode-toggle.is-on { background: var(--bs-warning); }
.reply-mode-toggle.is-on::after { transform: translateX(1rem); }

/* Hide private notes when printing - Print media (not a width breakpoint) */
@media print {
    .thread-row--private {
        display: none !important;
    }

    .thread-private-toggle {
        display: none !important;
    }
}


/* =============================================================
   Permission template matrix + member permission overrides
   (permission-templates editor and the member Permissions page)
   ============================================================= */

/* ── Roles x permissions matrix (template editor) ───────────── */
.perm-matrix-table { margin-bottom: 0; }

.perm-matrix-table th,
.perm-matrix-table td {
    vertical-align: middle;
    border-color: var(--bs-border-color);
}

/* Header rows hold the role definitions - tint to group them. */
.perm-matrix-table thead th { background: var(--bs-tertiary-bg); }

/* First column (permission name / "Role") stays pinned during horizontal scroll. */
.perm-matrix-table th.perm-matrix-rowhead,
.perm-matrix-table td.perm-matrix-rowhead {
    position: sticky;
    left: 0;
    z-index: 1;
    text-align: left;
    min-width: 14rem;
}
.perm-matrix-table th.perm-matrix-rowhead { background: var(--bs-tertiary-bg); }
.perm-matrix-table td.perm-matrix-rowhead { background: var(--bs-body-bg); }

/* Role-definition inputs in the header cells. */
.perm-matrix-table .role-name-select,
.perm-matrix-table .role-display-input,
.perm-matrix-table .role-scope-select {
    min-width: 8.5rem;
    max-width: 12rem;
    margin-inline: auto;
}

/* Role columns: even width with a faint divider for cross-row scanning. */
.perm-matrix-table thead th:not(.perm-matrix-rowhead) { min-width: 9rem; }
.perm-matrix-table thead th:not(.perm-matrix-rowhead),
.perm-matrix-table tbody tr.perm-row td:not(.perm-matrix-rowhead) {
    border-left: 1px solid var(--bs-border-color);
}

/* Larger, easier-to-hit checkboxes. */
.perm-matrix-table tbody .form-check-input {
    width: 1.15rem;
    height: 1.15rem;
    margin: 0;
    cursor: pointer;
}

/* Category divider rows. */
.perm-matrix-table .perm-category-row td {
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--bs-secondary-color);
}

/* Row hover (not the pinned first column, which must stay opaque). */
.perm-matrix-table tbody tr.perm-row:hover td:not(.perm-matrix-rowhead) {
    background: rgba(var(--bs-primary-rgb), 0.04);
}

/* ── Member permissions page ────────────────────────────────── */
/* Resolution summary: a brand accent so the "answer" reads first. */
.resolution-summary { border-left: 3px solid var(--bs-primary); }

/* "Browse all permissions" table column widths (classes, not inline styles). */
.member-perm-table th.perm-col-narrow { width: 7.5rem; }
.member-perm-table th.perm-col-actions { width: 13rem; }

/* ── Modals ─────────────────────────────────────────────────── */
/* Bootstrap's default dialog margin is 0.5rem, which on a phone reads as edge-to-edge - a
   confirmation ends up touching both screen edges. Raise it below sm so every modal keeps a
   visible gutter. Overriding the VARIABLE (rather than .modal-dialog's margin) is deliberate:
   Bootstrap also feeds it into the centred dialog's min-height calc, so the two stay in step.
   From sm up Bootstrap already uses 1.75rem. */
@media (max-width: 575.98px) {
    .modal { --bs-modal-margin: 1.25rem; }
}

/* ── Reduced motion (WCAG 2.3.3 / prefers-reduced-motion) ───────────────
   Users who ask their OS for reduced motion get a near-instant, non-animated
   experience: reconnect ripple, toast/drawer slide-ins, notification-banner
   bounce, skeleton shimmer and every transition collapse to imperceptible.
   The standard defensive reset - scoped to the media query, so it only ever
   applies when the user opted in. */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

/* ── EzTable sticky header ──────────────────────────────────────────────
   Applied to the .table-responsive wrapper by EzTable's Sticky parameter.
   Capping the height turns that wrapper into the scrollport, which is what
   gives the header something to pin to.

   The header cells carry their own background because EzTable leaves <thead>
   unclassed unless a Variant is set - a transparent pinned row would let the
   body rows scroll through it. Reading --bs-table-bg rather than --bs-body-bg
   keeps the tint when a Variant IS set, since table-{variant} sets that var
   and .table defaults it to the body background.

   The divider is an inset line, not a border: Reboot collapses table borders,
   so a th's own border belongs to the table and scrolls out from under the
   pinned row. 1px, no blur, no offset - a hairline rule, not a depth shadow. */
.ez-table-scroll--sticky {
    max-height: 70vh;
    overflow-y: auto;
}

.ez-table-scroll--sticky thead th {
    position: sticky;
    top: 0;
    z-index: 1;
    background-color: var(--bs-table-bg);
    box-shadow: inset 0 -1px 0 var(--bs-border-color);
}

/* ── EzTable loading skeleton ───────────────────────────────────────────
   Placeholder rows shown while a table loads (EzTable IsLoading + SkeletonRows).
   The bar shimmers left-to-right; each row starts fractionally later via --i,
   set as a custom property on the element so no rule is written inline. */
.ez-table-skeleton-bar {
    display: block;
    height: 0.85rem;
    border-radius: var(--bs-border-radius-sm);
    background: linear-gradient(90deg,
        var(--bs-secondary-bg) 25%,
        var(--bs-tertiary-bg) 37%,
        var(--bs-secondary-bg) 63%);
    background-size: 400% 100%;
    animation: ez-table-skeleton 1.4s ease-in-out infinite;
    animation-delay: calc(var(--i, 0) * 0.08s);
}

@keyframes ez-table-skeleton {
    from { background-position: 100% 50%; }
    to   { background-position: 0 50%; }
}
