/* ============================================================
   Messenger
   ============================================================ */

.msgr-root {
    display: flex;
    border: 1px solid var(--color-border);
    border-radius: 12px;
    overflow: hidden;
    min-height: 500px;
    max-height: 70vh;
    background: var(--color-bg);
}

/* ── Sidebar ── */

.msgr-sidebar {
    width: 300px;
    min-width: 300px;
    display: flex;
    flex-direction: column;
    border-right: 1px solid var(--color-border);
    background: var(--color-surface);
    overflow: hidden;
    min-height: 0;
}
.msgr-sidebar-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px 14px;
    border-bottom: 1px solid var(--color-border);
}
.msgr-sidebar-header h2 { margin: 0; font-size: var(--font-size-heading); }
.msgr-icon-btn {
    width: 32px;
    height: 32px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 8px;
    background: var(--color-primary);
    color: #fff;
    border: none;
    cursor: pointer;
    font-size: 15px;
    flex-shrink: 0;
    transition: opacity 0.15s;
}
.msgr-icon-btn:hover { opacity: 0.85; }
.msgr-search-bar {
    padding: 8px 10px;
    border-bottom: 1px solid var(--color-border);
    position: relative;
}
.msgr-search-clear {
    /* Hit box is 44x44 (touch-target minimum) even though the visible glyph stays
       small; a ::after pseudo-element paints a ~30px hover/active circle centered
       inside the box so the click target does not read as visually oversized.
       right:3px keeps the glyph's visual center where the old 18px button sat.
       Ported from canopy (canopy commit 3c4bc9ce). */
    position: absolute;
    right: 3px;
    top: 50%;
    transform: translateY(-50%);
    width: 44px;
    height: 44px;
    background: none;
    border: none;
    padding: 0;
    margin: 0;
    color: var(--color-text-light);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
}
.msgr-search-clear::after {
    content: '';
    position: absolute;
    width: 30px;
    height: 30px;
    border-radius: 50%;
    background: transparent;
    transition: background 0.15s;
}
.msgr-search-clear i {
    position: relative;
    font-size: 15px;
    line-height: 1;
}
.msgr-search-clear:hover,
.msgr-search-clear:active {
    color: var(--color-text);
}
.msgr-search-clear:hover::after,
.msgr-search-clear:active::after {
    background: var(--color-border);
}
.msgr-search-input {
    width: 100%;
    /* padding-right cleared to 46px (from 32px) so typed text stays clear of the
       new 44px search-clear hit box, whose inner edge sits 46px from the bar's
       right padding edge (right:3px + width:44px). */
    padding: 7px 46px 7px 10px;
    border: 1px solid var(--color-border);
    border-radius: 8px;
    background: var(--color-bg);
    color: var(--color-text);
    font-size: var(--font-size-body);
    box-sizing: border-box;
}
.msgr-scrollable-area {
    flex: 1;
    overflow-y: auto;
    /* Pure VERTICAL scroller, matching .msgr-messages. Without an explicit
       overflow-x, `overflow-y: auto` resolves overflow-x to `auto` too (CSS spec),
       making this a 2-D scroller — so iOS runs its scroll-vs-pan classifier on a
       horizontal swipe-back and fires touchcancel mid-gesture, eating the FIRST
       swipe (the "needs two swipes" bug). overflow-x: hidden makes it vertical-only
       so horizontal swipes pass straight to the conv-list swipe-back handler, the
       same as the conversation scroller where swipe-back already works first-try.
       (The child .msgr-source-nav-strip keeps its own horizontal scroll — clipping
       the parent's x-overflow doesn't disable a descendant's internal scroll.) */
    overflow-x: hidden;
    display: flex;
    flex-direction: column;
}
.msgr-conv-list {
    flex: 1;
}
/* While a search is active, this list and #messengerSearchResults are BOTH visible,
   and both used to carry `flex: 1` (basis 0), so they split the panel 50/50 no matter
   what they held. A search whose conversation-name section matched nothing therefore
   still reserved half the panel for a lone "Conversations" label, leaving a tall blank
   gap above the "Messages" section for as long as the async message search was in
   flight. Size to CONTENT here instead so the Messages section sits directly beneath,
   and let the results panel take the remaining space. The cap keeps a search with many
   name matches from pushing the Messages section off screen; it scrolls internally
   past that, and degrades to content height if the percentage cannot resolve.
   Ported from canopy. */
.msgr-conv-list.msgr-conv-list-searching {
    flex: 0 0 auto;
    max-height: 50%;
    overflow-y: auto;
}
.msgr-conv-list .empty-state-text {
    padding: 20px 16px;
}

/* Inline error state for a failed/timed-out first-load conversation list
   (_msgrRenderConvLoadError, js/admin-messenger.js). Replaces the skeleton so a
   stalled switchboard-api call never leaves the user staring at shimmer forever. */
.msgr-conv-load-error {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 10px;
    padding: 20px 16px;
}
.msgr-conv-load-error .empty-state-text {
    padding: 0;
}

/* Conversation-list skeleton - shown only on the genuine first-ever load (every
   later open paints instantly from the durable cache). Mirrors .msgr-conv-item
   geometry so the real list swaps in without a layout jump. The per-row phase
   offset on the shimmer makes the wait feel like a living surface, not a freeze. */
.msgr-conv-skeleton-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 14px;
    border-bottom: 1px solid var(--color-border);
}
.msgr-conv-skeleton-row .skeleton-circle { width: 40px; height: 40px; flex-shrink: 0; }
.msgr-conv-skeleton-lines {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 9px;
}
.msgr-conv-skeleton-lines .line-1 { width: 42%; }
.msgr-conv-skeleton-lines .line-2 { width: 78%; }
.msgr-conv-skeleton-row:nth-child(2) .skeleton { animation-delay: 0.08s; }
.msgr-conv-skeleton-row:nth-child(3) .skeleton { animation-delay: 0.16s; }
.msgr-conv-skeleton-row:nth-child(4) .skeleton { animation-delay: 0.24s; }
.msgr-conv-skeleton-row:nth-child(5) .skeleton { animation-delay: 0.32s; }
.msgr-conv-skeleton-row:nth-child(6) .skeleton { animation-delay: 0.40s; }

/* Message-thread skeleton - replaces the old "Loading messages..." text on the
   IDB/network load path (memory hits paint synchronously and never show it).
   Alternating in/out shimmer bubbles mirror .msgr-msg-bubble geometry (14px
   radius, 4px sender-side bottom corner) so the real thread swaps in without a
   layout jump; the per-bubble phase offset is set inline by
   _msgrThreadSkeletonHtml (admin-messenger-render.js), matching the conv-list
   skeleton's living-surface pattern above. */
.msgr-thread-skeleton {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 12px 14px;
}
.msgr-msg-skeleton { max-width: 72%; border-radius: 14px; }
.msgr-msg-skeleton-in  { align-self: flex-start; border-bottom-left-radius: 4px; }
.msgr-msg-skeleton-out { align-self: flex-end; border-bottom-right-radius: 4px; }

/* Load-older-history row: pinned at the very top of the thread while a scroll-back
   page is in flight (Telegram/WhatsApp-style infinite scroll-up). Inserted/removed
   directly on the live DOM by _msgrShowOlderLoader/_msgrHideOlderLoader
   (admin-messenger-render.js), never part of the _msgrRenderMessages() innerHTML
   rebuild, so it can never be left stranded by a render mid-fetch. Spinner styling
   mirrors .msgr-net-spinner (thread-header connection indicator) and reuses the
   same global @keyframes spin (style-01.css). Ported from canopy. */
.msgr-load-older-row {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 10px 0;
}
.msgr-load-older-spinner {
    display: inline-block;
    width: 18px; height: 18px;
    border: 2px solid var(--color-border);
    border-top-color: var(--color-primary);
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
}

/* Jump-to-search-result landed the user mid-history rather than at the live bottom.
   Scroll-up-for-more-history keeps working from there, but there is no "load newer"
   pagination anywhere in this codebase, so a banner is the only way back to the
   present without leaving/reopening the conversation. Pinned at the very top of the
   thread, same insertion pattern as .msgr-load-older-row (_msgrShowJumpBanner,
   admin-messenger.js), never part of the _msgrRenderMessages() innerHTML rebuild.
   Ported from canopy. */
.msgr-jump-banner {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 8px 14px;
    font-size: var(--font-size-small);
    color: var(--color-text-light);
    background: var(--color-surface-hover, rgba(0,0,0,0.03));
    border-bottom: 1px solid var(--color-border);
}
.msgr-jump-banner button {
    border: none;
    background: none;
    color: var(--color-primary);
    font-size: var(--font-size-small);
    font-weight: 600;
    cursor: pointer;
    padding: 0;
}
.msgr-jump-banner button:hover { text-decoration: underline; }

.msgr-retention-notice {
    padding: 10px 14px;
    font-size: var(--font-size-small);
    color: var(--color-text-light);
    text-align: center;
    border-top: 1px solid var(--color-border);
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 4px;
}

/* Conversation items */
.msgr-conv-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 14px;
    cursor: pointer;
    transition: background 0.15s, transform 100ms var(--ease-out);
    border-bottom: 1px solid var(--color-border);
    /* Bypass iOS WebKit's tap-delay heuristics that fire :hover on the first
       tap and withhold the click until the second (the "two taps required
       after swipe-back" bug — L162 epilogue, v2.92). On touch devices iOS
       treats a div with :hover as a hover-discovery target after an
       inconclusive prior gesture (a swipe whose touchend got classified as
       a drag, suppressing the click), and the next conv-item tap shows the
       :hover background without firing the click. `manipulation` short-
       circuits the heuristic and dispatches click on the first touchend. */
    touch-action: manipulation;
    /* The row is a tap/long-press gesture target, never selectable text. Without
       this, a long-press starts the native text-selection gesture, which grabs
       the word under the finger (the notif popup's title appears there). */
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
}
/* Hover background only on devices with true hover capability — touch
   devices (iOS, Android) would otherwise have the :hover background "stick"
   after a tap, indistinguishable from .active, masking whether the click
   actually fired. The exact misdiagnosis the user reported as "highlight
   moves but thread doesn't open." */
@media (hover: hover) {
    .msgr-conv-item:hover { background: var(--color-surface-hover, rgba(0,0,0,0.03)); }
}
.msgr-conv-item:active { transform: scale(0.99); }
.msgr-conv-item.active { background: var(--color-surface-hover, rgba(0,0,0,0.04)); }

.msgr-conv-avatar {
    width: 40px;
    height: 40px;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
}
.msgr-avatar-img {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    object-fit: cover;
}
.msgr-avatar-initials, .msgr-avatar-group {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 600;
    font-size: 14px; /* fixed: 40×40px circle - font-size must not scale with preset */
    color: #fff;
    background: var(--color-primary);
}
.msgr-avatar-group {
    background: var(--color-text-light);
    font-size: 13px; /* fixed: same circle */
}
.msgr-avatar-plant {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 20px;
    flex-shrink: 0;
}

/* ── WhatsApp bridge UI ─────────────——───────────────────────────────────────── */

/* Avatar container for WA conversations - wraps initials + badge */
.msgr-conv-avatar { position: relative; flex-shrink: 0; }

