/* Sticky-footer flex layout. body grows with content on long pages (min-height
   keeps it at least the viewport height), and main occupies all leftover flex
   space so the footer is glued to the bottom of the viewport on short pages
   instead of floating mid-screen. Fitted-page / Chats overrides further down
   replace height: with an explicit 100vh + overflow:hidden when the page
   wants the viewport-bound editor / inbox layout.

   Two min-height declarations on purpose. iOS Chrome (and Safari) report
   `100vh` as the viewport height INCLUDING the area behind the bottom
   browser toolbar, so a body sized to 100vh extends behind the toolbar and
   the flex-end `_Footer` partial ends up below the visible area --
   invisible on every non-fitted page (Home, Login, Companies, Apps, Users,
   New X / Invite X forms, etc.) and ALSO on every fitted page, because
   `min-height: 100vh` here would still beat the fitted rules' explicit
   `height: 100dvh` further down (height is clamped by min-height). Adding
   `min-height: 100dvh` as the second declaration tracks the *visible*
   viewport on every iOS browser since 15.4 / every evergreen browser, so
   the footer renders just above the toolbar where it belongs. The first
   100vh line stays as a fallback for the small number of browsers that
   don't yet grok dvh -- they keep the previous (iOS-buggy) behaviour
   instead of getting no minimum height at all. */
body {
    min-height: 100vh;
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
}
body > main {
    flex: 1 0 auto;     /* grow to fill, never shrink below natural height */
}
.site-footer {
    flex: 0 0 auto;     /* keep the footer at its natural rendered height */
}

/* App header background.
   * Light mode: slate blue (#334155) with solid-white link text -- the slate
     is dark enough to separate the header from white page content but a clear
     step away from pure black, and the muted Bootstrap default link color
     reads as muddy gray against it, so we bump it to full white.
   * Dark mode: revert to Bootstrap's stock bg-dark + navbar-dark colors so
     the header blends naturally with the rest of the dark UI. */
.bg-app-header {
    background-color: #334155 !important;
    --bs-navbar-color: #fff;
    --bs-navbar-hover-color: #fff;
    /* Active = "you're currently on this page". Amber sits comfortably
       against the slate header, reads as clearly different from the white
       idle / hover state, and matches the warm-on-cool contrast logic the
       rest of the UI already uses (status-Initiated row, etc.). */
    --bs-navbar-active-color: #fcd34d;
}
[data-bs-theme="dark"] .bg-app-header {
    background-color: #212529 !important;                       /* Bootstrap --bs-dark */
    --bs-navbar-color: rgba(255, 255, 255, 0.55);              /* navbar-dark defaults */
    --bs-navbar-hover-color: rgba(255, 255, 255, 0.75);
    --bs-navbar-active-color: #fcd34d;                          /* same amber against charcoal */
}
/* Slightly heavier weight on the active link so users with mild colour
   blindness still get the affordance. Subtle -- the colour change is the
   primary signal. */
.bg-app-header .navbar-nav .nav-link.active {
    font-weight: 600;
}

/* The brand wordmark's text sits in the upper portion of its SVG (with the
   tail dipping below). Without this nudge the centered nav-link items
   ("Chats", "Settings", availability toggles, etc.) line up with the
   geometric middle of the brand img, which is visually LOWER than the text.
   Pulling them up a couple px lines them up with the brand wordmark.
   The negative margin trick on the brand img (see _Layout.cshtml) doesn't
   propagate to siblings, so we do this explicitly. */
.navbar .navbar-nav .nav-link { position: relative; top: 2px; }
.navbar .nav-availability-toggle,
.navbar .nav-sound-toggle,
.navbar .nav-agents-online { position: relative; top: 2px; }

/* Sticky navbar. Long-form pages (Help, Developer) used to scroll the header
   off-screen, hiding the company picker + nav links + the sign-out button.
   position:sticky keeps it pinned without taking it out of flow, so the
   rest of the layout (Chats split-view, content cards, etc.) doesn't need
   any top-padding adjustment to compensate. z-index above the body but
   below the Bootstrap modal layer (1050). */
.navbar.bg-app-header {
    position: sticky;
    top: 0;
    z-index: 1030;
}

/* Hamburger toggler: the dark-themed navbar wants the inverse SVG so the
   three bars actually show. Bootstrap's default toggler-icon is the
   light-on-dark variant for navbar-dark; we just need to make sure no
   custom override broke it + give it a subtle border on focus. */
.navbar-dark .navbar-toggler {
    border-color: rgba(255,255,255,.25);
    /* ~10% smaller than Bootstrap's default. Combines a font-size shrink
       (which scales the toggler-icon SVG since Bootstrap sizes it in em)
       with proportionally tighter padding. */
    font-size: .9rem;
    padding: .2rem .5rem;
}
.navbar-dark .navbar-toggler:focus {
    box-shadow: 0 0 0 .15rem rgba(255,255,255,.2);
}

/* Right-cluster icons (agents/availability/sound/profile) live outside the
   navbar-collapse so they stay visible on mobile. Bootstrap's default
   .navbar-nav is column-direction below the expand breakpoint; we override
   to keep this row horizontal at every viewport size. */
.navbar .navbar-icons-cluster {
    flex-direction: row !important;
    flex-wrap: nowrap;
}

/* The right-group wraps the cluster + the hamburger so they travel
   together. flex-wrap:nowrap on the group itself forces those two
   elements to stay on the same row -- the WHOLE group can still wrap to
   a new line in the navbar's outer flex (which is what we want when the
   brand is too wide to share line 1 with them).
   flex-shrink:0 means the right group never gives up width to the
   page-links area on the left -- the priority-nav JS below redistributes
   items into an overflow dropdown instead of letting the cluster scrunch. */
.navbar .navbar-right-group {
    flex-wrap: nowrap;
    flex-shrink: 0;
}

/* Priority nav: the page-links list shrinks (visually) by hiding overflow
   items into a dropdown menu. The JS at the bottom of _Layout.cshtml
   manages which items live in the main list vs the overflow menu.

   The flex-grow:1 + min-width:0 combo is what lets the JS detect overflow:
   without it, primary-links would size to its content and the me-auto
   margin would absorb the leftover space, so scrollWidth would always
   equal clientWidth and nothing would ever move into "More". With
   flex-grow:1 the ul fills the navbar-collapse, and items overflow the
   moment they don't fit.

   Note: we deliberately do NOT use overflow:hidden here. That would clip
   the "More" dropdown menu when it drops down below the trigger, since
   Bootstrap navbar dropdowns are positioned with position:absolute (Popper
   is disabled in navbars) and they live inside this ul. The JS already
   keeps content fitting via scrollWidth measurement, so overflow:hidden
   would only have been a belt-and-suspenders clip -- and the cost was
   making the dropdown menu invisible. */