/* WA avatar uses primary theme color */
.msgr-avatar-wa { background: var(--color-primary) !important; color: #fff !important; }

/* Point-of-Contact initial chip pinned to bottom-right of the avatar circle.
   Visually matches .hr-cal-initial-chip (Leave Calendar): same color SoT
   (resolveUserColor in shared.js), same first-initial pattern. Positioned
   as an avatar overlay with a surface-colored halo to lift it off the photo. */
.msgr-poc-chip {
    position: absolute;
    bottom: -3px;
    right: -4px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    border-radius: 4px;
    background: var(--color-primary);
    color: #fff;
    font-size: 10px;
    font-weight: 700;
    line-height: 1;
    border: 2px solid var(--color-surface);
    pointer-events: none;
}

/* ── Source Nav (vertical column desktop / scroll strip mobile) ── */
.msgr-source-nav {
    display: flex;
    flex-direction: column;
    width: 72px;
    min-width: 72px;
    border-right: 1px solid var(--color-border);
    background: var(--color-surface);
    padding: 8px 0;
    gap: 2px;
    overflow-y: auto;
    overflow-x: hidden;
    flex-shrink: 0;
}
.msgr-source-nav-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    width: 100%;
    padding: 10px 6px;
    border: none;
    background: transparent;
    color: var(--color-text-light);
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
    position: relative;
    border-radius: 0;
    user-select: none;
    -webkit-user-select: none;
    -webkit-touch-callout: none;
}
.msgr-source-nav-item::before {
    content: '';
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-50%);
    width: 3px;
    height: 0;
    background: var(--item-color, var(--color-primary));
    border-radius: 0 2px 2px 0;
    transition: height 0.2s;
}
.msgr-source-nav-item.is-active::before { height: 28px; }
.msgr-source-nav-item.is-active {
    color: var(--color-text);
    background: var(--color-surface-hover);
}
.msgr-source-nav-item:hover:not(.is-active) { background: var(--color-surface-hover); }
.msgr-source-nav-icon { font-size: 20px; line-height: 1; }
.msgr-source-nav-label {
    font-size: 10px;
    font-weight: 600;
    text-align: center;
    line-height: 1.2;
    word-break: break-word;
}
.msgr-source-nav-strip { display: none; }

/* ── Folder tab reorder mode (Telegram-style drag) ── */
/* Chips wiggle + show a grab cursor while the rail is in reorder mode. Pointer
   drag handles the actual move (see _msgrWireReorderDrag), so taps must not
   scroll or select: touch-action none + no text selection. */
.msgr-source-reorder {
    cursor: grab;
    touch-action: none;
    animation: msgrTabWiggle 0.32s ease-in-out infinite alternate;
}
.msgr-source-reorder:active { cursor: grabbing; }
/* Stagger so the whole rail doesn't wiggle in lockstep (reads more alive). */
.msgr-source-reorder:nth-child(even) { animation-delay: 0.16s; }
/* The chip currently under the pointer: lifted, still, elevated above siblings. */
.msgr-source-dragging {
    animation: none;
    cursor: grabbing;
    transform: scale(1.06);
    opacity: 0.92;
    z-index: 2;
    box-shadow: 0 6px 16px rgba(0, 0, 0, 0.18);
    background: var(--color-surface-hover);
    border-radius: 10px;
}
/* The pinned 'unread' chip: dimmed, no wiggle, not draggable. */
.msgr-reorder-locked {
    animation: none;
    cursor: default;
    opacity: 0.45;
}
/* Done pill terminates the rail in reorder mode. Vertical-rail variant here;
   the horizontal strip override lives in the mobile media query. */
.msgr-reorder-done {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    margin: 6px 8px 2px;
    padding: 8px 4px;
    border: none;
    border-radius: 10px;
    background: var(--color-primary);
    color: #fff;
    font-size: 11px;
    font-weight: 700;
    cursor: pointer;
    flex-shrink: 0;
}
.msgr-reorder-done i { font-size: 15px; }
.msgr-reorder-done:hover { filter: brightness(1.06); }
@keyframes msgrTabWiggle {
    from { transform: rotate(-2deg); }
    to   { transform: rotate(2deg); }
}


/* Thread header subtitle */
.msgr-thread-subtitle { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }

/* WA external sender label in message bubbles */
.msgr-msg-sender-wa {
    color: #25d366;
    font-weight: 600;
}

/* Bridge status indicator in settings */
.msgr-wa-status { font-size: var(--font-size-label); font-weight: 500; }
.msgr-wa-status-online  { color: var(--color-whatsapp); }
.msgr-wa-status-idle    { color: var(--color-text-light); }
.msgr-wa-status-unknown { color: var(--color-text-light); font-style: italic; }

/* Manage Communication — repeated section card wrapper */
.comm-section-card {
    background: var(--color-bg);
    border-radius: 10px;
    border: 1px solid var(--color-border);
    padding: 20px;
    margin-bottom: 12px;
}

/* Role permissions grid */
.msgr-access-grid { width: 100%; border-collapse: collapse; table-layout: fixed; }
.msgr-access-grid thead th {
    padding: 0 0 10px 0;
    text-align: center;
    font-size: var(--font-size-small);
    font-weight: 600;
    color: var(--color-text-light);
    text-transform: uppercase;
    letter-spacing: 0.05em;
}
.msgr-access-grid thead th:first-child { text-align: left; width: 46%; }
.msgr-access-grid tbody td {
    padding: 10px 0;
    text-align: center;
    border-top: 1px solid var(--color-border-subtle, rgba(0,0,0,0.06));
    vertical-align: middle;
}
.msgr-access-grid tbody td:first-child { text-align: left; }
.msgr-grid-feature { display: flex; flex-direction: column; gap: 2px; padding-right: 12px; }
.msgr-grid-label { font-size: var(--font-size-body); font-weight: 500; color: var(--color-text); }
.msgr-grid-desc { font-size: var(--font-size-small); color: var(--color-text-light); }
.msgr-grid-cb { width: 18px; height: 18px; accent-color: var(--color-accent); cursor: pointer; }
.color-whatsapp { color: var(--color-whatsapp); }

/* ── end WhatsApp bridge UI ──────────────────——───────────────────────────────── */

.msgr-conv-info { flex: 1; min-width: 0; }
.msgr-conv-header-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 6px;
}
.msgr-conv-name {
    font-weight: 600;
    font-size: var(--font-size-body);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.msgr-dm-name-link:hover {
    color: var(--color-primary);
    text-decoration: underline;
    cursor: pointer;
}
.msgr-conv-time {
    font-size: var(--font-size-small);
    color: var(--color-text-light);
    flex-shrink: 0;
}
.msgr-conv-preview-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 6px;
    margin-top: 2px;
}
.msgr-conv-preview {
    font-size: var(--font-size-label);
    color: var(--color-text-light);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    flex: 1;
}
/* Telegram-style "Draft:" preview — the red label flags an unsent message in the
   conversation list; the draft body keeps the normal muted preview colour. */