.navbar .navbar-nav.primary-links {
    flex-wrap: nowrap;
    min-width: 0;          /* allow shrinking below content size */
    flex-grow: 1;          /* fill the navbar-collapse so overflow is real */
}
/* Keep individual nav items at their natural width. Without this they'd
   shrink (default flex-shrink:1), the text would wrap or compress, and
   scrollWidth would never exceed clientWidth -- so the priority-nav JS
   would have nothing to do. Pinned to flex-shrink:0 means items keep
   their full width and overflow cleanly off the right edge, which is
   what the JS measures against to decide what to move into "More". */
.navbar .navbar-nav.primary-links > .nav-item {
    flex-shrink: 0;
}
/* And the navbar-collapse itself needs to be shrinkable below its
   content size at md+, otherwise it pushes against the right-group and
   nothing inside it ever overflows. min-width:0 unlocks the shrink. */
@media (min-width: 768px) {
    .navbar.bg-app-header .navbar-collapse {
        min-width: 0;
    }
}
.navbar .nav-overflow .dropdown-toggle::after {
    /* No visible text label any more -- the caret IS the trigger. Drop the
       left margin so the caret sits centered in the nav-link's padding,
       and size up roughly 40% from Bootstrap's default .3em so the caret
       reads clearly without a text label next to it. Nudge it down 3px
       so it sits visually centered with the icon row to its right (the
       people / availability / sound icons are vertically centered around
       a slightly lower baseline than text). */
    margin-left: 0;
    border-top-width: .5em;
    border-right-width: .5em;
    border-left-width: .5em;
    position: relative;
    top: 3px;
}
/* Inside the overflow dropdown, the items came from the navbar with
   .nav-item / .nav-link classes still attached. Re-style them as
   dropdown items so they read correctly in the popout. */
.navbar .nav-overflow .dropdown-menu .nav-link {
    padding: .35rem 1rem;
    color: var(--bs-body-color);
    font-size: .9rem;
}
.navbar .nav-overflow .dropdown-menu .nav-link:hover,
.navbar .nav-overflow .dropdown-menu .nav-link:focus {
    background: var(--bs-tertiary-bg);
    color: var(--bs-link-color);
}

/* Mobile-only drawer behavior. Below md, the navbar-collapse becomes a
   floating, right-justified dropdown that overlays the page content
   instead of pushing it down. Matches the navbar's dark slate (light
   theme) / near-black (dark theme) background so it reads as a single
   continuous header element. */
@media (max-width: 767.98px) {
    .navbar.bg-app-header .navbar-collapse {
        position: absolute;
        top: 100%;                     /* sit immediately under the header */
        right: 0;
        min-width: 220px;
        max-width: 90vw;
        background-color: #334155;     /* match .bg-app-header light theme */
        padding: .75rem 1rem;
        border-radius: 0 0 0 8px;
        box-shadow: 0 8px 24px rgba(0,0,0,.35);
        z-index: 1029;                 /* below the navbar itself (1030) */
    }
    [data-bs-theme="dark"] .navbar.bg-app-header .navbar-collapse {
        background-color: #212529;     /* match .bg-app-header dark theme */
    }
    /* Right-justify the drawer items so they line up under the hamburger. */
    .navbar.bg-app-header .navbar-collapse .navbar-nav { align-items: flex-end !important; }
    .navbar.bg-app-header .navbar-collapse .nav-link   { text-align: right; }
}

/* Brand-icon and favicon stay static on new-chat alerts -- only the browser
   tab title flashes (see notifications.js flashTitle). The alert-variant
   SVG + ICO files are still on disk under wwwroot/images/ in case we ever
   want to re-enable the swap. */

/* Brand-image theme swap. Two PNG variants live in the navbar; CSS picks the
   one that matches the current data-bs-theme. The light variant ships with
   the pale-cyan bubble that pops against the slate header; the dark variant
   uses a light-grey bubble that reads against the deeper-black dark navbar. */
.navbar-brand .brand-img-dark { display: none; }
[data-bs-theme="dark"] .navbar-brand .brand-img-light { display: none; }
[data-bs-theme="dark"] .navbar-brand .brand-img-dark  { display: inline; }

/* Navbar availability toggle. Icon-only — green check-circle when the
   signed-in user is available, red x-circle when not. Both <svg>s sit in
   the DOM (server-rendered with the current state); class-driven CSS
   shows only the one matching the current state, JS toggles the class on
   click. Matches the profile icon's sizing for a balanced navbar. */
.nav-availability-toggle {
    background: transparent;
    border: 0;
    padding: 0 .35rem;
    line-height: 1;
    cursor: pointer;
    transition: opacity .12s;
}
.nav-availability-toggle:hover,
.nav-availability-toggle:focus { opacity: .8; outline: none; }
.nav-availability-toggle:focus-visible {
    outline: 2px solid #6ea8fe;
    outline-offset: 2px;
    border-radius: 50%;
}
.nav-availability-toggle .icon-available,
.nav-availability-toggle .icon-unavailable,
.nav-availability-toggle .icon-scheduled-off {
    display: none;
}
.nav-availability-toggle.is-available     .icon-available     { display: inline-block; }
.nav-availability-toggle.is-scheduled-off .icon-scheduled-off { display: inline-block; }
.nav-availability-toggle.is-unavailable   .icon-unavailable   { display: inline-block; }

/* New-chat sound toggle. Same display-only-the-matching-icon pattern as
   the availability button -- two SVGs in the DOM, CSS shows the one
   matching .is-on / .is-off. */
.nav-sound-toggle {
    background: transparent;
    border: 0;
    padding: 0 .35rem;
    line-height: 1;
    cursor: pointer;
    transition: opacity .12s;
}
.nav-sound-toggle:hover,
.nav-sound-toggle:focus { opacity: .8; outline: none; }
.nav-sound-toggle:focus-visible {
    outline: 2px solid #6ea8fe;
    outline-offset: 2px;
    border-radius: 50%;
}
.nav-sound-toggle .icon-sound-on,
.nav-sound-toggle .icon-sound-off { display: none; }
.nav-sound-toggle.is-on  .icon-sound-on  { display: inline-block; }
.nav-sound-toggle.is-off .icon-sound-off { display: inline-block; }

/* Glow behind the sound icon while the continuous alarm is active.
   notifications.js toggles body.has-pending-alarm when there is at least
   one unread ticket on the agent's queue, and clears it once any agent
   reads it (per-ticket MarkTicketRead cross-user sync). So this glow
   persists indefinitely until an unread chat is opened. Effect mimics
   the chat-client bubble's wec-bubble-ring -- a soft amber expanding
   ring pulse so it reads as "needs attention" without being garish.
   Shown even when the icon is in .is-off (muted) state: muted only
   silences the audio; the visual cue is still useful. */
body.has-pending-alarm .nav-sound-toggle {
    border-radius: 50%;
    animation: nav-sound-alarm-ring 1.8s ease-out infinite;
}
@keyframes nav-sound-alarm-ring {
    0%   { box-shadow: 0 0 0 0    rgba(240, 173, 78, .65); }
    70%  { box-shadow: 0 0 0 12px rgba(240, 173, 78, 0);   }
    100% { box-shadow: 0 0 0 0    rgba(240, 173, 78, 0);   }
}

/* Pulsing amber chip behind the navbar "Chats" link while the alarm is
   active. Shares the body.has-pending-alarm hook with the sound-icon
   glow above so both visual cues come on / go off in lockstep. The
   highlight is intentionally a soft tinted pill rather than a hard
   color swap so it doesn't fight the active-tab styling on the /Chats
   page (where .active already paints the link). Stays on after the
   audio cap is reached -- per spec, the flash continues until the
   agent personally clicks a NEW chat row (which acks the alarm). */
body.has-pending-alarm .nav-chats-link {
    border-radius: .375rem;
    animation: nav-chats-alarm-flash 1.4s ease-in-out infinite;
}
@keyframes nav-chats-alarm-flash {
    /* Light-mode peak: previous .35 alpha amber on a light navbar
       background read as a barely-tinted peach -- the cue easily
       missed in peripheral vision. Bumped to a deeper saturated
       fill (.85 alpha) + a ring outline so the chip has both a
       defined edge and a strong tint. Text shifts to near-black
       for contrast against the now-solid amber. Dark mode keeps
       its original muted variant below since the contrast
       problem only existed against a light navbar. */
    0%, 100% {
        background-color: rgba(240, 173, 78, 0);
        box-shadow: 0 0 0 0 rgba(240, 173, 78, 0);
        color: inherit;
    }
    50% {
        background-color: rgba(240, 173, 78, .85);
        box-shadow: 0 0 0 1px rgba(204, 133, 0, .55);
        color: #3d2c00;
    }
}
/* Dark-mode variant: amber stays the cue color, but lift the text to a
   warmer foreground so it reads on the dark navbar background. */
[data-bs-theme="dark"] body.has-pending-alarm .nav-chats-link {
    animation-name: nav-chats-alarm-flash-dark;
}
@keyframes nav-chats-alarm-flash-dark {
    0%, 100% {
        background-color: rgba(240, 173, 78, 0);
        color: inherit;
    }
    50% {
        background-color: rgba(240, 173, 78, .25);
        color: #ffe69c;
    }
}

/* Agents-online navbar indicator + hover/focus popover.
   The container is position:relative so the absolutely-positioned popover
   anchors below the number. We open on :hover and :focus-within so it's
   keyboard-accessible (tabindex=0 on the parent).
   The popover uses Bootstrap CSS vars so it picks up the right surface +
   border + text color in light AND dark mode without needing two rules. */