.msgr-draft-label { color: var(--color-danger, #e74c3c); font-weight: 600; }
.msgr-unread-badge {
    background: var(--color-danger, #e74c3c);
    color: #fff;
    font-size: var(--font-size-small);
    font-weight: 700;
    min-width: 18px;
    height: 18px;
    border-radius: 9px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1px 5px 0; /* +1px top notches numeral down 1px optically */
    flex-shrink: 0;
}

/* Pinned-conversation thumbtack (Telegram-style). Pins are conveyed by the
   float-to-top position plus this small tilted glyph, ALWAYS shown when pinned so
   the state is unmistakable; it sits just left of any unread / @ / needs-reply
   badge (which keep the far-right edge). The row itself is intentionally NOT tinted.
   See pinGlyph in _msgrRenderConversationList. */
.msgr-conv-pin-icon {
    display: inline-flex;
    align-items: center;
    color: var(--color-text-light);
    font-size: 12px;
    line-height: 1;
    flex-shrink: 0;
    opacity: 0.8;
}
.msgr-conv-pin-icon i { display: block; transform: rotate(45deg); }

/* FLIP reorder slide (pin / unpin). _msgrAnimateConvReorder captures each row's old
   position, lets the list re-render, then sets a per-row translateY offset inline and
   clears it under THIS transition so the row glides to its new slot instead of jumping
   (the pinned row rises to the top of its folder; displaced rows ease down). The gentle
   spring gives a subtle settle as each row arrives. transform-only (GPU-friendly); the
   class is added only for the glide and removed on transitionend. Higher specificity
   than the base .msgr-conv-item transition so it wins while present. */
.msgr-conv-item.msgr-conv-flip {
    transition: transform var(--dur-transition) var(--ease-spring);
    will-change: transform;
}

/* ── NOTIFICATIONS (core tab) ─────────────────────────────────
   Header bell + the unseen-count badge it shares with the default
   bottom-nav slot. Both .notif-unread-badge instances are painted by the
   single painter paintNotifBadge() in js/notifications.js. The bottom-nav
   Messenger slot's .msgr-nav-unread-badge (painted by msgrApplyDotState in
   js/shared.js) reuses this same red-circle base for a consistent count - it is
   deliberately a DISTINCT class from the messenger's per-conversation
   .msgr-unread-badge (above), which is in-flow inside each conversation row. */
.header-bell-btn {
    background: none;
    border: none;
    /* The app-header sits on the page background (not a primary-filled bar),
       so the bell uses --color-primary to match the logo and stay visible in
       both themes; --color-on-primary (white) would vanish on the light header. */
    color: var(--color-primary);
    font-size: 20px;
    line-height: 1;
    position: relative;
    padding: 4px 6px;
    cursor: pointer;
    flex-shrink: 0;
}
.notif-unread-badge,
.msgr-nav-unread-badge {
    position: absolute;
    top: -5px;
    right: -8px;
    background: var(--color-danger, #e74c3c);
    color: #fff;
    font-size: var(--font-size-small);
    font-weight: 700;
    min-width: 18px;
    height: 18px;
    border-radius: 9px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1px 5px 0;
    pointer-events: none;
}
/* Bottom-nav slot badge: top-right of the slot glyph. Mirrors the
   .bottom-nav-pending offsets, adjusted for the 18px badge height. */
.bottom-nav-item .bottom-nav-notif-badge {
    top: 2px;
    right: calc(50% - 22px);
}
/* Header bell: pull the count onto the glyph's top-right corner so it overlaps the
   bell like the bottom-nav badges do, instead of floating in the gap the base -8px
   offset leaves (reporter: "no overlap"). The button's 4px/6px padding insets the
   glyph from the padding box, so a smaller offset seats the badge on the corner. */
.header-bell-btn .notif-unread-badge {
    top: -4px;
    right: -3px;
}

/* Feed header row + Mark all as read */
.notif-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 8px;
}
.notif-title { margin: 0; }
.notif-mark-all {
    background: none;
    border: none;
    color: var(--color-primary);
    font-size: var(--font-size-small);
    font-weight: 600;
    cursor: pointer;
    padding: 4px 6px;
    flex-shrink: 0;
}

/* Filter pills (horizontal scroll row; hidden when only one module present) */
#notifPills {
    display: flex;
    flex-wrap: nowrap;
    gap: 8px;
    overflow-x: auto;
    padding-bottom: 8px;
    margin-bottom: 4px;
    -webkit-overflow-scrolling: touch;
}
#notifPills:empty { display: none; }
.notif-pill {
    flex: 0 0 auto;
    background: var(--color-bg-subtle);
    color: var(--color-text-light);
    border: 1px solid var(--color-border);
    border-radius: 14px;
    padding: 5px 12px;
    font-size: var(--font-size-small);
    cursor: pointer;
    white-space: nowrap;
}
.notif-pill.active {
    background: var(--color-primary);
    color: var(--color-on-primary);
    border-color: var(--color-primary);
}

/* Time-bucket header (Today / This week / Earlier) */
.notif-bucket-header {
    font-size: var(--font-size-small);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--color-text-light);
    margin: 14px 0 6px;
}

/* Feed row */
.notif-row {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    width: 100%;
    text-align: left;
    background: none;
    border: none;
    border-radius: 10px;
    padding: 10px;
    cursor: pointer;
}
.notif-row:hover { background: var(--color-bg-subtle); }
.notif-row.unread {
    background: color-mix(in srgb, var(--color-primary) 8%, transparent);
}
.notif-row-icon {
    position: relative;
    flex: 0 0 auto;
    width: 34px;
    height: 34px;
    border-radius: 50%;
    background: var(--color-bg-subtle);
    color: var(--color-primary);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 15px;
}
/* Leading unread dot on the row's icon. */
.notif-row.unread .notif-row-icon::after {
    content: '';
    position: absolute;
    top: -2px;
    right: -2px;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: var(--color-danger, #e74c3c);
    border: 2px solid var(--color-card);
}
.notif-row-main {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.notif-row-title {
    font-weight: 600;
    color: var(--color-text);
    font-size: var(--font-size-body);
}
.notif-row-body {
    font-size: var(--font-size-small);
    color: var(--color-text-light);
    overflow: hidden;
    text-overflow: ellipsis;
}
.notif-row-time {
    flex: 0 0 auto;
    font-size: var(--font-size-small);
    color: var(--color-text-light);
    white-space: nowrap;
    padding-top: 2px;
}
.notif-load-more { display: block; margin: 14px auto 4px; }
.notif-load-hint { text-align: center; padding: 16px 0; }

.msgr-source-icon-wrap {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.msgr-source-badge {
    position: absolute;
    top: -5px;
    right: -7px;
    background: var(--color-danger, #e74c3c);
    color: #fff;
    font-size: 10px;
    font-weight: 700;
    min-width: 16px;
    height: 16px;
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 2px 4px 0; /* +2px top notches numeral down 1px optically */
    line-height: 1;
    pointer-events: none;
}
/* Muted tab's own unread badge: grey, never the red attention colour - an
   accumulating count to check when convenient, not a signal demanding
   attention. See _msgrRenderSourceNav (js/admin-messenger.js). */
.msgr-source-badge-muted {
    background: var(--color-text-muted, #888);
}
/* Persistent "needs reply" indicator - POC has read but not replied yet.
   Solid red dot (no count, no pulse) so it reads quieter than a fresh-unread badge. */
.msgr-needs-reply-dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--color-danger, #e74c3c);
    flex-shrink: 0;
    display: inline-block;
}
/* CRM nudge dot on a conversation row: the shared .msgr-pending-dot base (see
   css/style-05.css), sized to match the CRM board's .crm-card-nudge-dot (7px)
   so the same "needs handling" signal reads identically on both surfaces. Kept
   in-flow inside .msgr-conv-preview-row, alongside the other per-conversation
   badges; unlike the board (a whole grid of cards), a single conversation row
   can keep the base class's pulse without becoming visual noise. */
.msgr-conv-nudge-dot {
    width: 7px;
    height: 7px;
}
.msgr-source-needs-reply-dot {
    position: absolute;
    top: -2px;
    right: -4px;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: var(--color-danger, #e74c3c);
    border: 2px solid var(--color-bg, #fff);
    pointer-events: none;
}

/* ── Thread ── */

.msgr-thread {
    flex: 1;
    display: flex;
    flex-direction: column;
    min-width: 0;
    background: var(--color-surface);
    position: relative;
}
/* Wraps the scrolling message list (+ empty state) so the floating
   scroll-to-latest button can anchor to the BOTTOM of the scroll region, which
   is the top of the typing/compose chrome. flex:1 + min-height:0 lets the inner
   .msgr-messages scroll, and the wrap shrinks as the compose grows, so the
   button rides just above a tall multi-line draft instead of overlapping it. */
.msgr-thread-scroll-wrap {
    flex: 1;
    display: flex;
    flex-direction: column;
    min-width: 0;
    min-height: 0;
    position: relative; /* anchor for the floating scroll-to-latest button */
}
/* Scroll-to-latest button: floats just above the compose bar, appears when the
   user has scrolled up from the bottom of the thread (.show toggled in JS).
   Anchored to .msgr-thread-scroll-wrap's bottom (a small fixed gap), so it
   tracks the top of the compose no matter how tall the compose grows. */
.msgr-scroll-btn {
    position: absolute;
    bottom: 16px;
    left: 50%;
    transform: translateX(-50%) translateY(8px);
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    box-shadow: 0 2px 10px rgba(0,0,0,0.18);
    color: var(--color-text);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 18px;
    cursor: pointer;
    opacity: 0;
    pointer-events: none;
    transition: opacity 180ms var(--ease-out), transform 180ms var(--ease-out);
    z-index: 5;
}
.msgr-scroll-btn.show {
    opacity: 1;
    pointer-events: auto;
    transform: translateX(-50%) translateY(0);
}
.msgr-scroll-btn:hover { color: var(--color-primary); }
/* Keep the .fi base's inline-flex centering — don't override display, or the glyph
   falls back to text-baseline and rides high in the circle. */
.msgr-scroll-btn .fi { font-size: 20px; line-height: 1; position: relative; top: 2px; }
.msgr-thread-header {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 16px;
    border-bottom: 1px solid var(--color-border);
    background: var(--color-surface);
}
.msgr-back-btn {
    display: none;
    background: none;
    border: none;
    font-size: var(--font-size-heading);
    cursor: pointer;
    color: var(--color-text);
    padding: 4px 8px;
    transition: transform 100ms var(--ease-out), color 0.15s;
}
.msgr-back-btn:active { transform: scale(0.88); }
.msgr-thread-header-info { cursor: pointer; flex: 1; min-width: 0; }
.msgr-thread-title { font-weight: 600; font-size: var(--font-size-heading); display: block; }
.msgr-thread-subtitle { font-size: var(--font-size-small); color: var(--color-text-light); }
/* Connection / staleness indicator: overrides the normal subtitle text while a degraded
   net state is showing. Subtle (same secondary tone, italic) so it never shouts; the
   subtitle is already display:flex; gap:6px so the spinner + text lay out inline. */
.msgr-net-status { font-style: italic; }
.msgr-net-spinner {
    display: inline-block; flex: none; width: 11px; height: 11px;
    border: 2px solid var(--color-text-light); border-top-color: transparent;
    border-radius: 50%; animation: spin 0.8s linear infinite;
}

/* Thread header avatar */
.msgr-thread-avatar {
    position: relative;
    width: 36px;
    height: 36px;
    border-radius: 50%;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 14px;
    font-weight: 600;
    color: var(--color-primary);
    flex-shrink: 0;
    cursor: pointer;
}
.msgr-thread-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
.msgr-thread-avatar .msgr-avatar-initials { width: 100%; height: 100%; }

/* Group photo section in member modal */
.msgr-group-photo-section {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    padding: 12px 0 16px;
    margin-bottom: 20px;
}
.msgr-group-photo-wrap {
    width: 80px;
    height: 80px;
    border-radius: 50%;
    overflow: hidden;
    background: var(--color-surface);
    border: 2px solid var(--color-border);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 28px;
    font-weight: 700;
    color: var(--color-primary);
    position: relative;
}
.msgr-group-photo-wrap img { width: 100%; height: 100%; object-fit: cover; }
.msgr-group-photo-container {
    position: relative;
    display: inline-flex;
}
.msgr-group-photo-badge {
    position: absolute;
    bottom: 0;
    right: 0;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    background: var(--color-primary);
    border: 2px solid var(--color-card);
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    font-size: 12px;
    pointer-events: none;
}
/* Centered modal title for group info */
.msgr-member-modal-title {
    text-align: center;
    margin: 8px 0 0;
    font-size: var(--font-size-h2);
}
.msgr-member-count {
    text-align: center;
    font-size: var(--font-size-small);
    color: var(--color-text-light);
    margin-top: 4px;
}

/* Conversation ID block in the chat info modal - two-row card */
.msgr-conv-id-row {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    margin: 10px auto 0;
    padding: 8px 12px;
    background: var(--color-surface-alt);
    border: 1px solid var(--color-border);
    border-radius: 8px;
    max-width: 100%;
    width: 100%;
    box-sizing: border-box;
    text-align: center;
}
.msgr-conv-id-label {
    color: var(--color-text-light);
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    font-size: 0.7rem;
}
.msgr-conv-id-value-row {
    display: flex;
    align-items: center;
    gap: 4px;
    width: 100%;
}
.msgr-conv-id {
    font-family: 'Courier New', Courier, monospace;
    color: var(--color-text);
    background: transparent;
    padding: 0;
    user-select: all;
    word-break: break-all;
    font-size: var(--font-size-small);
    flex: 1;
}
.msgr-conv-id-copy {
    background: transparent;
    border: none;
    cursor: pointer;
    padding: 2px 4px;
    color: var(--color-text-light);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
    border-radius: 4px;
    flex-shrink: 0;
    transition: color 0.15s, background 0.15s;
}
.msgr-conv-id-copy:hover { color: var(--color-primary); background: var(--color-card); }
.msgr-conv-id-copy--ok   { color: var(--color-success); }
/* Group name row - title + edit affordance */
.msgr-group-name-row {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0;
    margin: 0;
}
.msgr-group-name-text {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.msgr-group-name-edit-btn {
    flex-shrink: 0;
    width: 44px;
    height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: none;
    cursor: pointer;
    color: var(--color-text-light);
    border-radius: 8px;
    font-size: 15px;
    transition: color 0.15s, background 0.15s;
}
.msgr-group-name-edit-btn:hover { color: var(--color-primary); background: rgba(232, 105, 30,0.08); }
/* Group name edit - full-width input + below actions */
.msgr-group-name-edit-wrap { width: 100%; }
.msgr-group-name-input-row {
    position: relative;
    display: flex;
    align-items: center;
    margin-bottom: 10px;
}
.msgr-group-name-input {
    width: 100%;
    padding: 8px 44px 8px 12px;
    border: 1.5px solid var(--color-primary);
    border-radius: 8px;
    font-size: var(--font-size-heading);
    font-weight: 600;
    outline: none;
    box-shadow: 0 0 0 3px rgba(232, 105, 30,0.12);
    background: var(--color-card);
    color: var(--color-text);
    box-sizing: border-box;
}
.msgr-group-name-counter {
    position: absolute;
    right: 10px;
    top: 0;
    bottom: 0;
    display: flex;
    align-items: center;
    font-size: 11px;
    font-weight: 500;
    color: var(--color-text-light);
    pointer-events: none;
    white-space: nowrap;
}
.msgr-group-name-counter.near-limit { color: #b07d00; }
.msgr-group-name-counter.at-limit { color: var(--color-error); }
.msgr-group-name-actions {
    display: flex;
    gap: 8px;
}
.msgr-group-name-actions .btn {
    flex: 1;
    justify-content: center;
}
[data-theme="dark"] .msgr-group-name-input { background: #253030; }
@keyframes msgr-spin { to { transform: rotate(360deg); } }
.msgr-spin { display: inline-block; animation: msgr-spin 0.8s linear infinite; }

.msgr-thread-empty {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
}
.msgr-messages .empty-state-text {
    font-size: var(--font-size-small);
}
.msgr-new-choice-btn {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    padding: 12px 14px;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: 10px;
    cursor: pointer;
    text-align: left;
    transition: background 0.15s, border-color 0.15s;
}
@media (hover: hover) {
    .msgr-new-choice-btn:hover {
        background: color-mix(in srgb, var(--color-primary) 8%, var(--color-bg));
        border-color: var(--color-primary);
    }
}
.msgr-new-choice-btn .fi {
    font-size: 1.3rem;
    color: var(--color-primary);
    flex-shrink: 0;
}
.msgr-new-choice-label {
    font-weight: 600;
    font-size: var(--font-size-body);
    color: var(--color-text);
}
.msgr-new-choice-sub {
    font-size: var(--font-size-small);
    color: var(--color-text-light);
    margin-top: 2px;
}

.msgr-messages {
    flex: 1;
    overflow-y: auto;
    overflow-x: hidden;
    padding: 16px;
    display: flex;
    flex-direction: column;
    gap: 6px;
}

/* ── Message Bubbles ── */

.msgr-msg {
    max-width: 72%;
    display: flex;
    flex-direction: column;
    position: relative;
}
.msgr-msg-out { align-self: flex-end; }
.msgr-msg-in { align-self: flex-start; }

.msgr-msg-sender {
    font-size: var(--font-size-small);
    font-weight: 600;
    color: var(--color-primary);
    margin-bottom: 2px;
    padding-left: 4px;
}
.msgr-wa-sender {
    /* color set via inline style */
}
.msgr-msg-bubble {
    padding: 8px 12px;
    border-radius: 14px;
    font-size: var(--font-size-msgr);
    line-height: 1.4;
    /* flow-root so the floated timestamp (Telegram-style trailing time) is
       contained inside the bubble even when there are no reactions below it. */
    display: flow-root;
}
/* The message text itself. white-space:pre-wrap lives here, NOT on the bubble:
   the bubble now also holds the sender name, timestamp and reactions, and the
   template's inter-element newlines would otherwise render as blank lines. */
.msgr-msg-text {
    white-space: pre-wrap;
    word-break: break-word;
}
@media (max-width: 640px) {
    .msgr-msg-bubble { user-select: none; -webkit-user-select: none; }
}
.msgr-msg-out .msgr-msg-bubble {
    background: var(--color-primary);
    color: #fff;
    border-bottom-right-radius: 4px;
}
.msgr-msg-in .msgr-msg-bubble {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-bottom-left-radius: 4px;
}
/* Telegram-style sender avatar gutter for incoming group/WhatsApp
   messages. The row reserves a 40px left gutter (margin-left); the
   32px avatar sits at the bottom-left of that gutter and is
   rendered ONLY on the last message of a sender's run. */
.msgr-msg-grouped { margin-left: 40px; }
.msgr-msg-run-cont { margin-top: -2px; }
.msgr-msg-avatar {
    position: absolute;
    left: -40px;
    bottom: 0;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 600;
    font-size: 12px; /* fixed: 32px circle, must not scale with preset */
    color: #fff;
    user-select: none;
    -webkit-user-select: none;
}
.msgr-msg-avatar img { width: 100%; height: 100%; object-fit: cover; }
/* ── Inline message translation (Chinese <-> English) ──
   Rendered by _msgrTranslationHtml (admin-messenger-translate.js) after
   .msgr-msg-text, INSIDE the bubble. "Option 1": a thin hairline rule, a small
   language glyph, muted text. The translation is shared (DB cache) but shown only
   to the viewer who requested it (per-viewer revealed-set). */
.msgr-translation {
    margin-top: 6px;
    padding-top: 6px;
    border-top: 0.5px solid var(--color-border);
    font-size: 0.9em;
    line-height: 1.4;
    color: var(--color-text-secondary);
    display: flex;
    gap: 5px;
    align-items: baseline;
}
.msgr-translation .msgr-tx-text { white-space: pre-wrap; word-break: break-word; }
.msgr-tx-glyph { color: var(--color-primary); font-size: 0.95em; flex-shrink: 0; transform: translateY(1px); }
.msgr-translation-loading { animation: msgrTxPulse 1.1s ease-in-out infinite; }
.msgr-translation-error a { color: var(--color-primary); cursor: pointer; }
@keyframes msgrTxPulse { 0%, 100% { opacity: .55; } 50% { opacity: 1; } }
/* Magical "conjure" reveal, played ONCE by _msgrRevealTranslation() the moment the
   final translation becomes visible. Identical for the freshly-generated and the
   DB-cached paths (both end at the same bubble re-render -> final text). The
   .msgr-translation--reveal class is added by JS only at that moment and removed on
   animationend; it is never baked into _msgrTranslationHtml, so ordinary re-renders
   (reactions, read receipts) never replay it. Global reduced-motion block
   (style-06.css) zeroes both animations to a single static frame. */
@keyframes msgrTxConjure {
    0%   { opacity: 0; transform: translateY(5px); filter: blur(5px); }
    60%  { opacity: 1; }
    100% { opacity: 1; transform: translateY(0); filter: blur(0); }
}
@keyframes msgrTxSparkle {
    0%   { opacity: 0; transform: scale(0.2) rotate(-35deg); }
    55%  { opacity: 1; transform: scale(1.3) rotate(8deg); }
    100% { opacity: 1; transform: scale(1) rotate(0); }
}
.msgr-translation--reveal .msgr-tx-text {
    animation: msgrTxConjure var(--dur-celebrate) var(--ease-out) both;
    will-change: opacity, transform, filter;
}
.msgr-translation--reveal .msgr-tx-glyph {
    transform-origin: center;
    animation: msgrTxSparkle var(--dur-celebrate) var(--ease-spring) both;
}
/* Outbound bubbles are primary-filled with white text; keep the translation
   legible there (muted white instead of the light-surface secondary colour). */
.msgr-msg-out .msgr-translation { border-top-color: rgba(255,255,255,.35); color: rgba(255,255,255,.85); }
.msgr-msg-out .msgr-tx-glyph { color: rgba(255,255,255,.9); }
.msgr-msg-out .msgr-translation-error a { color: #fff; text-decoration: underline; }
/* Staff replies are right-aligned (.msgr-msg-out) but white-bubbled, so undo the
   white-on-primary overrides above and fall back to the standard muted treatment. */
.msgr-msg-staff-reply .msgr-translation { border-top-color: var(--color-border); color: var(--color-text-secondary); }
.msgr-msg-staff-reply .msgr-tx-glyph { color: var(--color-primary); }
.msgr-msg-staff-reply .msgr-translation-error a { color: var(--color-primary); text-decoration: none; }
/* Staff replies in WA conversations: right-aligned but white bubble */
.msgr-msg-staff-reply .msgr-msg-bubble {
    background: var(--color-surface);
    color: var(--color-text);
    border: 1px solid var(--color-border);
    border-bottom-right-radius: 4px;
}
.msgr-msg-staff-reply .msgr-voice-player { background: var(--color-surface); border: 1px solid var(--color-border); }
.msgr-msg-staff-reply .msgr-voice-play-btn { background: var(--color-primary); color: #fff; }
.msgr-msg-staff-reply .msgr-wv-bar { background: var(--color-border); }
.msgr-msg-staff-reply .msgr-wv-bar.played { background: var(--color-primary); }
.msgr-msg-staff-reply .msgr-voice-dur { color: var(--color-text-light); }
/* Clickable URLs inside message text + captions */
.msgr-link { text-decoration: underline; }
.msgr-msg-out .msgr-link { color: #fff; }
.msgr-msg-in .msgr-link,
.msgr-msg-staff-reply .msgr-link { color: var(--color-primary); }
/* General auto-linked URL (announcement bodies, etc.) - the default class
   used by linkifyText() when no link class is passed. */
.inline-link { color: var(--color-primary); text-decoration: underline; word-break: break-word; }
.msgr-msg-deleted .msgr-msg-bubble {
    opacity: 0.6;
    font-style: italic;
}
/* Failed send — keep the message fully legible (the user must read it to decide to
   resend); a red outline on the bubble + the retry row below carry the status. */
.msgr-msg-failed { opacity: 1; }
.msgr-msg-failed .msgr-msg-bubble,
.msgr-msg-failed .msgr-bubble-photo {
    box-shadow: 0 0 0 1.5px var(--color-danger, #e74c3c);
}
/* "Not delivered · Tap to retry" affordance under a failed bubble. */
.msgr-msg-retry {
    display: flex;
    align-items: center;
    gap: 4px;
    margin-top: 3px;
    padding: 1px 4px;
    font-size: var(--font-size-small);
    color: var(--color-danger, #e74c3c);
    cursor: pointer;
    user-select: none;
    -webkit-user-select: none;
}
.msgr-msg-retry i { font-size: 11px; line-height: 1; }
.msgr-msg-retry:active { opacity: 0.6; }
.msgr-msg-out .msgr-msg-retry { align-self: flex-end; }
.msgr-msg-in  .msgr-msg-retry { align-self: flex-start; }

/* Conv-list "a message didn't send" alert — red pill with a ! (action required, so it
   pulses per the Red Pulse Notification Rule). */
.msgr-send-failed-badge {
    background: var(--color-danger, #e74c3c);
    color: #fff;
    font-size: var(--font-size-small);
    font-weight: 800;
    min-width: 18px;
    height: 18px;
    border-radius: 9px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 5px 1px;
    flex-shrink: 0;
    margin-left: 4px;
    animation: msgrFailedPulse 1.6s ease-in-out infinite;
}
@keyframes msgrFailedPulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(231, 76, 60, 0.5); }
    50%      { box-shadow: 0 0 0 4px rgba(231, 76, 60, 0); }
}

.msgr-msg-meta {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 2px 4px 0;
    font-size: var(--font-size-small);
    color: var(--color-text-light);
}
.msgr-msg-out .msgr-msg-meta { justify-content: flex-end; }
.msgr-msg-edited { font-style: italic; }

/* ── Telegram-style unified text bubble ──
   For text messages the sender name, message text, timestamp and reactions all
   live INSIDE .msgr-msg-bubble instead of as separate stacked rows. Media
   messages (.msgr-msg-media-msg) keep their own chrome and external meta row. */
.msgr-msg-bubble .msgr-msg-sender { padding-left: 0; margin-bottom: 2px; }
/* Bottom strip = reactions + timestamp. With NO reactions it floats into the last
   text line (trailing time, Telegram-style). Once a reaction is present it reflows
   into its own row: reactions on the LEFT, timestamp on the RIGHT. */
.msgr-msg-bubble .msgr-msg-bottom {
    float: right;
    margin-left: 10px;
    line-height: 1;
    transform: translateY(5px); /* drop onto the last text line's baseline */
}
.msgr-msg-bottom .msgr-msg-meta {
    float: none;
    display: inline-flex;
    gap: 3px;
    padding: 0;
    margin: 0;
}
.msgr-msg-bottom .msgr-reactions { margin-top: 0; }
.msgr-msg-bubble:has(.msgr-reactions:not(:empty)) .msgr-msg-bottom {
    float: none;
    clear: both;
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 6px 0 0;
    transform: none;
}
.msgr-msg-bubble:has(.msgr-reactions:not(:empty)) .msgr-msg-bottom .msgr-msg-meta {
    margin-left: auto; /* push the time to the right of the reactions row */
}

/* Keep sender, timestamp and ticks legible on the coloured outgoing fill now that
   they sit inside the bubble. Excludes staff-reply bubbles (right-aligned but white). */
.msgr-msg-out:not(.msgr-msg-staff-reply) .msgr-msg-bubble .msgr-msg-sender { color: rgba(255,255,255,0.92) !important; }
.msgr-msg-out:not(.msgr-msg-staff-reply) .msgr-msg-bubble .msgr-msg-meta,
.msgr-msg-out:not(.msgr-msg-staff-reply) .msgr-msg-bubble .msgr-msg-time,
.msgr-msg-out:not(.msgr-msg-staff-reply) .msgr-msg-bubble .msgr-tick-sent,
.msgr-msg-out:not(.msgr-msg-staff-reply) .msgr-msg-bubble .msgr-tick-delivered { color: rgba(255,255,255,0.8); }
.msgr-msg-out:not(.msgr-msg-staff-reply) .msgr-msg-bubble .msgr-tick-read { color: #fff; }
.msgr-msg-out:not(.msgr-msg-staff-reply) .msgr-msg-bubble .msgr-tick-failed { color: #ffd1cc; }
/* Reply reference inside an outgoing bubble: translucent-white so it reads on the fill. */
.msgr-msg-out:not(.msgr-msg-staff-reply) .msgr-msg-bubble .msgr-reply-ref {
    background: rgba(255,255,255,0.16);
    border-left-color: rgba(255,255,255,0.7);
}
.msgr-msg-out:not(.msgr-msg-staff-reply) .msgr-msg-bubble .msgr-reply-ref-text { color: rgba(255,255,255,0.82); }

/* Ticks */
.msgr-tick { font-size: var(--font-size-small); letter-spacing: -2px; }
.msgr-tick-sent { color: var(--color-text-light); }
.msgr-tick-delivered { color: var(--color-text-light); }
.msgr-tick-read { color: var(--color-msgr-tick-read); }
.msgr-tick-failed { color: var(--color-danger); letter-spacing: 0; font-weight: 700; }
.msgr-tick-pending { color: var(--color-text-light); letter-spacing: 0; }


/* Reply reference */
.msgr-reply-ref {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 4px 8px 4px 10px;
    margin-bottom: 2px;
    border-left: 3px solid var(--color-primary);
    border-radius: 4px;
    background: rgba(0,0,0,0.04);
    cursor: pointer;
    font-size: var(--font-size-small);
}
.msgr-reply-ref-text-col {
    flex: 1;
    min-width: 0; /* allow ellipsis inside flex */
    overflow: hidden;
}
.msgr-reply-ref-name { font-weight: 600; display: block; font-size: var(--font-size-small); }
.msgr-reply-ref-text { color: var(--color-text-light); display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.msgr-reply-ref-thumb {
    width: 36px;
    height: 36px;
    object-fit: cover;
    border-radius: 6px;
    flex-shrink: 0;
}

/* Pending message - animated bouncing dots */
@keyframes msgrDotBounce {
    0%, 60%, 100% { transform: translateY(0); opacity: 0.35; }
    30%            { transform: translateY(-3px); opacity: 1; }
}
.msgr-tick-pending .msgr-dot {
    display: inline-block;
    width: 3px;
    height: 3px;
    border-radius: 50%;
    background: currentColor;
    animation: msgrDotBounce 1.1s ease-in-out infinite;
    margin: 0 1px;
    vertical-align: middle;
}
.msgr-tick-pending .msgr-dot:nth-child(2) { animation-delay: 0.18s; }
.msgr-tick-pending .msgr-dot:nth-child(3) { animation-delay: 0.36s; }

/* Media in bubbles */
.msgr-msg-media { margin-bottom: 4px; position: relative; overflow: hidden; }
.msgr-msg-img {
    max-width: 100%;
    max-height: 280px;
    border-radius: 10px;
    cursor: pointer;
    display: block;
    /* iOS WebKit fires its native image callout (Share / Save to Photos / Copy)
       on long-press of an <img>, intercepting our JS long-press timer before the
       emoji react menu can open. Suppress the callout so the bubble's long-press
       handler wins; short-tap still opens the lightbox via onclick. */
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
}
/* Video bubbles size to the video's TRUE aspect ratio so portrait and wide clips are
   never center-cropped (the old fixed 4/3 + object-fit:cover chopped everything that
   wasn't 4:3). The wrapper carries the ratio: --msgr-ar / --msgr-w are set inline from
   the stored media_meta dimensions (recipient's first paint), or by _msgrApplyVideoBox
   once metadata loads (old messages without stored dims). The video is absolutely
   positioned to fill the box. Until the ratio is known the default 4/3 box holds the
   loading skeleton. --msgr-w is pre-fitted so height stays within the 360px cap; that
   cap is just a safety net, and max-width:100% keeps it responsive on narrow screens.
   object-fit:contain guarantees no crop even if a poster's ratio differs by a rounding px. */
.msgr-msg-media:has(> .msgr-msg-video) {
    width: var(--msgr-w, 240px);
    max-width: 100%;
    aspect-ratio: var(--msgr-ar, 4 / 3);
    max-height: 440px;          /* mirrors _msgrVideoBoxWidth MAX_H so a portrait clip fills the column */
    border-radius: 10px;
    overflow: hidden;
}
.msgr-msg-video {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    border-radius: 10px;
    display: block;
    background: #1a1a1a;
    object-fit: contain;
    cursor: pointer;
}
/* Bubble video has no native controls (tapping opens the dedicated video
   lightbox, same convention as a photo tap opening the image lightbox) - this
   play badge is the only affordance that the poster is actually a video. */
.msgr-video-play-badge {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 46px;
    height: 46px;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.5);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 18px;
    pointer-events: none;
}
.msgr-video-play-badge i { margin-left: 2px; } /* optical centring of the play glyph */
/* ── Loading media placeholder ──
   PHOTOS: if the message carries an inline base64 LQIP (media_meta.jpeg_thumbnail,
   set on every HuoMa-sent photo), the box shows a BLURRED blow-up of it (Telegram-
   style blur-up) while the full image downloads, then _msgrPatchMediaSrc blooms the
   sharp photo in on top and crossfades the blur away (.msgr-media-lqip /
   .msgr-msg-lqip-layer below). When there's NO LQIP (some WhatsApp-inbound photos)
   it falls back to the black square + shimmer sheen, held until the full image loads.
   VIDEOS: keep their own calm sage-on-cream skeleton; this rule stops matching the
   moment a [poster] is set (_msgrAttachPosterCapture, or a bridge jpeg_thumbnail),
   so the animation halts cleanly with no JS. */
.msgr-msg-video:not([poster]) {
    background-color: #e8ecdb;
    background-image:
        linear-gradient(105deg, transparent 30%, rgba(255, 255, 255, 0.55) 50%, transparent 70%),
        linear-gradient(135deg, #eef1e1 0%, #e2e6d1 100%);
    background-size: 200% 100%, 100% 100%;
    background-repeat: no-repeat;
    background-position: 200% 0, 0 0;
    animation: msgrSkeletonDrift 2.4s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* Dark thread is near-black (--color-bg #111313). The old skeleton lifted the box to
   #1f2422 with a 135deg gradient whose top-left corner (#252b28) was brighter still, so
   the box perimeter read as a hard light "outline" around the loading clip. Flatten it to
   a single fill only a hair above the thread - the box stays just perceptible, but its
   edge no longer draws a bright line - and keep the diagonal sheen as the loading cue. */
[data-theme="dark"] .msgr-msg-video:not([poster]) {
    background-color: #171b1a;
    background-image:
        linear-gradient(105deg, transparent 30%, rgba(205, 222, 210, 0.08) 50%, transparent 70%);
}

/* Photo placeholder: a box reserved at photo size. CONDITIONAL loading state - the box
   carries only a barely-there tint up front; the visible loading cue (dark square + a
   shimmer sweep) is DELAYED ~260ms (the animation-delay below). A photo that resolves from
   cache within that window is swapped in before any loading state is seen, so fast/cached
   photos never flash a placeholder - only a genuine wait surfaces the shimmer. The box is
   replaced by the real image (scale-bloom) once it loads; see .msgr-img-bloom. */
.msgr-msg-img.msgr-img-pending {
    width: min(72vw, 240px);
    aspect-ratio: 1;
    height: auto;
    border-radius: 10px;
    object-fit: cover;
    background-color: rgba(120, 130, 124, 0.10);
    background-image: linear-gradient(105deg, transparent 30%, rgba(255, 255, 255, 0.13) 50%, transparent 70%);
    background-size: 200% 100%;
    background-repeat: no-repeat;
    background-position: 200% 0;
    animation:
        msgrPhotoShimmer 1.4s linear 260ms infinite,
        msgrImgArm 200ms var(--ease-out) 260ms forwards;
}
@keyframes msgrImgArm { to { background-color: #1a1a1a; } }
/* Dark chat background is near-black (--color-bg #111313), so the armed square lifts to a
   visible charcoal; light mode arms to a true dark square on the cream surface. */
[data-theme="dark"] .msgr-msg-img.msgr-img-pending {
    background-color: rgba(200, 215, 208, 0.06);
    background-image: linear-gradient(105deg, transparent 30%, rgba(255, 255, 255, 0.10) 50%, transparent 70%);
    animation:
        msgrPhotoShimmer 1.4s linear 260ms infinite,
        msgrImgArmDark 200ms var(--ease-out) 260ms forwards;
}
@keyframes msgrImgArmDark { to { background-color: #2b2f2f; } }

/* Photo entrance: a snappy SCALE BLOOM, added by _msgrPatchMediaSrc once the image has
   loaded. The picture grows from 90% to full while fading in, with a small spring
   overshoot - replacing the old ~1.2s "develop from black" reveal. The final size is set
   inline before this runs, so the bloom scales the photo at its true aspect (no black
   backdrop, no size morph). A per-photo animation-delay (set inline) staggers a burst so
   several photos bloom in sequence down the thread. */
@keyframes msgrPhotoBloom {
    0%   { opacity: 0; transform: scale(0.9); }
    65%  { opacity: 1; transform: scale(1.025); }
    100% { opacity: 1; transform: scale(1); }
}
.msgr-msg-img.msgr-img-bloom {
    transform-origin: center center;
    animation: msgrPhotoBloom 320ms var(--ease-out) both;
}

/* ── Blur-up LQIP placeholder ──
   When a pending photo has an inline base64 LQIP, render a blurred blow-up of it
   behind the (src-less) main img instead of the shimmer. Specificity (0,4,0) beats
   the dark-mode pending rule above so the shimmer is suppressed in both themes. The
   main img sits on top (z-index 1) and is transparent until its src resolves, so the
   blur shows through; _msgrPatchMediaSrc then blooms the sharp photo in and removes
   this layer. */
.msgr-msg-media.msgr-media-lqip .msgr-msg-img.msgr-img-pending {
    background: transparent;
    background-image: none;
    animation: none;
    position: relative;
    z-index: 1;
}
.msgr-msg-lqip-layer {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    transform: scale(1.08);   /* push the blur's soft edges past the clip so no fringe shows */
    filter: blur(12px);
    z-index: 0;
    pointer-events: none;
    transition: opacity 320ms ease-out;
}
.msgr-msg-lqip-layer.msgr-lqip-gone { opacity: 0; }

/* Indeterminate "downloading" spinner over the blur/shimmer placeholder. Supabase
   signed-URL <img> loads emit no progress events, so this is a motion-only signal
   that the photo is still arriving. Delayed ~400ms so a fast/cached resolve never
   flashes it. _msgrPatchMediaSrc removes it when the sharp image swaps in (shares
   the .msgr-lqip-gone fade). */
.msgr-img-loading-spinner {
    position: absolute;
    top: 50%; left: 50%;
    width: 30px; height: 30px;
    margin: -15px 0 0 -15px;
    border-radius: 50%;
    border: 2.5px solid rgba(255, 255, 255, 0.32);
    border-top-color: #fff;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.18), 0 1px 4px rgba(0, 0, 0, 0.28);
    opacity: 0;
    pointer-events: none;
    z-index: 2;
    animation:
        msgrImgSpinnerFadeIn 200ms ease-out 400ms forwards,
        msgrImgSpinnerSpin 0.9s linear 400ms infinite;
}
.msgr-img-loading-spinner.msgr-lqip-gone {
    animation: none;
    opacity: 0;
    transition: opacity 320ms ease-out;
}
@keyframes msgrImgSpinnerFadeIn { to { opacity: 1; } }
@keyframes msgrImgSpinnerSpin   { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
    .msgr-img-loading-spinner { animation: msgrImgSpinnerFadeIn 200ms ease-out 400ms forwards; }
}

/* Failed media resolve: the signing/proxy call came back empty, so the spinner would
   otherwise run forever. _msgrMarkMediaUnavailable swaps it for this tappable chip;
   tapping re-runs the resolve (_msgrRetryMediaPath). */
.msgr-media-failed .msgr-img-loading-spinner { display: none; }
.msgr-media-failed-overlay {
    position: absolute;
    inset: 0;
    z-index: 3;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    background: rgba(0, 0, 0, 0.45);
    color: #fff;
    font-size: 12.5px;
    font-weight: 600;
    text-align: center;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}
.msgr-media-failed-overlay i {
    display: inline-flex;
    font-size: 18px;
    line-height: 1;
    height: 18px;
    margin-bottom: 2px;
    opacity: 0.9;
}
.msgr-media-failed-overlay span {
    display: block;
    line-height: 1.3;
}
.msgr-media-failed-sub {
    font-size: 11px;
    font-weight: 500;
    opacity: 0.75;
}

/* ── Telegram-style photo album (mosaic) ──
   N photos sharing a media_group_id render as ONE tiled bubble. Each tile reuses the
   standard .msgr-msg-media (so the blur-up LQIP, the % upload overlay and the reveal
   all work unchanged), sized to fill its grid cell via object-fit:cover. Albums are
   2-4 photos (the send cap); a larger one shows 4 tiles with a "+N" overlay.
   The mosaic, caption text and bottom strip (reactions + time) all live INSIDE a
   single .msgr-msg-bubble.msgr-bubble-media (same shell as a captioned single photo)
   so the whole album reads as one chat bubble. */
.msgr-album-bubble { position: relative; }
.msgr-album-grid {
    display: grid;
    gap: 2px;
    width: min(72vw, 280px);
    border-radius: 14px;
    overflow: hidden;
    background: var(--color-card);   /* shows through the 2px tile seams */
}
/* Mosaic inside the media bubble: edge-to-edge breakout (mirrors .msgr-msg-media
   inside a captioned single photo) so the bubble fill never strips around the
   photos. Top-rounded when first child; bottom-flat so caption/meta sit below. */
.msgr-bubble-media > .msgr-album-grid {
    margin: 0 -12px 6px;
    width: auto;
    border-radius: 0;
}
.msgr-bubble-media > .msgr-album-grid:first-child {
    margin-top: -8px;
    border-radius: 14px 14px 0 0;
}
/* No-caption album: the bottom strip is tiny (just time, optional reactions). Keep
   the mosaic's bottom edge flush with the bubble fill underneath. */
.msgr-album-bubble.msgr-bubble-media { width: 300px; max-width: 100%; }
.msgr-album-n2 { grid-template-columns: 1fr 1fr; aspect-ratio: 2 / 1.18; }
.msgr-album-n3 {
    grid-template-columns: 1.5fr 1fr;
    grid-template-rows: 1fr 1fr;
    aspect-ratio: 1 / 0.82;
}
.msgr-album-n3 .msgr-album-tile:first-child { grid-row: 1 / span 2; }  /* big photo, left */
.msgr-album-n4 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; aspect-ratio: 1 / 1; }
.msgr-album-tile { position: relative; overflow: hidden; min-width: 0; min-height: 0; cursor: pointer; }
/* Tile media fills its cell — override the standalone-photo sizing AND the pending
   placeholder's fixed 240px square so a still-loading tile fills the cell too. */
.msgr-album-tile .msgr-msg-media { margin: 0; width: 100%; height: 100%; border-radius: 0; }
.msgr-album-tile .msgr-msg-img,
.msgr-album-tile .msgr-msg-img.msgr-img-pending {
    width: 100%;
    height: 100%;
    max-width: none;
    max-height: none;
    aspect-ratio: auto;
    border-radius: 0;
    object-fit: cover;
}
/* The per-tile scrim+ring already reads as "sending", so don't ALSO 55%-dim the tile
   (that double-darkens). Shrink the ring a touch to suit a smaller cell. */
.msgr-album-tile.msgr-msg-sending { opacity: 1; }
.msgr-album-tile .msgr-upload-ring { width: 38px; height: 38px; }
.msgr-album-tile .msgr-upload-pct { font-size: 11px; }
/* "+N" overlay on the last visible tile of a larger album. */
.msgr-album-more {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.45);
    color: #fff;
    font-size: 1.5rem;
    font-weight: 700;
    z-index: 2;
    pointer-events: none;
}
/* A failed tile: red inset ring + a compact icon-only retry overlay (the full
   "Not delivered · Tap to retry" row is too wide for a tile). */
.msgr-album-tile.msgr-msg-failed { box-shadow: inset 0 0 0 2px var(--color-danger, #e74c3c); }
.msgr-album-tile .msgr-msg-retry {
    position: absolute;
    inset: 0;
    margin: 0;
    flex-direction: column;
    justify-content: center;
    gap: 2px;
    background: rgba(0, 0, 0, 0.42);
    color: #fff;
    z-index: 3;
}
.msgr-album-tile .msgr-msg-retry span { display: none; }   /* icon only in a tight tile */
.msgr-album-tile .msgr-msg-retry i { font-size: 18px; }

/* ── Telegram-style media ──
   Bare photo/video (no caption, no reply) renders frameless: the picture goes
   edge-to-edge with the sender name ABOVE it (like a text message) and only the
   timestamp overlaid bottom-right.
   Captioned media, voice and documents sit inside the standard coloured bubble. */
.msgr-bubble-photo {
    position: relative;
    overflow: hidden;
    border-radius: 14px;
    line-height: 0;
    max-width: 100%;
}
.msgr-msg-out .msgr-bubble-photo { border-bottom-right-radius: 4px; }
.msgr-msg-in  .msgr-bubble-photo { border-bottom-left-radius: 4px; }
.msgr-bubble-photo .msgr-msg-media { margin: 0; border-radius: 0; }
.msgr-bubble-photo .msgr-msg-img,
.msgr-bubble-photo .msgr-msg-video { border-radius: 0; }
.msgr-bubble-photo .msgr-msg-img { max-height: 360px; }
/* Timestamp/ticks overlaid bottom-right on a dark chip */
.msgr-meta-overlay {
    position: absolute;
    bottom: 8px; right: 8px;
    z-index: 2;
    padding: 2px 7px;
    border-radius: 10px;
    background: rgba(0,0,0,0.45);
    color: #fff !important;
    line-height: 1.3;
}
.msgr-meta-overlay .msgr-msg-time { color: #fff; }
.msgr-meta-overlay .msgr-tick-sent,
.msgr-meta-overlay .msgr-tick-delivered { color: rgba(255,255,255,0.85); }
.msgr-meta-overlay .msgr-tick-read { color: #fff; }

/* Media inside the standard coloured bubble (captioned image/video, voice, document):
   strip the inner element's own chrome so it sits ON the bubble fill, not boxed inside. */
.msgr-bubble-media .msgr-voice-player,
.msgr-bubble-media .msgr-doc-bubble,
.msgr-bubble-media .msgr-doc-bubble:hover {
    background: transparent !important;
    border: none !important;
    padding: 0 !important;
}
/* Captioned image/video: the picture goes edge-to-edge (paddingless) - only the
   name above and caption/time below keep the bubble padding, exactly like Telegram.
   A fixed media width keeps the bubble from collapsing to the caption's width. */
.msgr-bubble-media:has(> .msgr-msg-media) { width: 300px; max-width: 100%; }
.msgr-bubble-media > .msgr-msg-media {
    margin: 0 -12px 6px;   /* break out of the bubble's side padding */
    width: auto;
    border-radius: 0;
}
.msgr-bubble-media > .msgr-msg-media:first-child {
    margin-top: -8px;       /* and the top padding, when the image is the first row */
    border-radius: 14px 14px 0 0;
}
/* A captioned video must break out edge-to-edge like a captioned image. The video
   sizing rule (.msgr-msg-media:has(> .msgr-msg-video)) carries max-width:100%, which
   clamps the width:auto breakout to the bubble's padding box (276px) - the -12px side
   margins then shift the box left but it can't grow, leaving a strip of bubble fill on
   the RIGHT. Lift the clamp here so width:auto fills the full 300px bubble, edge to edge. */
.msgr-bubble-media > .msgr-msg-media:has(> .msgr-msg-video) {
    max-width: none;
}
.msgr-bubble-media .msgr-msg-img {
    width: 100%;
    border-radius: 0;
    max-height: 360px;
    object-fit: cover;
    display: block;
}

/* Shimmer keyframe - the sheen layer glides across (the base gradient stays
   anchored), then holds off-screen for the tail of the cycle so the wait
   breathes (a calm pass-and-rest) instead of sweeping like a metronome.
   Used by the VIDEO placeholder (its load is brief, so one calm pass reads well). */
@keyframes msgrSkeletonDrift {
    0%   { background-position: 200% 0, 0 0; }
    65%  { background-position: -120% 0, 0 0; }
    100% { background-position: -120% 0, 0 0; }
}

/* Photo placeholder shimmer - a CONTINUOUS repeating sweep (no rest). A photo can sit
   loading for a while, so the black square keeps actively shimmering the whole time
   rather than passing once and pausing. */
@keyframes msgrPhotoShimmer {
    from { background-position: 200% 0; }
    to   { background-position: -200% 0; }
}

/* Honour OS-level reduce-motion preference - show the skeleton statically. */
@media (prefers-reduced-motion: reduce) {
    .msgr-msg-video:not([poster]),
    .msgr-msg-img.msgr-img-pending {
        animation: none;
    }
}
/* Custom voice bubble player (no native <audio> - eliminates iOS loading spinner) */
.msgr-voice-player {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 7px 12px;
    border-radius: 20px;
    cursor: pointer;
    min-width: 180px;
    max-width: 240px;
    margin-bottom: 4px;
    user-select: none;
}
.msgr-msg-in  .msgr-voice-player { background: var(--color-surface); border: 1px solid var(--color-border); }
.msgr-msg-out .msgr-voice-player { background: var(--color-primary); }
.msgr-voice-play-btn {
    width: 30px;
    height: 30px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}
.msgr-msg-in  .msgr-voice-play-btn { background: var(--color-primary); color: #fff; }
.msgr-msg-out .msgr-voice-play-btn { background: rgba(255,255,255,0.22); color: #fff; }
.msgr-voice-bar {
    flex: 1;
    height: 3px;
    border-radius: 2px;
    position: relative;
    overflow: hidden;
}
.msgr-msg-in  .msgr-voice-bar { background: var(--color-border); }
.msgr-msg-out .msgr-voice-bar { background: rgba(255,255,255,0.3); }
.msgr-voice-fill {
    position: absolute;
    left: 0; top: 0; bottom: 0;
    width: 0%;
    border-radius: 2px;
    transition: width 0.1s linear;
}
.msgr-msg-in  .msgr-voice-fill { background: var(--color-primary); }
.msgr-msg-out .msgr-voice-fill { background: rgba(255,255,255,0.9); }
.msgr-voice-dur {
    font-size: 11px;
    flex-shrink: 0;
    min-width: 34px;
    text-align: right;
}
.msgr-msg-in  .msgr-voice-dur { color: var(--color-text-light); }
.msgr-msg-out .msgr-voice-dur { color: rgba(255,255,255,0.85); }
.msgr-voice-player .vp-pause { display: none; }
.msgr-voice-player.playing .vp-play  { display: none; }
.msgr-voice-player.playing .vp-pause { display: block; }

/* ── Document attachment bubble ── */
.msgr-doc-bubble {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    border-radius: 10px;
    text-decoration: none;
    max-width: 260px;
    background: rgba(0,0,0,0.06);
    color: var(--color-text);
    transition: background 120ms;
}
.msgr-doc-bubble:visited { color: var(--color-text); }
.msgr-doc-bubble:hover { background: rgba(0,0,0,0.12); }
[data-theme="dark"] .msgr-doc-bubble { background: rgba(255,255,255,0.08); }
[data-theme="dark"] .msgr-doc-bubble:hover { background: rgba(255,255,255,0.14); }
.msgr-msg-out .msgr-doc-bubble,
.msgr-msg-out .msgr-doc-bubble:visited { background: rgba(255,255,255,0.18); color: #fff; }
.msgr-msg-out .msgr-doc-bubble:hover { background: rgba(255,255,255,0.28); }
.msgr-msg-out .msgr-doc-bubble svg { color: rgba(255,255,255,0.9); }
.msgr-doc-bubble svg { flex-shrink: 0; color: var(--color-primary); }
.msgr-doc-name {
    font-size: 0.8rem;
    font-weight: 500;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: inherit;
}

/* WhatsApp poll (Switchboard 1.37.0): read-only snapshot card, question + options.
   Same neutral surface treatment as .msgr-doc-bubble. Voters/total_voters are ALWAYS
   null for a WA poll (votes are E2E-encrypted), so most polls render with no bars. */
.msgr-poll {
    min-width: 220px;
    max-width: 280px;
    padding: 4px 2px;
}
.msgr-poll-kind {
    font-size: 0.7rem;
    font-weight: 600;
    opacity: 0.65;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    margin-bottom: 2px;
}
.msgr-poll-question {
    font-size: 0.85rem;
    font-weight: 600;
    margin-bottom: 8px;
    white-space: pre-wrap;
    word-break: break-word;
}
.msgr-poll-opt { margin-bottom: 7px; }
.msgr-poll-opt-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
    font-size: 0.8rem;
}
.msgr-poll-opt-text { word-break: break-word; }
.msgr-poll-votes { font-size: 0.72rem; opacity: 0.65; flex-shrink: 0; }
.msgr-poll-bar {
    height: 4px;
    border-radius: 2px;
    margin-top: 3px;
    background: rgba(0,0,0,0.08);
    overflow: hidden;
}
[data-theme="dark"] .msgr-poll-bar { background: rgba(255,255,255,0.12); }
.msgr-poll-bar-fill {
    height: 100%;
    border-radius: 2px;
    background: var(--color-primary);
    transition: width 200ms ease;
}
.msgr-msg-out .msgr-poll-bar { background: rgba(255,255,255,0.25); }
.msgr-msg-out .msgr-poll-bar-fill { background: #fff; }
.msgr-poll-footer {
    font-size: 0.72rem;
    opacity: 0.6;
    margin-top: 2px;
}

/* Shared WhatsApp contact card(s) (Switchboard 1.39.0, media_type 'contact').
   Same neutral surface treatment as .msgr-doc-bubble/.msgr-poll; a person-initial
   avatar circle reuses the .msgr-avatar-initials sizing/weight convention. A
   contactsArrayMessage share renders one .msgr-contact-card per person, stacked. */
.msgr-contact-card {
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 200px;
    max-width: 280px;
    padding: 4px 2px;
}
.msgr-contact-card + .msgr-contact-card {
    margin-top: 8px;
    padding-top: 8px;
    border-top: 1px solid rgba(0,0,0,0.08);
}
[data-theme="dark"] .msgr-contact-card + .msgr-contact-card { border-top-color: rgba(255,255,255,0.12); }
.msgr-contact-avatar {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 600;
    font-size: 14px;
    color: #fff;
    background: var(--color-primary);
}
.msgr-msg-out .msgr-contact-avatar { background: rgba(255,255,255,0.28); }
.msgr-contact-info { min-width: 0; flex: 1; }
.msgr-contact-name {
    font-size: 0.85rem;
    font-weight: 600;
    word-break: break-word;
    margin-bottom: 3px;
}
.msgr-contact-phones { display: flex; flex-direction: column; gap: 2px; }
.msgr-contact-phone {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    font-size: 0.78rem;
    opacity: 0.85;
    cursor: pointer;
    padding: 1px 0;
}
.msgr-contact-phone:hover { opacity: 1; }
.msgr-contact-phone-num { word-break: break-all; }
.msgr-contact-copy-icon { font-size: 0.75rem; flex-shrink: 0; opacity: 0.7; }
.msgr-contact-copy-icon--ok { color: var(--color-success); opacity: 1; }
.msgr-contact-phone-none { font-size: 0.78rem; font-style: italic; opacity: 0.6; }

/* Beyond-layer WhatsApp media (see admin-messenger-render.js). */
.msgr-msg-unsupported {
    font-size: 0.8rem;
    font-style: italic;
    opacity: 0.65;
    max-width: 260px;
}

/* ── Expired media placeholder (90-day cleanup) ── */
.msgr-media-expired {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 12px;
    border-radius: 10px;
    background: rgba(0,0,0,0.04);
    border: 1px dashed rgba(0,0,0,0.18);
    font-size: 0.78rem;
    font-style: italic;
    color: var(--color-text-light);
    max-width: 320px;
    margin-bottom: 4px;
}
.msgr-media-expired svg { flex-shrink: 0; opacity: 0.7; }
.msgr-msg-out .msgr-media-expired {
    background: rgba(255,255,255,0.12);
    border-color: rgba(255,255,255,0.28);
    color: rgba(255,255,255,0.85);
}

/* ── Waveform bars ── */
.msgr-waveform {
    flex: 1;
    display: flex;
    align-items: center;
    gap: 2px;
    height: 32px;
    padding: 0 4px;
    overflow: hidden;
}
.msgr-wv-bar {
    flex: 1;
    min-width: 2px;
    border-radius: 2px;
    transition: background 0.06s;
}
/* Sent (dark bg) */
.msgr-msg-out .msgr-wv-bar         { background: rgba(255,255,255,0.32); }
.msgr-msg-out .msgr-wv-bar.played  { background: rgba(255,255,255,0.92); }
/* Received (light bg) */
.msgr-msg-in  .msgr-wv-bar         { background: var(--color-border); }
.msgr-msg-in  .msgr-wv-bar.played  { background: var(--color-primary); }

/* System messages */
.msgr-msg-system {
    display: flex;
    justify-content: center;
    margin: 6px 0;
}
.msgr-msg-system span {
    font-size: var(--font-size-small);
    color: var(--color-text-light);
    font-style: italic;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    padding: 2px 14px;
    border-radius: 12px;
}

/* Date separator */
.msgr-date-sep {
    text-align: center;
    padding: 8px 0;
}
.msgr-date-sep span {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 12px;
    padding: 2px 12px;
    font-size: var(--font-size-small);
    color: var(--color-text-light);
}

/* Highlight on search result click */
.msgr-msg-highlight {
    animation: msgrHighlight 2s cubic-bezier(0.22, 1, 0.36, 1);
}
@keyframes msgrHighlight {
    0%, 30% { background: rgba(255,213,79,0.3); }
    100% { background: transparent; }
}

/* Typing indicator */
.msgr-typing {
    padding: 4px 16px;
    font-size: var(--font-size-small);
    color: var(--color-text-light);
    font-style: italic;
}

/* ── Compose Bar ── */

.msgr-compose {
    border-top: 1px solid var(--color-border);
    padding: 8px 12px;
    background: var(--color-surface);
    transition: border-color 150ms var(--ease-out), padding-bottom 0.25s var(--ease-keyboard);
}
.msgr-compose:focus-within {
    border-color: rgba(232, 105, 30,0.35);
}
.msgr-compose-dragover {
    background: rgba(232, 105, 30,0.06);
    border-top: 2px dashed var(--color-primary);
}
/* Generic drop-zone highlight for text + image composers (announcements,
   enrichments, etc.) wired via wireFileDropAndPaste(). Uses an inset outline
   + tint so nothing shifts layout while a file is dragged over the modal. */
.media-dropzone-dragover {
    box-shadow: inset 0 0 0 2px var(--color-primary);
    background: color-mix(in srgb, var(--color-primary) 5%, transparent);
}
.msgr-compose-row {
    display: flex;
    align-items: center;
    gap: 8px;
}
/* ── Hold-to-record voice: in-place recording bar (Telegram-style) ── */
.msgr-recording-bar {
    flex: 1;
    display: flex;
    align-items: center;
    gap: 10px;
    height: 38px;
    min-width: 0;
    overflow: hidden;
    animation: recBarEnter 160ms var(--ease-out) both;
}
@keyframes recBarEnter {
    from { opacity: 0; transform: translateX(8px); }
    to   { opacity: 1; transform: translateX(0); }
}
.msgr-rec-dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--color-danger);
    flex-shrink: 0;
    animation: msgPulse 1s ease-in-out infinite;
}
.msgr-rec-time {
    font-variant-numeric: tabular-nums;
    font-weight: 500;
    color: var(--color-text);
    flex-shrink: 0;
}
.msgr-rec-wave {
    flex: 1;
    display: flex;
    align-items: center;
    gap: 2px;
    height: 28px;
    min-width: 0;
    overflow: hidden;
}
.msgr-rec-wv-bar {
    flex: 1;
    min-width: 2px;
    height: 100%;
    border-radius: 2px;
    background: var(--color-primary);
    transform: scaleY(0.08);
    transform-origin: center;
}
.msgr-rec-cancel-hint {
    flex-shrink: 0;
    color: var(--color-text-light);
    font-size: var(--font-size-small);
    white-space: nowrap;
    display: flex;
    align-items: center;
    gap: 4px;
    transition: opacity 0.12s var(--ease-out), color 0.12s var(--ease-out);
}
/* Cancel-armed: drag passed the threshold, releasing here discards */
.msgr-recording-bar.cancel-armed .msgr-rec-dot {
    background: var(--color-danger);
    animation: none;
    opacity: 1;
}
.msgr-recording-bar.cancel-armed .msgr-rec-cancel-hint {
    color: var(--color-danger);
    font-weight: 600;
}
/* In-place transform: while recording, hide the input + attach, show the bar */
.msgr-compose-row.recording > .msgr-attach-btn,
.msgr-compose-row.recording > .msgr-input-wrap { display: none; }
/* Too-short / hint tooltip, anchored to the action slot (position: relative) */
.msgr-rec-tooltip {
    position: absolute;
    bottom: calc(100% + 8px);
    right: 0;
    background: var(--color-text);
    color: var(--color-surface);
    font-size: var(--font-size-small);
    padding: 6px 10px;
    border-radius: 8px;
    white-space: nowrap;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.15s var(--ease-out);
    z-index: 5;
}
.msgr-rec-tooltip.show { opacity: 1; }
.msgr-attach-btn {
    background: none;
    border: none;
    padding: 0;
    width: 38px;
    height: 38px;
    flex-shrink: 0;
    color: var(--color-text-light);
    cursor: pointer;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 25px;
    transition: color 0.15s, transform 100ms var(--ease-out);
}
.msgr-attach-btn:hover  { color: var(--color-primary); }
.msgr-attach-btn:active { transform: scale(0.88); }
.msgr-voice-btn {
    background: none;
    border: none;
    padding: 0;
    width: 44px;
    height: 44px;
    color: var(--color-text-light);
    cursor: pointer;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--font-size-heading);
    transition: color 0.15s var(--ease-out), transform 120ms var(--ease-out), opacity 150ms var(--ease-out);
    touch-action: none;
    user-select: none;
    -webkit-user-select: none;
    -webkit-touch-callout: none;
}
.msgr-voice-btn:hover  { color: var(--color-primary); }
.msgr-voice-btn:active { transform: scale(0.88); }
.msgr-action-slot {
    position: relative;
    width: 38px;
    height: 38px;
    flex-shrink: 0;
}
.msgr-action-slot > .msgr-voice-btn,
.msgr-action-slot > .msgr-send-btn {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    flex-shrink: unset;
}
.msgr-send-btn {
    border-radius: 50%;
    background: var(--color-primary);
    color: #fff;
    border: none;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--font-size-heading);
    transition: opacity 150ms var(--ease-out), transform 120ms var(--ease-out);
}
.msgr-send-btn:hover  { opacity: 0.85; }
.msgr-send-btn:active { transform: scale(0.9); }
.msgr-send-btn .fi { position: relative; top: 1px; left: -1px; }
.msgr-compose-input {
    flex: 1;
    border: none;
    border-radius: 8px;
    padding: 10px 10px 6px;
    font-size: var(--font-size-body);
    resize: none;
    max-height: 186px;
    min-height: 38px;
    background: var(--color-bg);
    color: var(--color-text);
    font-family: inherit;
    box-sizing: border-box;
    /* Native grow-to-content where supported (same cap as the JS fallback:
       max-height above still clamps it, so growth is pixel-equivalent).
       js/admin-messenger-compose.js gates its manual autoResize behind
       CSS.supports('field-sizing', 'content'), so this is the primary path,
       not decoration. Ported from canopy commit 77984614. */
    field-sizing: content;
}

/* Compose input + response star button live together in this wrapper
   so the star pins to the bottom-right of the text input area (inline-smiley
   placement). The wrapper carries the same background as the textarea so the
   star reads as "inside" the text field rather than alongside it — match the
   textarea in BOTH themes (the dark theme hardcodes the textarea bg, so we
   mirror that override here). */
.msgr-input-wrap {
    flex: 1;
    display: flex;
    align-items: flex-end;
    gap: 0;
    background: var(--color-bg);
    border-radius: 8px;
    min-width: 0;
    position: relative; /* anchor for the @mention typeahead dropdown */
}

/* @mention typeahead — floats above the composer (chat-app pattern) */
.msgr-mention-dropdown {
    position: absolute;
    left: 0;
    right: 0;
    bottom: calc(100% + 8px);
    max-height: 220px;
    overflow-y: auto;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 10px;
    box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);
    z-index: 20;
    padding: 4px;
}
.msgr-mention-option {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 7px 8px;
    border-radius: 7px;
    cursor: pointer;
}
.msgr-mention-option.active {
    background: var(--color-bg);
}
.msgr-mention-option-avatar {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: var(--color-primary);
    color: #fff;
    font-size: 11px;
    font-weight: 700;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}
.msgr-mention-option-name {
    font-size: var(--font-size-body);
    color: var(--color-text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* @mention highlight inside a rendered bubble */
.msgr-mention {
    color: var(--color-primary);
    font-weight: 600;
}
.msgr-msg-out .msgr-mention {
    color: #fff;
    text-decoration: underline;
    text-underline-offset: 2px;
}
/* Staff-reply bubbles are right-aligned (.msgr-msg-out) but white — keep the
   accent colour, not the white-on-coloured treatment. */
.msgr-msg-staff-reply .msgr-mention {
    color: var(--color-primary);
    text-decoration: none;
}
/* Messenger Settings modal — notification preferences */
/* Each top-level settings category is a titled group. Siblings (future
   categories) are delineated by a top divider + spacing. */
.msgr-settings-group + .msgr-settings-group {
    margin-top: 20px;
    padding-top: 20px;
    border-top: 1px solid var(--color-border);
}
.msgr-settings-group-title {
    font-size: var(--font-size-body);
    font-weight: 600;
    color: var(--color-text);
    margin: 0 0 12px;
}
.msgr-settings-section { display: flex; flex-direction: column; }
.msgr-notif-row {
    align-items: center;
    gap: 12px;
    padding: 10px 0;
    border-bottom: 1px solid var(--color-border);
    width: 100%;
}
.msgr-notif-row:last-of-type { border-bottom: none; }
.msgr-notif-icon {
    flex-shrink: 0;
    width: 38px;
    height: 38px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: color-mix(in srgb, var(--color-primary) 12%, transparent);
    color: var(--color-primary);
    font-size: 17px;
}
.msgr-notif-text {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 2px;
    line-height: 1.3;
}
.msgr-settings-foot { margin: 12px 0 0; line-height: 1.4; }
.msgr-settings-sub { margin: 0 0 8px; line-height: 1.4; }
/* Notification popups (per-conversation + per-folder) — reuse .msgr-ctx-menu +
   .msgr-notif-row. Only the toggle-popup width + small headers are new. */
.msgr-ctx-menu-notif { width: 300px; min-width: 264px; }
.msgr-ctx-menu-notif .msgr-notif-row { padding: 9px 8px; }
.msgr-ctx-menu-notif .msgr-notif-icon { width: 32px; height: 32px; font-size: 15px; }
.msgr-notif-menu-head {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 8px 8px;
    border-bottom: 1px solid var(--color-border);
    margin-bottom: 2px;
    font-weight: 600;
    color: var(--color-text);
}
.msgr-notif-menu-head i { color: var(--color-primary); flex-shrink: 0; }
.msgr-notif-menu-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Pinned-message bar — sits below the thread header, jumps to the pinned
   message on tap. Accent left border (chat-app convention). */
.msgr-pinned-bar {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 14px;
    background: var(--color-surface, #fff);
    border-bottom: 1px solid var(--color-border);
    border-left: 3px solid var(--color-primary);
    cursor: pointer;
    flex-shrink: 0;
    transition: background 0.15s;
}
.msgr-pinned-bar:hover { background: color-mix(in srgb, var(--color-primary) 6%, transparent); }
/* Realtime hard-timeout status bar + WhatsApp channel-broken cascade banner
   (js/admin-messenger-realtime.js _msgrSbRenderRtBanner, js/admin-messenger.js
   _msgrRenderWaChannelBanner; ported from canopy). Both reuse the shared .alert
   component (margin reset to sit flush against the thread panel edges, unlike
   a form's .alert). */
.msgr-rt-status-bar.show,
#msgrWaChannelBanner.show {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin: 0;
    border-radius: 0;
    border-width: 0 0 1px 0;
    flex-shrink: 0;
    font-size: var(--font-size-small);
}
.msgr-rt-status-bar .btn { flex-shrink: 0; }
/* Paused/reconnect state (reconnect budget exhausted) is persistent but must
   read as calmer than a hard error - the poll backstop is still delivering
   messages, this is informational, not a failure demanding urgency. */
.msgr-rt-status-bar-lost.show {
    background: var(--color-warning-bg);
    color: var(--color-warning);
    border-color: var(--color-warning);
}
.msgr-pinned-icon {
    color: var(--color-primary);
    font-size: 15px;
    flex-shrink: 0;
    transform: rotate(45deg);
}
.msgr-pinned-info {
    display: flex;
    flex-direction: column;
    min-width: 0;
    line-height: 1.3;
}
.msgr-pinned-label {
    font-size: var(--font-size-small);
    font-weight: 600;
    color: var(--color-primary);
}
.msgr-pinned-count { color: var(--color-text-light); font-weight: 500; }
.msgr-pinned-text {
    font-size: var(--font-size-small);
    color: var(--color-text-light);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* "@" badge: conversation has an unread message that @mentions
   me. Accent (not danger red) so it reads distinctly from the unread count. */
.msgr-mention-badge {
    background: var(--color-primary);
    color: #fff;
    font-size: var(--font-size-small);
    font-weight: 700;
    min-width: 18px;
    height: 18px;
    border-radius: 9px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 5px 1px;
    flex-shrink: 0;
    margin-left: 4px;
}
[data-theme="dark"] .msgr-input-wrap {
    background: rgb(37, 48, 48);
}
.msgr-input-wrap > .msgr-compose-input {
    background: transparent;
    flex: 1;
    padding-right: 6px;
}
[data-theme="dark"] .msgr-input-wrap > .msgr-compose-input {
    background: transparent;
}
.msgr-response-btn {
    background: none;
    border: none;
    padding: 0;
    width: 36px;
    height: 38px;
    flex-shrink: 0;
    color: var(--color-text-light);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 18px;
    transition: color 0.15s var(--ease-out), transform 120ms var(--ease-out);
}
.msgr-response-btn:hover  { color: var(--color-warning, #e0a000); }
.msgr-response-btn:active { transform: scale(0.88); }