.nav-agents-online {
    position: relative;
    cursor: default;
    font-size: .9rem;
    line-height: 1;
    user-select: none;
}
.nav-agents-online:focus,
.nav-agents-online:focus-visible {
    outline: 2px solid #6ea8fe;
    outline-offset: 2px;
    border-radius: .25rem;
}
.nav-agents-popover {
    position: absolute;
    top: calc(100% + .25rem);
    right: 0;
    z-index: 1080;
    min-width: 200px;
    max-width: 320px;
    background: var(--bs-body-bg);
    color: var(--bs-body-color);
    border: 1px solid var(--bs-border-color);
    border-radius: .375rem;
    padding: .5rem .75rem;
    box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .15);
    font-size: .85rem;
    display: none;
}
.nav-agents-popover ul li {
    padding: .1rem 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.nav-agents-online:hover .nav-agents-popover,
.nav-agents-online:focus-within .nav-agents-popover,
.nav-agents-online.is-open .nav-agents-popover {
    /* .is-open is the touch-friendly path: a click handler in _Layout
       toggles the class on tap, since :hover is not a thing on touch
       devices and :focus doesn't fire reliably on a non-button parent. */
    display: block;
}

/* Settings shell */
.settings-shell {
    display: grid;
    grid-template-columns: 220px 1fr;
    gap: 1rem;
    align-items: start;
}
.settings-side {
    background: var(--bs-body-bg);
    color: var(--bs-body-color);
    border: 1px solid var(--bs-border-color);
    border-radius: .375rem;
    padding: 1rem;
    position: sticky;
    top: 1rem;
}
.settings-side .settings-nav a {
    display: block;
    padding: .35rem .6rem;
    text-decoration: none;
    color: var(--bs-secondary-color);
    border-radius: .25rem;
    font-size: .95rem;
}
.settings-side .settings-nav a:hover { background: var(--bs-tertiary-bg); color: var(--bs-link-hover-color); }
.settings-side .settings-nav a.active { background: var(--bs-primary); color: #fff; font-weight: 600; }
.settings-main { min-width: 0; }

/* ---------- Settings topbar (narrow viewports) -----------------------------
   Hidden by default. The sidebar (.settings-side) handles wide layouts; the
   topbar takes over below 900px to spare vertical space and give the App
   picker + section nav a horizontal home above the page content. */
.settings-topbar { display: none; }

@media (max-width: 899.98px) {
    .settings-shell {
        /* minmax(0, 1fr) — not just 1fr — so the column can't be forced
           wider than the viewport by overflowing content in settings-main
           (e.g. a wide table). Without this the topbar gets dragged off
           the right edge alongside the page-card's overflowing content. */
        grid-template-columns: minmax(0, 1fr);
        /* Stack the topbar above the main content area. auto for the topbar
           lets it size to its content; auto for main so the page-card-wide
           layouts grow naturally below it. The fitted variant overrides
           the main row to minmax(0,1fr) further down. */
        grid-template-rows: auto auto;
        gap: .75rem;
    }
    .settings-side { display: none; }
    .settings-topbar {
        display: flex;
        flex-direction: column;
        gap: .55rem;
        padding: .65rem .75rem .7rem;
        background: var(--bs-body-bg);
        color: var(--bs-body-color);
        border: 1px solid var(--bs-border-color);
        border-radius: .375rem;
        /* min-width:0 lets this flex/grid item shrink below its
           intrinsic content width — without it the App picker's max-width
           360px would still try to claim 360px even when the viewport is
           narrower than that. */
        min-width: 0;
    }

    /* App picker row: label sits inline with the dropdown so the bar reads
       as one tidy horizontal strip rather than a stacked sub-form. */
    .settings-topbar-app {
        display: flex;
        align-items: center;
        gap: .5rem;
        margin: 0;
        min-width: 0;
    }
    .settings-topbar-app > label {
        white-space: nowrap;
        font-weight: 600;
        flex: 0 0 auto;
    }
    .settings-topbar-app > select {
        flex: 1 1 auto;
        min-width: 0;
        max-width: 360px;
    }

    /* Section pill row: a single horizontally-scrolling strip flanked
       by left/right chevron buttons. flex:nowrap + overflow-x:auto on
       the nav; the area itself is flex so the chevrons sit beside the
       strip. Hidden scrollbar (the chevrons + swipe are the affordance). */
    .settings-topbar-nav-area {
        position: relative;
        margin: 0 -.75rem -.7rem;
        display: flex;
        align-items: center;
        gap: .25rem;
        padding: 0 .25rem;
    }
    .settings-topbar-nav {
        flex: 1 1 auto;
        min-width: 0;
        display: flex;
        flex-wrap: nowrap;
        overflow-x: auto;
        gap: .35rem;
        padding: .1rem .5rem .55rem;
        /* No scroll-behavior:smooth here -- the initial centering
           sets nav.scrollLeft directly and we want that to be instant.
           Chevron clicks pass behavior:"smooth" to scrollBy() so they
           still animate. */
        /* Hide native scrollbar across engines. The chevrons + swipe
           are the user's affordance; the scrollbar would just steal
           vertical room and look out of place inside the band. */
        scrollbar-width: none;
        -ms-overflow-style: none;
    }
    .settings-topbar-nav::-webkit-scrollbar { display: none; }

    .settings-topbar-pill {
        flex: 0 0 auto;
        padding: .35rem .75rem;
        font-size: .9rem;
        line-height: 1.2;
        color: var(--bs-secondary-color);
        background: var(--bs-tertiary-bg);
        border: 1px solid var(--bs-border-color);
        border-radius: 999px;     /* pill */
        text-decoration: none;
        white-space: nowrap;
        transition: background .12s, color .12s, border-color .12s;
    }
    .settings-topbar-pill:hover,
    .settings-topbar-pill:focus-visible {
        color: var(--bs-link-hover-color);
        background: var(--bs-secondary-bg);
        border-color: var(--bs-border-color);
        text-decoration: none;
    }
    .settings-topbar-pill.active {
        background: var(--bs-primary);
        border-color: var(--bs-primary);
        color: #fff;
        font-weight: 600;
    }
    .settings-topbar-pill.active:hover { color: #fff; }

    /* Scroll arrows. Compact circular buttons flanking the pill strip.
       Each is shown only when there's room to scroll in its direction. */
    .settings-topbar-nav-arrow {
        flex: 0 0 auto;
        width: 28px;
        height: 28px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        background: var(--bs-body-bg);
        color: var(--bs-secondary-color);
        border: 1px solid var(--bs-border-color);
        border-radius: 999px;
        padding: 0;
        cursor: pointer;
        box-shadow: 0 1px 3px rgba(0, 0, 0, .08);
        transition: color .12s, background .12s, border-color .12s;
    }
    .settings-topbar-nav-arrow[hidden] { display: none; }
    .settings-topbar-nav-arrow:hover,
    .settings-topbar-nav-arrow:focus-visible {
        color: var(--bs-link-color);
        background: var(--bs-tertiary-bg);
        outline: none;
    }
}
/* Use Bootstrap's CSS vars so the card swaps surface + border colors when
   data-bs-theme="dark" is set on <html>. Same rule works in light + dark. */
.page-card {
    background: var(--bs-body-bg);
    color: var(--bs-body-color);
    border: 1px solid var(--bs-border-color);
    border-radius: .375rem;
    padding: 1rem 1.25rem;
    margin-bottom: 1rem;
    /* House style: most pages read better with a constrained width.
       Wide pages (lists, dashboards, two-pane layouts) opt out by
       adding .page-card-wide. The Reports dashboard uses .reports-page
       as a wrapper and its nested .page-card cards inherit that
       wrapper's full-bleed rule below.
       Pages that set their own inline max-width (e.g. Apps/Edit at 720,
       Users/Edit at 820) keep that tighter constraint since inline
       styles win over class rules. */
    max-width: 850px;
    margin-left: auto;
    margin-right: auto;
}

/* Opt-out: list pages (Users/Index, Apps/Index, Companies/Index,
   CannedResponses/Index) need the full width to host wide tables. */
.page-card.page-card-wide {
    max-width: none;
    margin-left: 0;
    margin-right: 0;
}

/* Reports dashboard renders many .page-card cards (filters, KPIs,
   charts, tables) inside .reports-page. They all want full width. */
.reports-page .page-card {
    max-width: none;
    margin-left: 0;
    margin-right: 0;
}

/* Every Settings shell page (AppChatSettings, CannedResponses, Business
   Hours, Departments, Chat Fields) fills the full width of the right
   pane -- the sidebar already carves off the horizontal space, so the
   850px narrow constraint reads as a stranded card with too much white
   space on the right. Inside .settings-main, expand to full bleed. */
.settings-main .page-card {
    max-width: none;
    margin-left: 0;
    margin-right: 0;
}

/* ============================================================================
 * Error page (Pages/Error.cshtml)
 *
 * Used for 400 / 403 / 404 / 405 / 429 / 500 via UseStatusCodePagesWithReExecute
 * and UseExceptionHandler. Layout:
 *   - Big muted status code at the top so the user can immediately see WHAT
 *     went wrong (404 / 500 / etc.) without reading the body copy
 *   - Friendly title + one-paragraph message in the page's normal text style
 *   - Optional trace-ID block, only rendered on 500, with a Copy button so a
 *     user on the phone with support can read the ID off cleanly
 *   - "Home" button -- /Index does the right thing for both anonymous (sends
 *     to /Account/Login) and signed-in (sends to /Chats) users
 *
 * Centered with a max-width so the page reads well on every viewport without
 * needing its own responsive rules. Reuses .page-card for the surface so it
 * inherits theme colors and matches the rest of the app.
 * ============================================================================ */
.error-card {
    max-width: 560px;
    margin: 48px auto;
}
.error-status {
    font-size: 4rem;
    font-weight: 700;
    line-height: 1;
    color: var(--bs-secondary-color);
    margin: 0 0 .5rem 0;
    /* tabular-nums keeps 404 vs 500 visually the same width if a designer
       ever swaps in a variable-width font */
    font-variant-numeric: tabular-nums;
}
.error-trace {
    margin-top: 1.25rem;
    padding: .75rem 1rem;
    background: var(--bs-tertiary-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: .375rem;
}
.error-trace code {
    font-size: .85rem;
    word-break: break-all;
    /* Keep the ID readable in dark mode -- Bootstrap's default code
       color comes from a teal that disappears against the tertiary bg. */
    color: var(--bs-body-color);
    background: transparent;
    padding: 0;
}
.error-actions {
    display: flex;
    gap: .5rem;
}

/* ============================================================================
 * Fitted-page layout
 *
 * For editor pages where the title at the top + the Save bar at the bottom
 * should always be visible, with the form content scrolling in between.
 * Triggered by adding .is-fitted to the .page-card; body:has() then flips the
 * entire layout chain (body → main → settings-shell → settings-main →
 * page-card → form) to viewport-bounded flex column. Pages without
 * .is-fitted continue to behave normally.
 *
 * The chain:
 *   body         (100vh, flex column)
 *     nav, company-bar  (auto)
 *     main              (flex 1, flex column)
 *       (optional .settings-shell → .settings-main, both flex column)
 *       (optional .row > .col- on pages like Profile)
 *         page-card.is-fitted  (flex column, fills available height)
 *           h2                   (auto, pinned at top)
 *           form                 (flex 1, flex column — only when it has a
 *                                 .page-card-body child)
 *             .page-card-body    (flex 1, overflow-y: auto — scrolls)
 *             .page-card-actions (auto, pinned at the bottom)
 * ============================================================================ */

body:has(.page-card.is-fitted) {
    /* Two `height` declarations on purpose. iOS Chrome (and Safari) report
       `100vh` as the viewport height INCLUDING the area behind the bottom
       browser toolbar, so the body's bottom edge -- and any flex-pinned
       child sitting there, like the global _Footer -- ends up below the
       visible area, invisible and unreachable. `100dvh` (dynamic viewport
       height, iOS 15.4+ / all evergreen browsers) tracks the *visible*
       viewport and shrinks/grows with the toolbar, so the footer renders
       where it belongs. We keep the `100vh` line as a fallback for the
       handful of older browsers that don't grok dvh -- they get the
       previous (slightly iOS-buggy) behaviour rather than no height at
       all. Same pattern below for .reports-page / .dev-shell. */
    height: 100vh;
    height: 100dvh;
    margin: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    padding-bottom: 0;
}
body:has(.page-card.is-fitted) > nav,
body:has(.page-card.is-fitted) > .company-bar {
    flex: 0 0 auto;
}
body:has(.page-card.is-fitted) > main {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}
/* Settings pages route through .settings-shell (grid) → .settings-main. The
   grid is a flex item under main, so flex:1 makes the grid CONTAINER fill
   main's height — but the grid ROW inside is still content-sized by default
   (auto), and .settings-shell already sets align-items:start so the items
   sit at the top of that auto row. To get a real height for settings-main
   to flex against:
     1) grid-template-rows:minmax(0,1fr) forces the row to fill the grid's
        height (instead of being auto-sized to content). minmax(0,1fr) — not
        just 1fr — so the row is allowed to shrink past content and the
        page-card-body's overflow:auto actually engages.
     2) align-self:stretch on .settings-main overrides the inherited
        align-items:start so settings-main fills the row vertically.
     .settings-side keeps the default align-items:start so the nav stays at
     its natural height at the top, not stretched. */
body:has(.page-card.is-fitted) .settings-shell {
    flex: 1;
    min-height: 0;
    grid-template-rows: minmax(0, 1fr);
    overflow: hidden;
}
body:has(.page-card.is-fitted) .settings-main {
    align-self: stretch;
    display: flex;
    flex-direction: column;
    min-height: 0;
    overflow: hidden;
}
/* Narrow-viewport variant of the fitted settings layout. The topbar replaces
   the sidebar, so the grid switches to one column with two rows: an auto-
   height topbar and a minmax(0,1fr) main pane that absorbs the rest of the
   viewport and scrolls internally. Without this override the desktop rule
   above would force a single 1fr row and the topbar would either share
   a row with main or get clipped by overflow:hidden. */
@media (max-width: 899.98px) {
    body:has(.page-card.is-fitted) .settings-shell {
        grid-template-columns: minmax(0, 1fr);
        grid-template-rows: auto minmax(0, 1fr);
    }
}
/* Sticky-positioning correction for fitted pages.
   .settings-side uses `position: sticky; top: 1rem;` so on long scrolling
   pages (non-fitted mode) the left nav follows the user down. In fitted
   mode the body/main/.settings-shell chain is overflow:hidden, which by
   the sticky spec qualifies as a "scrolling mechanism" -- the nearest
   such ancestor becomes the sticky containing block. The element's
   distance from that container's top is 0px, so `top: 1rem` immediately
   shifts the nav down ~16px relative to the content card on the right,
   which is what produced the ~10px misalignment on App/Chat Settings,
   Business Hours, and Chat Fields (Canned Responses is unaffected
   because it doesn't use .is-fitted and the body scroll keeps the
   nav at its natural position until the user actually scrolls). Zeroing
   the top offset on fitted pages lines the nav up with the content card
   without disrupting non-fitted sticky behavior. */
body:has(.page-card.is-fitted) .settings-side {
    top: 0;
}
/* Profile centers its card via .row.justify-content-center > .col-md-6 —
   propagate flex through both so the card can claim the viewport height.
   Bootstrap's .row has flex-wrap:wrap by default; with a single col child
   the resulting flex LINE sizes to content (not the row's stretched
   height) so the col can't actually reach to the bottom even with
   align-items:stretch. flex-wrap:nowrap forces a single non-wrapping line
   so align-items + align-self:stretch on the col really do anchor it to
   the row's full height. overflow:hidden along the chain mirrors the
   settings-shell path so a tall page-card-body scrolls internally instead
   of pushing the actions row below the viewport. */
body:has(.page-card.is-fitted) > main > .row {
    flex: 1;
    min-height: 0;
    margin-left: 0;
    margin-right: 0;
    flex-wrap: nowrap;
    align-items: stretch;
    align-content: stretch;
    overflow: hidden;
}
body:has(.page-card.is-fitted) > main > .row > [class*="col-"] {
    display: flex;
    flex-direction: column;
    min-height: 0;
    align-self: stretch;
    overflow: hidden;
}

/* The fitted card itself. margin-bottom: 0 because adding 1rem at the bottom
   would push the card below the viewport and reintroduce main-level scrolling. */
.page-card.is-fitted {
    flex: 1;
    min-width: 0;            /* allow shrink past content width so inner rows can't push the card past viewport */
    min-height: 0;
    width: 100%;             /* fill parent (main) -- prevents min-content sizing from making the card narrower */
    max-width: 100%;         /* but never wider than parent, even if inline style declared a larger value */
    display: flex;
    flex-direction: column;
    margin-bottom: 0;
}
/* Final firewall: clip any horizontal overflow that escapes the
   is-fitted chain at the main element, so the document body never
   scrolls horizontally. Children that genuinely need horizontal
   scroll (like data tables) should opt in via their own overflow:auto
   wrapper. */
body:has(.page-card.is-fitted) > main {
    overflow-x: hidden;
    min-width: 0;
}
/* Title stays at the top of the card. */
.page-card.is-fitted > h2 {
    flex: 0 0 auto;
}
/* When the form is the body/actions carrier — i.e. it has a .page-card-body
   as a direct child — turn it into the flex column. The :has check skips
   any sibling forms that exist purely to host a hidden input (e.g. the
   rotate-secret form on /Apps/Edit). */
.page-card.is-fitted > form:has(> .page-card-body) {
    flex: 1 1 auto;
    min-width: 0;            /* mirrors the card's own shrink behavior */
    min-height: 0;
    display: flex;
    flex-direction: column;
}
/* The scrollable middle. min-height: 0 is the magic incantation that lets
   a flex item with overflow:auto actually shrink past its content's intrinsic
   height — otherwise overflow never engages and the card grows instead.
   overflow-x: hidden suppresses the horizontal scrollbar that Bootstrap's
   .row (margin:0 -12px) otherwise causes when the vertical scrollbar takes
   up its few pixels of width — the .row's negative margin is invisible
   anyway (cols pad it back in) so clipping it is safe. */
.page-card.is-fitted .page-card-body {
    flex: 1 1 auto;
    overflow-y: auto;
    overflow-x: hidden;
    min-width: 0;            /* lets the body shrink past inner content widths so children don't push it wide */
    min-height: 0;
}
/* Bootstrap .row defaults to flex-wrap:wrap but also min-width:auto,
   which means a row full of .col-auto fixed-width inputs refuses to
   shrink below the SUM of those inputs' widths -- pushing the parent
   body past the viewport at narrow widths. Forcing min-width:0 on
   rows inside the fitted body lets the row shrink to its allocated
   space; flex-wrap:wrap then wraps the col-auto items to multiple
   lines naturally. */
.page-card.is-fitted .page-card-body .row {
    min-width: 0;
}
/* Override the sticky positioning from the non-fitted .page-card-actions
   rule below — in fitted mode the bar is anchored by flex, not sticky scroll. */
.page-card.is-fitted .page-card-actions {
    position: static;
    flex: 0 0 auto;
}

/* ============================================================================
 * Sticky-action variant (used when .page-card isn't .is-fitted)
 *
 * On non-fitted pages the action bar uses position:sticky so the Save row
 * still follows the user down a long form. This is the fallback for any
 * page that uses .page-card-actions without opting into the full fitted
 * layout. The .is-fitted rule above overrides position back to static so
 * flex can take over.
 * ============================================================================ */
.page-card-actions {
    position: sticky;
    bottom: 0;
    background: var(--bs-body-bg);
    padding-top: .75rem;
    padding-bottom: .75rem;
    margin-top: 1rem;
    border-top: 1px solid var(--bs-border-color);
    z-index: 10;
    display: flex;
    gap: .75rem;
    align-items: center;
    flex-wrap: wrap;
}
.status-Initiated  { background: #fff3cd; color: #856404; }
.status-InProgress { background: #cfe2ff; color: #084298; }
.status-Completed  { background: #d1e7dd; color: #0f5132; }
.status-Canceled   { background: #f8d7da; color: #842029; }

/* ---------- Global site footer ---------------------------------------
   Small, centered, low-emphasis row at the bottom of every page. Houses
   Help / Developer / Contact us / copyright. Designed to be inconspicuous:
   only show up if the user goes looking for it, and never compete with
   the page content for attention.

   Theming: leans entirely on Bootstrap's --bs-* variables so light/dark
   switch comes free. Top border separates the footer from the page card
   above without needing a solid background fill. */
.site-footer {
    /* Tightened to roughly half the previous footprint: less top margin, less
       vertical padding. The links + copy line still sit on two rows but with
       just enough breathing room to look intentional, not crammed. */
    margin-top: 1.25rem;
    padding: .55rem 0 .65rem;
    border-top: 1px solid var(--bs-border-color);
    font-size: .8rem;
    color: var(--bs-secondary-color);
    text-align: center;
}

/* ============================================================================
 * Viewport-bound layout for long-content pages
 *
 * Same pattern as the .is-fitted editor pages, but scoped to /Reports,
 * /Help, and /Developer -- all pages where the Current Company picker
 * + footer should stay visible while the user scrolls long content. main
 * is the internal scroll container. The bleed-through that appeared past
 * a previously-sticky footer goes away because the body itself no longer
 * scrolls.
 *
 * dev-toc's sticky-top tweak lives in developer.css since it's specific
 * to the Help/Developer layout structure.
 * ============================================================================ */
body:has(.reports-page),
body:has(.dev-shell) {
    /* See the .page-card.is-fitted block above for why both `height`
       declarations are present -- short version, `100dvh` keeps the
       global footer on-screen under iOS Chrome's bottom toolbar; the
       `100vh` line is a fallback for browsers without dvh. */
    height: 100vh;
    height: 100dvh;
    margin: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

/* ============================================================================
 * Public-page pinned-footer chain. Landing (/Index) and Sign-in
 * (/Account/Login) both want the global _Footer pinned at the visible
 * bottom of the viewport at every page height, with the page content
 * scrolling above it. Same flex-column / 100dvh body shape as the
 * agent-side `.page-card.is-fitted` chain above, but scoped via
 * lighter wrapper classes (`.landing-page-fitted`, `.signin-page-fitted`)
 * because these pages don't have the page-card + sticky-action-row
 * structure -- they just want the footer pinned and the content above
 * to scroll naturally.
 *
 * History: we briefly stripped the landing-page rules (2026-06-09 PM)
 * because the original used `100vh` and rendered the footer over content
 * on iOS Chrome. Once the rest of the codebase moved to `100dvh` and
 * proved the fix worked, we restored both pages here under a shared
 * selector to keep the rule centralised.
 * ============================================================================ */
body:has(.landing-page-fitted),
body:has(.signin-page-fitted) {
    height: 100vh;
    height: 100dvh;
    margin: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    padding-bottom: 0;
}
body:has(.landing-page-fitted) > nav,
body:has(.landing-page-fitted) > .company-bar,
body:has(.signin-page-fitted)  > nav,
body:has(.signin-page-fitted)  > .company-bar {
    flex: 0 0 auto;
}
body:has(.landing-page-fitted) > main,
body:has(.signin-page-fitted)  > main {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    /* Cancel the .mt-4 spacer on main when the layout is viewport-bound,
       same reasoning as the .reports-page / .dev-shell block above:
       outer flex margins on the scrolling region show up as trapped
       pixels at the top of the scroll area. */
    margin-top: 0 !important;
}
body:has(.landing-page-fitted) .site-footer,
body:has(.signin-page-fitted)  .site-footer {
    margin-top: 0;
    flex-shrink: 0;
}
body:has(.reports-page) > nav,
body:has(.reports-page) > .company-bar,
body:has(.dev-shell) > nav,
body:has(.dev-shell) > .company-bar {
    flex: 0 0 auto;
}
body:has(.reports-page) > main,
body:has(.dev-shell) > main {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    /* Cancel the .mt-4 spacer on main when the layout is viewport-bound --
       outer flex margins on the scrolling region tend to manifest as a
       trapped few pixels at the top of the scroll area. Convert it to
       inner padding so the breathing room sits with the content. */
    margin-top: 0 !important;
    padding-top: 1rem;
}
/* Footer slips right below the (now flex-pinned) main with no top-margin
   visual gap, the same way the chats inbox handles it. */
body:has(.reports-page) .site-footer,
body:has(.dev-shell) .site-footer {
    margin-top: 0;
}

.site-footer-links {
    list-style: none;
    padding: 0;
    margin: 0 0 .2rem 0;
    display: flex;
    justify-content: center;
    align-items: center;
    flex-wrap: wrap;
    gap: 0;
}

/* Bullet separator between links, drawn via ::after so the wrapping
   behaviour is automatic -- if a link wraps to a new line, its dot wraps
   with it, and the trailing dot on the last visible item disappears
   because of :last-child below. */
.site-footer-links li {
    display: inline-flex;
    align-items: center;
}
.site-footer-links li + li::before {
    content: "\00B7";  /* middle dot */
    margin: 0 .55rem;
    color: var(--bs-tertiary-color);
}

.site-footer-links a {
    color: var(--bs-secondary-color);
    text-decoration: none;
}
.site-footer-links a:hover,
.site-footer-links a:focus {
    color: var(--bs-link-color);
    text-decoration: underline;
}
/* Active-page highlight in the footer. Same amber as the navbar's
   --bs-navbar-active-color so the "you're here" signal reads
   consistently top + bottom. Colour + weight change is the cue. */
.site-footer-links a.active,
.site-footer-links a.active:hover,
.site-footer-links a.active:focus {
    color: #fcd34d;
    font-weight: 600;
}

.site-footer-copy {
    color: var(--bs-tertiary-color);
    font-size: .75rem;
}

/* Narrow viewports (phone-width chat detail in particular):
   tighten the footer by ~15% across the board so it claws back ~10px
   of vertical real estate for the chat thread / composer. Breakpoint
   bumped from 480 -> 599.98 to line up with the other phone-focused
   rules (chat-detail.css mobile block, chats-inbox.css narrow rules,
   etc.). Dot separators get an extra tighten so all 2-3 links still
   fit on a single row even on the narrowest phones -- flex-wrap on
   .site-footer-links is the safety net if a future link ever pushes
   past available width. */
@media (max-width: 599.98px) {
    .site-footer {
        font-size: .68rem;           /* was .8rem; ~15% smaller */
        padding: .47rem 0 .55rem;    /* was .55rem 0 .65rem */
    }
    .site-footer-copy {
        font-size: .64rem;           /* was .75rem */
    }
    .site-footer-links {
        margin: 0 0 .17rem 0;        /* was .2rem */
    }
    .site-footer-links li + li::before {
        margin: 0 .4rem;             /* was .55rem -- tighter dot gap */
    }
}

.status-pill       { padding: 2px 8px; border-radius: 10px; font-size: 12px; font-weight: 600; }

/* ---------- Current Company sub-bar (Phase 4C) ---------- */
.company-bar {
    background: var(--bs-tertiary-bg);
    border-bottom: 1px solid var(--bs-border-color);
    padding: 14px 0;
    font-size: 13px;
}
.company-bar .company-bar-label { font-weight: 600; color: var(--bs-secondary-color); }
.company-bar .company-bar-name  { font-weight: 600; color: var(--bs-link-color); }
.company-bar .company-bar-select { width: auto; min-width: 220px; }

/* ----------------------------------------------------------------------------
 * Chat invitation banners
 * Stacked rows directly under the navbar (above .company-bar). One per open
 * invitation. Server-rendered on initial paint and kept in sync live by
 * notifications.js. Subtle-blue pill-style background, no shadow, so they
 * read as informational rather than alarming. The wrapper hides itself when
 * empty so it doesn't leave a stray gap.
 * --------------------------------------------------------------------------*/
.chat-invite-banners {
    display: flex;
    flex-direction: column;
    gap: 0;
}
.chat-invite-banner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
    padding: .5rem .9rem;
    background-color: var(--bs-info-bg-subtle);
    color: var(--bs-info-text-emphasis);
    border-bottom: 1px solid var(--bs-info-border-subtle);
    font-size: .9rem;
    flex-wrap: wrap;
}
.chat-invite-banner-text {
    flex: 1 1 auto;
    min-width: 0;
}
.chat-invite-customer {
    /* Long emails / company names get an ellipsis on tight viewports rather
       than pushing the action buttons off the row. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    display: inline-block;
    max-width: 100%;
    vertical-align: bottom;
}
.chat-invite-banner-actions {
    display: inline-flex;
    align-items: center;
    gap: .5rem;
    flex: 0 0 auto;
}
.chat-invite-open {
    font-weight: 600;
    text-decoration: underline;
    color: var(--bs-info-text-emphasis);
}
.chat-invite-open:hover { color: var(--bs-emphasis-color); }
.chat-invite-dismiss {
    background: transparent;
    border: 0;
    color: var(--bs-secondary-color);
    font-size: 1.25rem;
    line-height: 1;
    padding: 0 .25rem;
    cursor: pointer;
}
.chat-invite-dismiss:hover { color: var(--bs-emphasis-color); }

/* ----------------------------------------------------------------------------
 * Responsive admin list tables
 *
 * Used by /Companies, /Apps, /Users (and other admin grids). Three modes:
 *
 *   >= 1100px      : full table, all columns visible.
 *   700 - 1099px   : table view with .col-hide-md columns hidden, so the
 *                    remaining columns get enough horizontal room without
 *                    forcing horizontal scroll. Each grid picks which
 *                    columns to drop (Max Apps/Users on Companies, Public
 *                    Key on Apps, Departments + Apps on Users).
 *   < 700px        : stacked detail view -- thead hidden, each row becomes
 *                    a vertical card-style block with "Label: value" pairs
 *                    drawn from each cell's data-label attribute. A line
 *                    separator between rows (no zebra striping) keeps the
 *                    list scannable on narrow viewports.
 *
 * Cells without a data-label render as a plain block (used for the action
 * column / no-data colspan row).
 * --------------------------------------------------------------------------*/
@media (min-width: 700px) and (max-width: 1099.98px) {
    .list-table .col-hide-md { display: none; }
}

/* Canned Responses (and any list page with long-text columns):
   CSV keyword strings have no whitespace break opportunities, so the
   browser refuses to break them on its own. overflow-wrap:anywhere
   lets a break happen at any character when the column gets narrow.
   Applied at ALL widths now (not just the 700-1099 mid-range) so the
   Keywords column wraps as aggressively as Triggers at any viewport. */
.list-table td[data-label="Triggers"],
.list-table td[data-label="Keywords"] {
    overflow-wrap: anywhere;
    word-break: break-word;
    max-width: 18rem;
}

@media (max-width: 699.98px) {
    .list-table,
    .list-table thead,
    .list-table tbody,
    .list-table tr,
    .list-table td,
    .list-table th { display: block; width: 100%; }

    .list-table thead { display: none; }

    .list-table.table > :not(caption) > * > * {
        /* Drop the Bootstrap cell padding + border-bottom shadow so our own
           row-level border below is the only divider. */
        padding: 0;
        box-shadow: none;
        background: transparent;
    }
    .list-table.table-striped > tbody > tr {
        /* Cancel the alternating row tint in stacked mode -- the line
           separator carries the structure now and a striped background on
           full-width blocks reads as noisy. */
        background: transparent;
        --bs-table-bg-type: transparent;
    }

    .list-table tr {
        padding: .55rem .25rem;
        border-bottom: 1px solid var(--bs-border-color);
    }
    .list-table tr:last-child { border-bottom: 0; }

    .list-table td {
        padding: .1rem 0 !important;
        border: 0 !important;
    }

    .list-table td[data-label]::before {
        content: attr(data-label) ": ";
        font-weight: 600;
        color: var(--bs-secondary-color);
        margin-right: .35rem;
    }
    /* Action cell (no data-label) gets a little breathing room above so the
       Edit button doesn't crash into the last data row. */
    .list-table td:not([data-label]) {
        padding-top: .4rem !important;
    }
    /* Empty-state colspan row (also no data-label) sits centered. */
    .list-table tr > td:not([data-label]):only-child {
        text-align: center;
        color: var(--bs-secondary-color);
    }
}

/* ============================================================================
 * .mobile-collapse — narrow-viewport text clamp + More/Less toggle
 *
 * Markup written by the page author:
 *     <p class="text-muted mobile-collapse">Long descriptive text...</p>
 * mobile-collapse.js wraps the inner content in a .mobile-collapse-text span
 * at runtime and appends a .mobile-collapse-toggle button. The clamp +
 * toggle only engage at <=899px; above that the helper is invisible and
 * the text reads as-written.
 * ============================================================================ */
.mobile-collapse-toggle { display: none; }

@media (max-width: 899.98px) {
    /* CSS-driven clamping (NOT a JS character cap) so the truncation
       point adapts to actual viewport width. JS just wraps the original
       content in .mobile-collapse-text and decides when to show/hide
       the toggle based on whether the text actually overflows the
       single-line clamp.

       display:flex carries !important because the .mobile-collapse host
       often also has Bootstrap's .d-block utility (`display: block
       !important`); without !important on our rule, the host stays block,
       the inner text wraps normally, and the clamp never engages. */
    .mobile-collapse:not(.is-expanded) {
        display: flex !important;
        align-items: baseline;
        flex-wrap: nowrap;
        max-width: 100%;
    }
    .mobile-collapse.is-expanded {
        display: block !important;
    }
    .mobile-collapse:not(.is-expanded) .mobile-collapse-text {
        flex: 0 1 auto;
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    .mobile-collapse.is-expanded .mobile-collapse-text {
        display: inline;
        white-space: normal;
        overflow: visible;
        text-overflow: clip;
    }
    .mobile-collapse:not(.is-expanded) .mobile-collapse-toggle {
        flex: 0 0 auto;
        margin-left: .25rem;
    }
    .mobile-collapse-toggle {
        display: inline;
        background: none;
        border: 0;
        color: var(--bs-link-color);
        padding: 0;
        font: inherit;
        font-size: .8em;
        text-decoration: underline;
        cursor: pointer;
        /* Belt-and-suspenders: make sure the button captures clicks even
           if a transparent parent label is sitting on top of it. */
        position: relative;
        z-index: 1;
    }
    .mobile-collapse-toggle:hover,
    .mobile-collapse-toggle:focus-visible {
        color: var(--bs-link-hover-color);
        outline: none;
    }
    .mobile-collapse-toggle[hidden] { display: none; }

    /* ---- Test Chat preset buttons (Settings/TestChat.cshtml) ----
       At mobile width the test-case row would otherwise wrap the
       longer preset labels onto multiple lines and chew up vertical
       space. Cap width at 120px and ellipsise the label; the active
       preset expands back to fit its full text so the user can read
       what they're working with. */
    #tcPresets .btn[data-preset] {
        max-width: 85px;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        text-align: left;
    }
    #tcPresets .btn[data-preset].active {
        max-width: none;
        background: var(--bs-primary);
        border-color: var(--bs-primary);
        color: #fff;
    }
}

/* Selected preset highlight (applies at all widths). Tests #1-5 are
   stylistically siblings; .active marks the user's choice. */
#tcPresets .btn[data-preset].active {
    background: var(--bs-primary);
    border-color: var(--bs-primary);
    color: #fff;
}
#tcPresets .btn[data-preset].active:hover {
    color: #fff;
}

/* ============================================================================
 * WEC.confirm modal (wec-utils.js -- replaces window.confirm() on the agent
 * side). Themed via Bootstrap CSS variables so dark + light surfaces both
 * read cleanly. Fixed-position backdrop covers the viewport; the dialog
 * centers itself.
 * ============================================================================ */
.wec-confirm-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, .55);
    z-index: 1080;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
}
.wec-confirm-dialog {
    background: var(--bs-body-bg);
    color: var(--bs-body-color);
    border: 1px solid var(--bs-border-color);
    border-radius: .5rem;
    box-shadow: 0 12px 30px rgba(0, 0, 0, .35);
    max-width: 420px;
    width: 100%;
    padding: 1rem 1.25rem;
}
.wec-confirm-title {
    font-weight: 600;
    margin-bottom: .35rem;
}
.wec-confirm-message p {
    margin: 0 0 .5rem 0;
}
.wec-confirm-message p:last-child {
    margin-bottom: 0;
}
.wec-confirm-actions {
    margin-top: 1rem;
    display: flex;
    justify-content: flex-end;
    gap: .5rem;
}

/* ----- Connection banner (floating chip) --------------------------------
   Used by wec-connection.js's auto-created fallback element when a JS
   module attaches a banner option without a primary in-page element.
   Class name (.inbox-conn) is a legacy artifact -- originally rendered
   only on the Chats page by chats-inbox.js. Moved here so notifications.js
   (layout-level) can render the same chip on every signed-in page when the
   server connection drops. Hidden by default (no .show); shows yellow while
   reconnecting, red when fully disconnected.
*/
.inbox-conn {
    position: fixed;
    /* Sit ABOVE the global site-footer, not inside its vertical band.
       The footer is ~55-65px tall (`.55rem 0 .65rem` padding + ~32px
       of two-line content + 1.25rem top margin). Pinning the banner at
       bottom:24px overlapped the footer's lower half and visually made
       the footer look taller (reported 2026-06-14). 72px clears the
       footer on every viewport the global footer ships at, with a
       small breathing-room gap. The inline-style override in
       wec-connection.js places it at bottom:2px/left:4px; that wins
       because inline styles beat external CSS. This rule is the
       fallback if JS hasn't run yet. */
    bottom: 72px;
    left: 24px;
    background: #fff9e3;
    color: #8a6d1f;
    padding: 8px 12px;
    border-radius: 4px;
    font-size: 12px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, .15);
    z-index: 2147483645;
    display: none;
}
.inbox-conn.show { display: block; }
.inbox-conn.off  { background: #fdecea; color: #a02020; }
.inbox-conn .dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: #caa54a;
    margin-right: 6px;
    vertical-align: middle;
    animation: inbox-conn-pulse 1.4s ease-in-out infinite;
}
.inbox-conn.off .dot { background: #a02020; animation: none; }
.inbox-conn button.conn-retry {
    background: transparent;
    border: none;
    color: inherit;
    text-decoration: underline;
    cursor: pointer;
    padding: 0 4px;
    font: inherit;
}
@keyframes inbox-conn-pulse {
    0%, 100% { opacity: .4; }
    50%      { opacity: 1; }
}