/**
 * Messenger — split-pane chat layout
 * Sidebar (conversation list) + main panel (messages / empty state)
 */

/* ════════════════════════════════════════════════════════════════════
 * APP FONT — Plus Jakarta Sans (self-hosted)
 * ════════════════════════════════════════════════════════════════════
 * The UI face, self-hosted as local woff2 files (variable font, one file
 * per subset covering the whole 400–700 weight range) instead of loading
 * Google Fonts at runtime — no external requests, and the files are
 * precached by the service worker (app/Config/Pwa.php) so the font
 * renders offline and never produces failed-resource console errors.
 * Subset/ranges match Google Fonts' own CSS exactly. */
@font-face {
    font-family: "Plus Jakarta Sans";
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url("../fonts/plus-jakarta-sans/pjs-latin.woff2") format("woff2");
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
    font-family: "Plus Jakarta Sans";
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url("../fonts/plus-jakarta-sans/pjs-latin-ext.woff2") format("woff2");
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
    font-family: "Plus Jakarta Sans";
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url("../fonts/plus-jakarta-sans/pjs-cyrillic-ext.woff2") format("woff2");
    unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}

@font-face {
    font-family: "Plus Jakarta Sans";
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url("../fonts/plus-jakarta-sans/pjs-vietnamese.woff2") format("woff2");
    unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}

/* ════════════════════════════════════════════════════════════════════
 * TABLE OF CONTENTS
 * ════════════════════════════════════════════════════════════════════
 *  1. Design tokens (--ms-*)
 *  2. Helheim design tokens (--hm-*)
 *  3. App shell
 *  4. Sidebar
 *     4.1  Sidebar header
 *     4.2  Sidebar search
 *     4.3  Conversation list
 *     4.4  Account menu & dropdown
 *     4.5  Shared components (avatar, icon button, cards, tables, badges,
 *          pagination, page header, switch)
 *  5. Main panel
 *     5.1  Empty state
 *     5.2  Chat panel & header
 *     5.3  Messages & loading
 *     5.4  Message bubbles
 *     5.5  File attachments
 *     5.6  Typing indicator
 *     5.7  Composer
 *  6. Profile panel
 *  7. Helheim modals
 *     7.1  Modal chrome (dialog, header, body, footer)
 *     7.2  Modal form fields
 *     7.3  New-chat results list
 *     7.4  Modal backdrop
 *     7.5  Image preview (lightbox)
 *  8. Responsive: mobile shows one pane at a time
 *  9. Responsive: sidebar compact
 * 10. Authentication (Phase 1)
 *     10.1 Backdrop & layout
 *     10.2 Logo & wordmark
 *     10.3 Auth card & stages
 *     10.4 Form fields
 *     10.5 Alerts
 *     10.6 Buttons & busy state
 *     10.7 Status lines & footer
 * 11. Authentication responsive tweaks
 * 12. Toasts
 * 13. Offline restriction styling (Phase 3b + plan E)
 * 14. Launch splash (1:1 copy of the client startup screen)
 * ════════════════════════════════════════════════════════════════════ */

/* Global box-sizing reset: the auth page is built without Bootstrap,
 * so widths+padding/borders must not overflow their containers.
 * (Bootstrap applies the same rule on pages that load it.) */
*,
*::before,
*::after {
    box-sizing: border-box;
}

/* 1 · Design tokens */
:root {
    /* App-wide neutrals plus the configurable accent tokens. --ms-accent and
     * --ms-secondary are overridden per page by partials/head.php from
     * app/Config/Messenger.php (primaryColor / secondaryColor); the values
     * below are the fallbacks for pages rendered without that partial. */
    --ms-bg: #f1f4f6;
    --ms-surface: #ffffff;
    --ms-text: #1c2430;
    --ms-text-muted: #68747f;
    --ms-accent: #6658ec;
    --ms-secondary: #f0f0f0;
    /* Text/icons that sit on the primary fill (button labels, send icon,
     * unread counts) — see onPrimaryColor in app/Config/Messenger.php. */
    --ms-on-primary: #ffffff;
    /* Derived accent family — the hover shade, deep stroke accent, toggle
     * and new-chat FAB all derive from --ms-accent, so changing
     * primaryColor alone re-tints the whole family. partials/head.php
     * injects explicit values only when the config overrides a key. */
    --ms-primary-hover: color-mix(in srgb, var(--ms-accent) 88%, #000);
    --ms-stroke: color-mix(in srgb, var(--ms-accent) 55%, #000);
    --ms-toggle: var(--ms-accent);
    --ms-new-chat: var(--ms-accent);
    --ms-border: #1b2430;
    /* Boot splash frame — see splashColor in app/Config/Messenger.php. */
    --ms-splash: #0d1110;
    /* Neutral families — same overrides as the accent tokens: the values
     * below are the fallbacks for pages rendered without partials/head.php
     * (installer, offline shell); every page served through head.php takes
     * them from app/Config/Messenger.php. */
    --ms-bg: #f1f4f6;
    --ms-surface: #ffffff;
    --ms-text: #1c2430;
    --ms-text-muted: #68747f;
    --ms-danger: #dc2626;
    --ms-success: #0f8f43;
    --ms-warning: #d97706;
    /* New-chat FAB icon — onNewChatColor in app/Config/Messenger.php. */
    --ms-on-new-chat: #ffffff;
}

/* 2 · Design tokens (UI palette)
 * One neutral scale plus the blue accent family. --hm-primary-bg and
 * --hm-secondary-bg read the configurable --ms-accent / --ms-secondary
 * tokens (set from app/Config/Messenger.php), so a single config change
 * re-tints every component that uses them. */
:root {
    /* Backgrounds & surfaces — every value here derives from the
     * configurable --ms-* hues, so a single change in
     * app/Config/Messenger.php re-tints the whole UI coherently. */
    --hm-bg: var(--ms-bg);
    --hm-surface: color-mix(in srgb, var(--ms-surface) 92%, transparent);
    --hm-surface-solid: var(--ms-surface);
    --hm-surface-soft: color-mix(in srgb, var(--ms-bg) 65%, var(--ms-surface));
    /* Soft accent wash for icon tiles (file-attachment icons, admin stat
     * cards, icon-button hover washes) — derived from the SAME accent hue
     * as --hm-primary-tint, so every icon tile speaks one tinted language:
     * a 9% wash here, the 16% wash of --hm-primary-tint for runes/active
     * states — two strengths of one hue. */
    --hm-surface-blue: color-mix(in srgb, var(--ms-accent) 9%, #fff);

    /* Brand neutrals — the dark ink family comes from textColor. */
    --hm-navy: var(--hm-text);
    --hm-navy-soft: color-mix(in srgb, var(--hm-text) 78%, #fff);
    /* Accent stroke color for icons, links and emphasis text (the readable
     * counterpart of the bright primary). The deeper/softer shades used by
     * badges, read ticks and tracks are derived from this single hue, so
     * changing strokeColor re-tints the whole accent family coherently. */
    --hm-blue: var(--ms-stroke);
    --hm-blue-strong: color-mix(in srgb, var(--ms-stroke) 82%, #000);
    --hm-blue-soft: color-mix(in srgb, var(--ms-stroke) 28%, #fff);

    /* Text */
    --hm-text: var(--ms-text);
    --hm-muted: var(--ms-text-muted);
    --hm-faint: color-mix(in srgb, var(--ms-text-muted) 72%, #fff);

    /* Status accents */
    --hm-green: var(--ms-success);
    --hm-warning: var(--ms-warning);
    --hm-red: var(--ms-danger);
    --hm-red-deep: color-mix(in srgb, var(--ms-danger) 78%, #000);

    /* Hairlines — the border hues come from --ms-border at two opacities
     * (configurable in app/Config/Messenger.php). */
    --hm-line: color-mix(in srgb, var(--ms-border) 12%, transparent);
    --hm-line-strong: color-mix(in srgb, var(--ms-border) 22%, transparent);

    /* Solid brand fills (the old signature gradients are gone) */
    --hm-primary-bg: var(--ms-accent);
    /* Hover state of primary fills (app/Config/Messenger.php
     * primaryHoverColor) — every primary control darkens on hover/focus. */
    --hm-primary-hover: var(--ms-primary-hover);
    /* Text/icon color that sits ON the bright primary fill (button labels,
     * send icon, unread counts). Driven by onPrimaryColor in
     * app/Config/Messenger.php. */
    --hm-on-primary: var(--ms-on-primary);
    /* Primary washes for hovered/active rows — derived from the accent hue. */
    --hm-primary-tint-strong: color-mix(in srgb, var(--ms-accent) 20%, transparent);
    --hm-primary-tint: color-mix(in srgb, var(--ms-accent) 16%, transparent);
    --hm-primary-tint-soft: color-mix(in srgb, var(--ms-accent) 8%, transparent);
    /* Status washes at the opacities the UI uses (badges, toasts, rows). */
    --hm-success-tint: color-mix(in srgb, var(--ms-success) 14%, transparent);
    --hm-success-tint-soft: color-mix(in srgb, var(--ms-success) 12%, transparent);
    --hm-danger-tint: color-mix(in srgb, var(--ms-danger) 8%, transparent);
    --hm-danger-tint-strong: color-mix(in srgb, var(--ms-danger) 14%, transparent);
    --hm-warning-tint: color-mix(in srgb, var(--ms-warning) 14%, transparent);
    --hm-warning-tint-mid: color-mix(in srgb, var(--ms-warning) 12%, transparent);
    --hm-muted-tint: color-mix(in srgb, var(--hm-muted) 14%, transparent);

    /* Alert recipe: tinted wash + hairline + strong text per status. */
    --hm-success-alert-bg: color-mix(in srgb, var(--ms-success) 10%, transparent);
    --hm-success-alert-line: color-mix(in srgb, var(--ms-success) 28%, transparent);
    --hm-success-alert-text: color-mix(in srgb, var(--ms-success) 55%, var(--hm-text));
    --hm-danger-alert-bg: color-mix(in srgb, var(--ms-danger) 10%, transparent);
    --hm-danger-alert-line: color-mix(in srgb, var(--ms-danger) 28%, transparent);
    --hm-warning-alert-bg: color-mix(in srgb, var(--ms-warning) 10%, transparent);
    --hm-warning-alert-line: color-mix(in srgb, var(--ms-warning) 28%, transparent);
    --hm-warning-alert-text: color-mix(in srgb, var(--ms-warning) 55%, var(--hm-text));
    --hm-info-alert-bg: color-mix(in srgb, var(--hm-blue-strong) 10%, transparent);
    --hm-info-alert-line: color-mix(in srgb, var(--hm-blue-strong) 28%, transparent);

    /* Toggle switch — driven by toggleColor (app/Config/Messenger.php): the
     * ON track is the solid hue, the OFF track a pale wash of the same hue. */
    --hm-toggle: var(--ms-toggle);
    --hm-toggle-track: color-mix(in srgb, var(--ms-toggle) 8%, var(--ms-bg));

    --hm-avatar-bg: var(--ms-secondary);
    --hm-secondary-bg: var(--ms-secondary);
    --hm-secondary-hover: color-mix(in srgb, var(--ms-secondary) 94%, #000);
    /* New-chat FAB — its own accent hue (newChatColor) plus the icon color
     * that sits on it (onNewChatColor); hover darkens the fill. */
    --hm-fab: var(--ms-new-chat);
    --hm-fab-hover: color-mix(in srgb, var(--ms-new-chat) 88%, #000);
    --hm-on-fab: var(--ms-on-new-chat);
    --hm-danger-bg: var(--ms-danger);
    --hm-danger-hover: color-mix(in srgb, var(--ms-danger) 88%, #000);

    /* Overlay scrim hue — the dark veil behind modals/lightbox, the × chips
     * on attachment tiles and the avatar upload dim. Deliberately
     * theme-independent: these sit ABOVE either theme's surfaces, and a
     * light scrim (the --hm-navy mix would flip to near-white in dark)
     * would wash out the dialog, lightbox or photo. Same navy the dropdown
     * shadows already use; per-site opacity is mixed on top. */
    --hm-scrim-base: #0f172a;

    /* Layout constants */
    --hm-header-height: 68px;
    --hm-sidebar-width: 320px;

    /* Font stacks */
    --hm-font-sans: "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    --hm-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;

    /* Motion */
    --hm-ease: 150ms ease;
    /* Unified press interaction: every clickable control gives a subtle push
     * down while the pointer is down (see the grouped global :active rule).
     * A translate (not a scale) is used so text is never re-rasterized smaller —
     * scaling would make bold labels render thinner. */
    --hm-press: translateY(1px);
}

/* ===== Dark theme =====
 * html.hm-dark is added by partials/head.php from the user's theme
 * preference (app/Config/Messenger.php darkTheme); the base --ms-* hues
 * for dark are injected there. This block only adjusts the DERIVED tokens
 * whose light recipes mix toward #fff/#000 — on a dark surface the same
 * mix would wash out (a 9% accent + white tile would glow instead of
 * tint, and a #000-derived stroke would vanish), so they blend toward the
 * dark surface / lift instead. color-scheme: dark also flips native
 * scrollbars and form controls. */
html.hm-dark {
    color-scheme: dark;

    /* Accent stroke lifted for dark surfaces (used when head.php leaves
     * strokeColor null in darkTheme) — a #000-derived stroke would be
     * invisible on dark, so it blends toward white instead. */
    --ms-stroke: color-mix(in srgb, var(--ms-accent) 68%, #fff);
    --ms-primary-hover: color-mix(in srgb, var(--ms-accent) 84%, #000);

    /* Surface-blended derived tokens (see the :root recipes above). */
    --hm-surface-blue: color-mix(in srgb, var(--ms-accent) 16%, var(--ms-surface));
    --hm-navy-soft: color-mix(in srgb, var(--hm-text) 72%, var(--ms-surface));
    --hm-blue-soft: color-mix(in srgb, var(--ms-stroke) 30%, var(--ms-surface));
    --hm-faint: color-mix(in srgb, var(--ms-text-muted) 70%, var(--ms-surface));
    /* Secondary fills lighten on hover instead of darkening. */
    --hm-secondary-hover: color-mix(in srgb, var(--ms-secondary) 94%, #fff);
}

/* The black focus indicator (html.hm-focus-border) is invisible on dark
 * surfaces — use the light text ink instead (--hm-navy follows textColor
 * from app/Config/Messenger.php darkTheme). */
html.hm-dark.hm-focus-border .hm-switch:focus-visible {
    outline-color: var(--hm-navy);
}

html.hm-dark.hm-focus-border .messenger-composer__attachment:focus-visible,
html.hm-dark.hm-focus-border .field input:focus,
html.hm-dark.hm-focus-border .field select:focus,
html.hm-dark.hm-focus-border .field textarea:focus,
html.hm-dark.hm-focus-border .form-control:focus,
html.hm-dark.hm-focus-border .form-select:focus,
html.hm-dark.hm-focus-border .otp-input:focus {
    border-color: var(--hm-navy);
}

/* 3 · App shell */
.messenger-app {
    /* svh (small viewport) is used instead of dvh: in a standalone PWA the
     * browser's bottom bar overlays the page WITHOUT resizing dvh, so
     * 100dvh was taller than the visible area — the bottom of every panel
     * (logout card, newest message) sat under the bar, unreachable. svh is
     * the viewport height with all browser UI visible, so the fixed-height
     * shell always fits the visible area and its inner panels scroll. 100vh
     * stays as the pre-svh fallback for older browsers.
     *
     * --app-height is the JS-managed live height (messenger-utils.js visualViewport
     * tracker): when the mobile keyboard opens, the visual viewport shrinks
     * while svh stays put, so without this the composer would slide under the
     * keyboard. The tracker overrides the variable with the current visible
     * height in px, keeping the composer pinned above the keyboard. Falls back
     * to the :root default (100svh) on desktop and when the API is absent.
     *
     * position: fixed + a JS-tracked top — iOS Safari/PWA fix. iOS doesn't
     * just scroll the document to keep a focused field above the keyboard —
     * WebKit separately pans the VISUAL viewport itself (window.
     * visualViewport.offsetTop), which happens even when nothing on the
     * page is scrollable, and which window.scrollTo() cannot cancel. So
     * instead of fighting that pan, the shell now follows it: --app-height
     * (below) shrinks the shell to the visible height as before, and the
     * new --app-offset-top var (messenger-utils.js) mirrors
     * visualViewport.offsetTop into the shell's `top`, keeping it aligned
     * with whatever iOS currently considers "visible" instead of getting
     * left behind above it. See the html:has() rule below and the
     * applyViewport() tracker in messenger-utils.js. */
    position: fixed;
    top: var(--app-offset-top, 0px);
    left: 0;
    right: 0;
    /* height still comes from --app-height below; top now comes from the
     * live --app-offset-top var instead of a static inset, so the shell
     * tracks iOS's own pan frame-by-frame rather than fight it. */
    height: 100vh;
    height: var(--app-height, 100svh);
    margin: 0;
    overflow: hidden;
    background: var(--ms-bg);
    color: var(--ms-text);
    /* The app-wide face — Plus Jakarta Sans (--hm-font-sans), the single
     * token behind the Google Fonts load in head.php. Applied on the shell
     * (which is <body>, so teleported Bootstrap modals/toasts inherit it
     * too) and mirrored by .auth-page. */
    font-family: var(--hm-font-sans);
}

/* Companion to the position: fixed shell above — with <body> taken out of
 * flow, <html> is the only element left that could still expose a
 * scrollable page for iOS to pan against. Scoped to messenger pages only
 * (:has(body.messenger-app)) so the auth/admin pages, which rely on normal
 * body scrolling, are untouched. */
html:has(body.messenger-app) {
    height: 100%;
    overflow: hidden;
    overscroll-behavior: none;
}

.messenger-layout {
    display: flex;
    height: 100vh;
    height: var(--app-height, 100svh);
    margin: 0 auto;
    background: var(--ms-surface);
}

/* 4 · Sidebar
/* 4 · Sidebar */
.messenger-sidebar {
    position: relative;
    display: flex;
    flex-direction: column;
    width: var(--hm-sidebar-width);
    min-width: var(--hm-sidebar-width);
    border-right: 1px solid var(--hm-line);
    background: color-mix(in srgb, var(--ms-surface) 97%, transparent);
}

/* 4.1 · Sidebar header */

/* Header: brand mark + current-user identity. The box is exactly the chat
 * header's: same 68px height (the fixed clamp keeps the brand copy from
 * inflating it), same 9px 14px padding, same 10px gap — so both headers
 * display at the same height with the same internal spacing. */
.messenger-sidebar__header {
    display: flex;
    align-items: center;
    gap: 10px;
    height: var(--hm-header-height);
    min-height: var(--hm-header-height);
    padding: 9px 14px;
    border-bottom: 1px solid var(--hm-line);
}

.brand-copy {
    min-width: 0;
    flex: 1;
}

/* Identity name — matches the chat header title (.messenger-chat__title)
 * exactly: same sans face, 1rem, 700 weight, full ink and the same 1.2
 * line-height, so the text block aligns pixel-for-pixel with the chat
 * header's. Shared by both sidebars (the user's name in the messenger
 * header, the app wordmark in the admin), so the whole header language
 * stays one. */
.brand-name {
    display: block;
    overflow: hidden;
    color: var(--hm-navy);
    font-size: 1rem;
    font-weight: 700;
    line-height: 1.2;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Identity line under the header name (Guest / Jarl / Konung). Mirrors the
 * chat header status (.messenger-chat__status): same 0.71rem size, regular
 * weight, quiet color and the same 1.5 line-height — the text block then
 * sits exactly where the chat's status does. Used by both sidebars — same
 * recipe. */
.brand-copy .brand-role {
    display: block;
    margin-top: 3px;
    overflow: hidden;
    color: var(--hm-faint);
    font-size: 0.71rem;
    line-height: 1.5;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Current-user avatar with a green presence dot (header, left). */
.messenger-sidebar__avatar-wrap {
    position: relative;
    display: inline-block;
    flex: 0 0 auto;
}

/* The header avatar is a link to the profile page. The wrap keeps its flex
 * sizing and presence dot; the link carries the click affordance (pointer +
 * a soft ring on hover/focus) and the same floating-glass tooltip as the
 * other icon buttons (the profile label). */
.messenger-sidebar__avatar-link {
    /* Flex (not block): the wrap inside is an inline-block whose baseline
     * added ~6px of line-height bleed, inflating the link to 52px and
     * pushing the avatar 3px high against the chat header. As a flex
     * container the link hugs its 46px child exactly. */
    display: flex;
    align-items: center;
    flex: 0 0 auto;
    border-radius: 50%;
    text-decoration: none;
    position: relative;
    cursor: pointer;
    transition:
        filter var(--hm-ease);
}

.messenger-sidebar__avatar-link:hover,
.messenger-sidebar__avatar-link:focus-visible {
    outline: none;
    filter: brightness(0.96);
}

.messenger-sidebar__avatar {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 46px;
    height: 46px;
    border-radius: 50%;
    object-fit: cover;
    flex-shrink: 0;
    color: var(--hm-navy);
    font-size: 1.05rem;
    font-weight: 700;
    font-family: inherit;
    line-height: 1;
    -webkit-user-select: none;
    /* Safari 15 and older */
    user-select: none;
}

/* The initial-circle state of the header avatar matches .hm-avatar-initial
 * (same rune-circle look + secondary fill + hairline); the photo <img> keeps
 * no border. */
.messenger-sidebar__avatar:not(img) {
    overflow: hidden;
    border: 1px solid var(--hm-line-strong);
    background: var(--hm-secondary-bg);
}

.messenger-sidebar__avatar-dot {
    position: absolute;
    right: 1px;
    bottom: 1px;
    width: 10px;
    height: 10px;
    border: 2px solid var(--hm-secondary-bg);
    border-radius: 50%;
    background: var(--hm-green);
}

.messenger-sidebar__avatar-dot.is-offline {
    background: var(--hm-faint);
}

/* The account-free offline shell renders the sidebar header with an EMPTY
 * identity (avatar + name + role are restored from the cached current user
 * after boot, see messenger.js). Keep the whole identity column invisible
 * while it is still pending — the empty avatar circle and blank text are
 * placeholders, not data. `visibility` (not display) preserves the header
 * layout, so the real avatar/name/role appear in place without any reflow. */
.messenger-sidebar__header.is-pending .messenger-sidebar__avatar-wrap,
.messenger-sidebar__header.is-pending .brand-copy {
    visibility: hidden;
}

/* App icon in the header (used by the admin sidebar). Painted with the
 * rune-circle recipe the same way Helheim presents its brand mark: a rounded
 * square painted on a soft neutral tile, so the
 * transparent PNG logo reads over the pale tile instead of a flat box. */
.messenger-sidebar__app-icon {
    flex: 0 0 auto;
    width: 46px;
    height: 46px;
    border: 1px solid var(--hm-line-strong);
    border-radius: 13px;
    overflow: hidden;
    background: var(--hm-avatar-bg);
}

.messenger-sidebar__app-icon img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
}

/* 4.2 · Sidebar search */

/* Search row: filled search pill. The "new chat" action lives separately as a
 * FAB pinned to the sidebar's bottom-right corner (see .messenger-search__new
 * below); the Archives entry is a full-width row below the search (see
 * .messenger-sidebar__archives-btn). */
.messenger-sidebar__search {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 12px 12px 10px;
}

/* Filled pill in the secondary color (bubbles share the same fill), borderless
 * at rest — the black border appears only on focus, matching the app-wide
 * focus language. */
.messenger-search {
    display: flex;
    align-items: center;
    gap: 9px;
    flex: 1;
    min-width: 0;
    height: 43px;
    padding: 0 13px;
    border: 1px solid transparent;
    border-radius: 999px;
    background: var(--hm-secondary-bg);
    transition: border-color var(--hm-ease);
}

/* No hover state: the pill keeps its default secondary fill under the cursor
 * (only the black focus border, when enabled, changes anything). */

/* The black focus border is gated behind html.hm-focus-border — a class
 * added to <html> by partials/head.php only when app/Config/Messenger.php
 * focusBorder is enabled. When disabled, focused fields simply keep their
 * resting border (the pill stays borderless). */

.messenger-search__icon {
    flex: 0 0 auto;
    color: var(--hm-blue-strong);
}

.messenger-search__input {
    flex: 1;
    min-width: 0;
    border: 0;
    outline: 0;
    color: var(--hm-text);
    font-family: inherit;
    font-size: 0.9rem;
    background: transparent;
}

.messenger-search__input::placeholder {
    color: var(--hm-muted);
}

/* New-chat FAB — a floating action button pinned to the sidebar's bottom-right
 * corner (the sidebar anchors it via position: relative), floating over the
 * conversation list like Telegram/WhatsApp. It carries its own accent color
 * (newChatColor in app/Config/Messenger.php — change one hex, the FAB
 * re-tints) and darkens on hover; flat and borderless, consistent with the
 * app's zero-shadow language. */
.messenger-search__new {
    position: absolute;
    right: 16px;
    bottom: 20px;
    z-index: 5;
    display: grid;
    place-items: center;
    width: 52px;
    height: 52px;
    border: 0;
    border-radius: 50%;
    color: var(--hm-on-fab);
    background: var(--hm-fab);
    /* The FAB is the one elevated control — a soft glow in its own fill
     * color (derived from --hm-fab, so it re-tints with the config). */
    box-shadow: 0 10px 22px -8px color-mix(in srgb, var(--hm-fab) 55%, transparent);
    cursor: pointer;
    transition:
        background var(--hm-ease),
        transform var(--hm-ease),
        box-shadow var(--hm-ease);
}

.messenger-search__new:hover,
.messenger-search__new:focus-visible {
    background: var(--hm-fab-hover);
    outline: none;
}

/* Archives entry: a full-width row directly below the search, styled like the
 * "Back to chats" link — an accent-tinted icon chip + bold navy label, no card,
 * no border, no hover fill. It swaps the sidebar to the archived-chats view in
 * place (no page reload); `is-active` marks the archived view with the same
 * soft primary wash the list's active row uses. The badge sits on the right. */
.messenger-sidebar__archives-btn {
    display: flex;
    align-items: center;
    gap: 9px;
    width: 100%;
    min-height: 42px;
    margin: 0 0 7px;
    padding: 0 12px;
    /* 12px, the search pill's left edge */
    border: 0;
    color: var(--hm-navy);
    font-family: inherit;
    font-size: 0.9rem;
    font-weight: 600;
    text-align: left;
    background: transparent;
    cursor: pointer;
    transition:
        color var(--hm-ease),
        background var(--hm-ease);
}

/* Icon chip: the same accent-tinted circle as the "Back to chats" link's —
 * sized like the header's circular icon buttons (42px) so the row reads as a
 * full-size sidebar control. */
.messenger-sidebar__archives-icon {
    display: grid;
    place-items: center;
    width: 42px;
    height: 42px;
    flex: 0 0 auto;
    border-radius: 50%;
    color: var(--hm-blue-strong);
    background: var(--hm-primary-tint);
}

.messenger-sidebar__archives-label {
    flex: 1;
    min-width: 0;
    line-height: 1.2;
}

/* Hover keeps the same quiet, plain-row look as the "Back to chats" link:
 * only the label tint shifts (no card fill, no darkened wash). */
.messenger-sidebar__archives-btn:hover,
.messenger-sidebar__archives-btn:focus-visible {
    color: var(--hm-blue-strong);
    outline: none;
}

.messenger-sidebar__archives-btn.is-active {
    color: var(--hm-blue-strong);
    background: var(--hm-primary-tint);
}

/* The entry is hidden while the archived view is open (the "Back to chats"
 * link becomes the only navigation back); the hidden attribute must beat the
 * button's display:flex — same pattern as the muted bell / empty states. */
.messenger-sidebar__archives-btn[hidden] {
    display: none;
}

/* 4.3 · Conversation list */

/* Uppercase section label above the conversation list. */
.messenger-sidebar__section-label {
    margin: 0;
    padding: 7px 16px;
    color: var(--hm-faint);
    font-size: 0.64rem;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
}

/* Conversation list (scrollable). The generous bottom padding keeps the last
 * row clear of the new-chat FAB that floats over the list's bottom-right
 * corner. */
.messenger-sidebar__list-wrap {
    flex: 1;
    min-height: 0;
    padding: 0 8px 96px;
    overflow-y: auto;
    /* Vertical-only scroll: a stray wide row must never add a horizontal
     * scrollbar inside the panel. */
    overflow-x: hidden;
    overscroll-behavior: contain;
}

.conversation-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

/* Items use flex (not grid) because the sidebar search filter toggles
 * `display: flex` on them in messenger-utils.js. */
.conversation-item {
    position: relative;
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    min-height: 68px;
    margin-bottom: 0;
    padding: 9px;
    border: 1px solid transparent;
    border-radius: 15px;
    text-decoration: none;
    color: inherit;
    cursor: pointer;
    /* iOS tap hygiene for the row: no system gray highlight box (it renders
     * over the full 68px row and reads as extra height above the 46px
     * avatar), no long-press text selection / magnifier, no touch callout.
     * The app's own :hover/:active feedback (row wash + press translate)
     * stays — this only kills the OS-level overlays. */
    -webkit-tap-highlight-color: transparent;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
    transition:
        background var(--hm-ease),
        border-color var(--hm-ease),
        transform var(--hm-ease);
}

.conversation-item:hover {
    background: color-mix(in srgb, var(--ms-surface) 62%, transparent);
}

/* Selected conversation: a soft primary tint marks the open chat — the one
 * accent-colored state in the sidebar, so it reads instantly against the
 * neutral hover wash. The name/time overrides below step up the contrast on
 * the tint and are placed after the unread rules so they win the cascade. */
.conversation-item.is-active {
    border-color: transparent;
    background: var(--hm-primary-tint);
}

/* Unread chat treatment (WhatsApp-style hierarchy): a conversation with
 * unread messages gets a navy-tinted preview and the primary fill on its
 * unread pill so "what needs attention" reads at a glance. The name itself
 * is uniform across every row — full ink at weight 600, no read/unread
 * color split — so the name color never carries state; the preview tint
 * and pill do. No background wash on the row itself. Driven purely by the
 * row's data-unread-count attribute, so it stays in sync with every client
 * update path (poll, open, archive) with zero extra JS. */
.conversation-item[data-unread-count="0"] .conversation-item__preview {
    color: var(--hm-muted);
}

.conversation-item[data-unread-count="1"] .conversation-item__name,
.conversation-item[data-unread-count="2"] .conversation-item__name,
.conversation-item[data-unread-count="3"] .conversation-item__name,
.conversation-item[data-unread-count="4"] .conversation-item__name,
.conversation-item[data-unread-count="5"] .conversation-item__name {
    /* Unread: same weight as read rows — the navy ink alone carries the
     * emphasis (no full-name bolding). */
    color: var(--hm-navy);
    font-weight: 600;
}

.conversation-item[data-unread-count="1"] .conversation-item__preview,
.conversation-item[data-unread-count="2"] .conversation-item__preview,
.conversation-item[data-unread-count="3"] .conversation-item__preview,
.conversation-item[data-unread-count="4"] .conversation-item__preview,
.conversation-item[data-unread-count="5"] .conversation-item__preview {
    color: var(--hm-navy-soft);
}

.conversation-item[data-unread-count="1"] .conversation-item__badge,
.conversation-item[data-unread-count="2"] .conversation-item__badge,
.conversation-item[data-unread-count="3"] .conversation-item__badge,
.conversation-item[data-unread-count="4"] .conversation-item__badge,
.conversation-item[data-unread-count="5"] .conversation-item__badge {
    /* White text on the primary fill — white would fail contrast on a dark
     * primary, so this is driven by the configured on-primary color. */
    color: var(--hm-on-primary);
    background: var(--hm-primary-bg);
}

/* Active-row text: the name steps to full ink and the time to the soft ink
 * level, keeping both clearly readable on the primary tint. */
.conversation-item.is-active .conversation-item__name {
    color: var(--hm-navy);
}

.conversation-item.is-active .conversation-item__time {
    color: var(--hm-navy-soft);
}

/* List-level archive/unarchive control (WhatsApp-style): a compact circular
 * button that fades in over the right edge of a conversation row on hover, so
 * a chat can be archived straight from the list without opening it. Solid
 * surface + hairline + shadow keep it legible over the row's time/badge; the
 * icon pair swaps via the row's data-archived flag (arrow into the box =
 * archive, arrow out = unarchive). Hidden on touch devices (no hover), where
 * the open chat's menu remains the archive path. Scoped to the row so the
 * selectors beat the global [data-tooltip] anchor (position: relative) that
 * the tooltip recipe applies to every tooltipped element. */
.conversation-item .conversation-item__archive {
    position: absolute;
    right: 8px;
    top: 50%;
    transform: translateY(-50%);
    z-index: 2;
    display: grid;
    place-items: center;
    width: 30px;
    height: 30px;
    padding: 0;
    border: 1px solid var(--hm-line);
    border-radius: 50%;
    color: var(--hm-muted);
    background: var(--hm-surface-solid);
    cursor: pointer;
    opacity: 0;
    pointer-events: none;
    transition:
        opacity var(--hm-ease),
        color var(--hm-ease),
        background var(--hm-ease);
}

.conversation-item .conversation-item__archive svg {
    display: none;
}

.conversation-item[data-archived="0"] .conversation-item__archive-icon,
.conversation-item[data-archived="1"] .conversation-item__unarchive-icon {
    display: block;
}

.conversation-item .conversation-item__archive:hover,
.conversation-item .conversation-item__archive:focus-visible {
    color: var(--hm-blue-strong);
    background: var(--hm-surface-blue);
}

/* Keyboard users see the control on focus even without a pointer; the row
 * hover reveal below is desktop-only (hover-capable pointers). */
.conversation-item .conversation-item__archive:focus-visible {
    opacity: 1;
    pointer-events: auto;
}

@media (hover: hover) {
    .conversation-item:hover .conversation-item__archive {
        opacity: 1;
        pointer-events: auto;
    }

    /* The control replaces the time/badge column while the pointer is over
     * the row (same trade-off as the chat bubbles' reply arrow). */
    .conversation-item:hover .conversation-item__meta {
        opacity: 0;
    }
}

/* Swipe-to-reveal row actions (mobile): every conversation row lives in a
 * .conversation-item-wrap holding the Archive/Delete action layer BEHIND the
 * row. A left swipe (or mouse drag) translates the row over, exposing the
 * actions in the vacated space; the solid background keeps the sliding row
 * from showing the row beneath. The wrapper stays overflow-visible so the
 * hover button's tooltip (which extends below the row) is never clipped, and
 * touch-action: pan-y hands vertical scrolling to the browser while
 * horizontal gestures reach the JS. */
.conversation-item-wrap {
    --hm-swipe-actions-w: 144px;
    position: relative;
    border-radius: 15px;
    touch-action: pan-y;
    -webkit-user-select: none;
    user-select: none;
}

.conversation-item-wrap .conversation-item {
    z-index: 1;
    /* A slightly longer, decelerating curve for the swipe snap so the row
     * glides open/closed instead of easing abruptly (dragging itself runs
     * with no transition and follows the finger 1:1). */
    transition:
        transform 220ms cubic-bezier(0.22, 0.61, 0.36, 1),
        background var(--hm-ease),
        border-color var(--hm-ease);
}

/* A row's floating tooltip (archive/unarchive buttons) points DOWNWARD and
 * overlaps the row below it. Without this lift the tooltip's own z-index is
 * trapped inside the row's z-index: 1 stacking context, so the NEXT row
 * (also z-index: 1, later in the DOM) paints over it — the neighboring time
 * label covers the bubble. Making the whole wrap a z-index: 3 stacking
 * context while it is hovered/focused keeps the row and its tooltip on top
 * of every other row. (Upward tooltips like the muted bell are unaffected.) */
.conversation-item-wrap:hover,
.conversation-item-wrap:focus-within {
    z-index: 3;
}

.conversation-item-wrap.is-swiped .conversation-item {
    transform: translateX(calc(-1 * var(--hm-swipe-actions-w)));
    background: var(--hm-surface-solid);
}

/* The action layer behind the row: two colored panels revealed by the swipe.
 * Deliberately NOT overflow-clipped so the buttons' hover tooltips (which
 * extend below the row) stay visible — the rightmost button rounds its own
 * outer corners instead. */
.conversation-item__actions {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    width: var(--hm-swipe-actions-w);
    display: flex;
    opacity: 0;
    visibility: hidden;
    transition: opacity 120ms ease;
}

.conversation-item-wrap.is-swiped .conversation-item__actions {
    opacity: 1;
    visibility: visible;
}

.conversation-item__action {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    flex: 1;
    min-width: 0;
    padding: 0;
    border: 0;
    color: var(--hm-on-primary);
    font-family: inherit;
    font-size: 0.62rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    cursor: pointer;
    transition: filter var(--hm-ease);
}

.conversation-item__action:hover {
    filter: brightness(1.08);
}

.conversation-item__action--archive {
    background: var(--hm-blue-strong);
}

.conversation-item__action--delete {
    background: var(--hm-red);
    border-radius: 0 15px 15px 0;
}

/* Only one icon and one label show at a time, driven by the archive action's
 * data-archived flag (arrow into the box = archive, out = unarchive). */
.conversation-item__action svg {
    display: none;
    flex: 0 0 auto;
}

.conversation-item__action--delete .conversation-item__action-icon {
    display: block;
}

.conversation-item__action--archive[data-archived="0"] .conversation-item__archive-icon,
.conversation-item__action--archive[data-archived="1"] .conversation-item__unarchive-icon {
    display: block;
}

.conversation-item__action--archive .conversation-item__action-label--unarchive {
    display: none;
}

.conversation-item__action--archive[data-archived="1"] .conversation-item__action-label--archive {
    display: none;
}

.conversation-item__action--archive[data-archived="1"] .conversation-item__action-label--unarchive {
    display: block;
}

.conversation-item__avatar-wrap {
    position: relative;
    flex: 0 0 auto;
    /* Pin the wrap's box so the absolutely-positioned presence dot always has
     * a well-defined containing block. WebKit (iOS Safari/Chrome) has a known
     * quirk where an absolute child of an implicitly flex-sized wrapper is
     * anchored to the wrong spot (top-left) — explicit dimensions give the
     * dot a stable anchor on first layout, independent of image load timing. */
    width: 46px;
    height: 46px;
}

.conversation-item__avatar {
    display: block;
    width: 46px;
    height: 46px;
    border-radius: 50%;
    object-fit: cover;
    flex-shrink: 0;
}

.conversation-item__avatar-wrap .hm-avatar-initial {
    width: 46px;
    height: 46px;
}

.conversation-item__status {
    position: absolute;
    right: 1px;
    bottom: 1px;
    width: 10px;
    height: 10px;
    border: 2px solid var(--hm-secondary-bg);
    border-radius: 50%;
    background: var(--hm-green);
}

.conversation-item__status.is-offline {
    background: var(--hm-faint);
}

.conversation-item__body {
    display: flex;
    flex-direction: column;
    gap: 3px;
    min-width: 0;
    flex: 1;
}

.conversation-item__name {
    display: block;
    overflow: hidden;
    color: var(--hm-navy);
    font-size: 0.9rem;
    font-weight: 600;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Latest-activity preview line: the text is truncated server-side; the
 * ellipsis here is only a backstop for the remaining sliver. Flex row so the
 * read-receipt ticks sit to the right of the truncated text without pushing
 * it out of line. Empty strings render as an empty line so the two-line
 * name/preview block keeps its height across conversations. */
.conversation-item__preview {
    display: flex;
    align-items: center;
    gap: 4px;
    overflow: hidden;
    color: var(--hm-muted);
    /* One step below the 0.9rem name, so the preview reads as supporting
     * text while the row stays compact. */
    font-size: 0.82rem;
    line-height: 1.3;
}

.conversation-item__preview-text {
    overflow: hidden;
    min-width: 0;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Read-receipt ticks (WhatsApp-style) for the current user's own last
 * message, shown LEADING the preview line (before the text): a single check
 * in the muted grey until the other participant reads it, then the second
 * check joins on its right in the accent blue. The single state renders ONLY
 * the one check (narrow viewBox, so no dead space trails it); the read state
 * re-renders as the two-check pair, with the extra check fading in. The SVG
 * sizes from its height, so the base check is pixel-identical in both states
 * — the flip is a fade-in, never a shift or a resize. */
.conversation-item__ticks {
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
    margin-right: 2px;
    color: var(--hm-faint);
}

.conversation-item__ticks-icon {
    display: block;
    height: 10px;
    width: auto;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* The second check's fade-in plays ONLY on a genuine read flip — JS adds
 * .is-flipping when it rebuilds the tick pair (see applyReadCursors / the
 * readChanged path of applyPreviewMeta). Every other rebuild of the row
 * (list hydration, Archives toggle, search clear, offline shell boot, page
 * load) must land at the final state: an unconditional animation would
 * replay the fade on each re-render, so an already-read conversation's
 * second tick visibly blinks. */
.conversation-item__ticks.is-flipping .conversation-item__ticks-check--second {
    animation: conversationTickFadeIn 150ms ease;
}

.conversation-item__ticks.is-read {
    color: var(--hm-blue-strong);
}

@keyframes conversationTickFadeIn {
    from {
        opacity: 0;
    }

    to {
        opacity: 1;
    }
}

/* Right-hand status column (Helheim-style): time top-right, unread badge
 * directly beneath it. */
.conversation-item__meta {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 5px;
    flex-shrink: 0;
    padding-top: 2px;
}

/* Right-hand status row that groups the muted bell and the unread badge on
 * one line — a muted conversation with an unread count now shows both side
 * by side instead of the bell pushing the badge down (they used to stack
 * vertically under the time). */
.conversation-item__badges {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 6px;
    flex-shrink: 0;
}

.conversation-item__meta .conversation-item__time {
    display: block;
    overflow: hidden;
    color: var(--hm-muted);
    font-size: 0.72rem;
    line-height: 1.2;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.conversation-item__badge {
    display: grid;
    place-items: center;
    flex-shrink: 0;
    min-width: 21px;
    height: 21px;
    padding: 0 6px;
    border-radius: 999px;
    color: var(--hm-on-primary);
    font-size: 0.64rem;
    font-weight: 700;
    line-height: 1;
    background: var(--hm-blue-strong);
}

/* The badge is display:grid, so the UA's [hidden] default (display:none)
 * would be overridden — hide explicitly like the muted bell does. */
.conversation-item__badge[hidden] {
    display: none;
}

/* Archives badge — a right-aligned pill on the Archives entry row (the shared
 * .conversation-item__badge pill stays the base; this rule sets its spacing).
 * The flex row pushes it to the far right via margin-left:auto. */
.messenger-sidebar__archives-btn .messenger-archives-badge {
    margin: 0 0 0 auto;
    min-width: 19px;
    height: 19px;
    padding: 0 5px;
    font-size: 0.6rem;
}

/* "Back to chats" link above the section label on the Archives page. It
 * stays a lighter, more compact secondary action (34px tall) with a quiet
 * frosted-glass fill and hairline edge, settling into a stronger tint on
 * hover. The list area's own 8px side padding is folded into 4px side margins
 * (8 + 4 = 12px); the width compensates for those two 4px margins so the row
 * keeps the full content width. */
/* "Back to chats" row atop the archived list — a plain text row: the
 * accent-tinted arrow chip + bold navy label, no card, no border, no hover
 * fill. Quiet, like a simple navigation link. */
.messenger-sidebar__back {
    display: flex;
    align-items: center;
    gap: 9px;
    min-height: 42px;
    margin: 0 0 7px;
    padding: 0 0 0 4px;
    /* 4px + the list area's 8px = 12px, the search pill's left edge */
    color: var(--hm-navy);
    font-family: inherit;
    font-size: 0.9rem;
    font-weight: 600;
    text-align: left;
    text-decoration: none;
}

.messenger-sidebar__back-icon {
    display: grid;
    place-items: center;
    width: 42px;
    height: 42px;
    flex: 0 0 auto;
    border-radius: 50%;
    color: var(--hm-blue-strong);
    background: var(--hm-primary-tint);
}

.messenger-sidebar__back:hover,
.messenger-sidebar__back:focus-visible {
    color: var(--hm-blue-strong);
    outline: none;
    text-decoration: none;
}

/* Per-chat sound mute indicator (bell-off), shown under the relative time.
 * The hidden attribute is toggled client-side, so the explicit [hidden]
 * rule keeps it off even though the element is a grid. */
.conversation-item__muted {
    display: inline-grid;
    place-items: center;
    color: var(--hm-muted);
    line-height: 0;
}

.conversation-item__muted[hidden] {
    display: none;
}

.conversation-list__empty {
    padding: 2rem 1rem;
    text-align: center;
    color: var(--hm-muted);
    font-size: 0.85rem;
}

/* The hidden attribute must win over the element's default block display when
 * the search "No matches." placeholder / "No conversations yet" state is
 * toggled (same pattern as the infinite-scroll loader below). */
.conversation-list__empty[hidden] {
    display: none;
}

/* Infinite-scroll loader at the bottom of the conversation list. Kept in the
 * layout (so scroll-distance checks keep working without extra nodes) but
 * invisible until a batch is actually being fetched. */
.conversation-list__loader {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 8px 0 4px;
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--hm-ease);
}

.conversation-list__loader.is-loading {
    opacity: 1;
}

.conversation-list__loader[hidden] {
    display: none;
}

/* The bottom-of-list loader uses the same bouncing dots as the chat
 * (shared .hm-loading-dots) — one activity language everywhere. Slightly
 * larger dots so they read at list scale. */
.conversation-list__loader .hm-loading-dots span {
    width: 5px;
    height: 5px;
}

/* 4.4 · Account menu & dropdown */

/* Account menu ("..." options button) in the header. The button itself is a
 * shared .icon-button (same circular recipe as the chat header toggle), so only
 * the dropdown wrapper needs its own rule. */
.messenger-sidebar__dropdown {
    position: relative;
    flex: 0 0 auto;
}

/* The Security Protocol shield sits next to the "..." menu and uses the
 * same circular icon-button recipe, so the pair reads as one control group.
 * The slight negative margin pulls them closer than the header's uniform
 * 10px gap — two adjacent icons need tighter rhythm than icon-to-text
 * spacing. On the page itself the icon tints primary so the current
 * section is obvious at a glance. */
.messenger-sidebar__security {
    margin-right: -6px;
}

.messenger-sidebar__security.is-active,
.messenger-sidebar__security.is-active:hover,
.messenger-sidebar__security.is-active:focus-visible {
    color: var(--ms-accent);
    border-color: var(--hm-line);
    background: color-mix(in srgb, var(--ms-surface) 76%, transparent);
}

/* Global HM dropdown component — every Bootstrap dropdown in the app shares
 * this floating-glass recipe: the header account menus, the profile avatar
 * photo menu, the chat header menu and the admin invitations split all use
 * the same .dropdown-menu / .dropdown-item look. Only the circular "..." /
 * icon-button toggles hide Bootstrap's caret (the admin split button keeps
 * its own). */
.icon-button.dropdown-toggle::after,
.messenger-profile__avatar-edit-btn::after {
    display: none;
}

.dropdown-menu {
    min-width: 180px;
    margin-top: 6px !important;
    padding: 6px;
    border: 1px solid var(--hm-line-strong);
    border-radius: 14px;
    background: var(--ms-surface);
    /* A soft lift separates the menu from the page; the only shadow in the
     * app apart from the new-chat FAB glow — menus float, cards don't. */
    box-shadow: 0 10px 24px -10px rgba(15, 23, 42, 0.22);
}

/* Opening pop — opacity only, because Bootstrap/Popper positions the menu via
 * an inline transform (animating transform would make it jump to 0,0). */
.dropdown-menu.show {
    animation: hm-menu-fade 140ms ease-out;
}

.dropdown-item {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 10px 12px;
    border: 0;
    border-radius: 9px;
    color: var(--hm-text);
    font-size: 0.85rem;
    font-weight: 600;
    font-family: inherit;
    text-align: left;
    text-decoration: none;
    background: transparent;
    cursor: pointer;
    transition:
        background var(--hm-ease),
        transform var(--hm-ease);
}

.dropdown-item:hover,
.dropdown-item.active {
    color: var(--hm-text);
    background: var(--hm-primary-tint);
}

/* Bootstrap's default .dropdown-item:active paints the item blue with white
 * text while the pointer is down. Keep the HM hover wash + text color instead,
 * so clicking feels the same as hovering. */
.dropdown-item:active {
    color: var(--hm-text);
    background: var(--hm-primary-tint);
}

/* Destructive actions are visually separated from the safe items above them
 * by a hairline — a quiet, professional divider that needs no extra markup. */
.dropdown-item.logout-item,
a.dropdown-item[href*="logout"] {
    margin-top: 6px;
    padding-top: 11px;
    border-top: 1px solid var(--hm-line);
    color: var(--hm-red);
}

.dropdown-item.logout-item:hover,
a.dropdown-item[href*="logout"]:hover,
.dropdown-item.logout-item:active,
a.dropdown-item[href*="logout"]:active,
.dropdown-item.logout-item.active,
a.dropdown-item[href*="logout"].active {
    background: var(--hm-danger-tint);
}

.dropdown-item--danger {
    margin-top: 6px;
    padding-top: 11px;
    border-top: 1px solid var(--hm-line);
    color: var(--hm-red);
}

.dropdown-item--danger:hover,
.dropdown-item--danger:active,
.dropdown-item--danger.active {
    background: var(--hm-danger-tint);
}

/* 4.5 · Shared components */

/* Global avatar-initial recipe — the no-picture fallback shared by every user
 * avatar (sidebar conversation items, chat header, profile page, admin tables,
 * new-chat search). Filled with the secondary color (driven by secondaryColor
 * in app/Config/Messenger.php — change one hex, every placeholder re-tints)
 * plus a strong hairline, so every placeholder is identical. Sizing: fills a
 * sized wrapper (100%) unless a context rule pins explicit dimensions (e.g.
 * .conversation-item__avatar-wrap .hm-avatar-initial, .admin-table
 * .hm-avatar-initial). */
.hm-avatar-initial {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    overflow: hidden;
    border: 1px solid var(--hm-line-strong);
    border-radius: 50%;
    background: var(--hm-secondary-bg);
    color: var(--hm-navy);
    font-size: 1.15rem;
    font-weight: 700;
    font-family: inherit;
    line-height: 1;
    flex-shrink: 0;
    -webkit-user-select: none;
    /* Safari 15 and older */
    user-select: none;
}

/* Identity-line class shared by the chat + admin sidebar headers (chat shows
 * the current user's role, admin the "ADMIN PANEL" wordmark — both uppercase
 * small-caps labels). The look itself comes from .brand-copy .brand-role. */
.messenger-sidebar__user-name {
    margin: 0;
    line-height: 1.3;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Shared circular icon button (Helheim signature control) — used by the
 * chat header actions and the composer attach button. */
.icon-button {
    display: grid;
    place-items: center;
    width: 42px;
    height: 42px;
    flex: 0 0 auto;
    border: 1px solid transparent;
    border-radius: 50%;
    color: var(--hm-blue);
    background: transparent;
    cursor: pointer;
    transition:
        color 150ms ease,
        background 150ms ease,
        border-color 150ms ease,
        transform 150ms ease;
}

.icon-button:hover,
.icon-button:focus-visible,
.icon-button.show {
    color: var(--hm-navy);
    border-color: var(--hm-line);
    background: color-mix(in srgb, var(--ms-surface) 76%, transparent);
    outline: none;
}

/* Global card surface (Helheim "profile-card / settings-card" recipe). Styling
 * lives here once and is reused wherever a content card is needed, so pages
 * never restyle cards individually. */
.hm-card {
    padding: 22px;
    border: 1px solid var(--hm-line);
    border-radius: 22px;
    background: var(--hm-surface);
}

/* Global data-table card (Helheim "data card" recipe) — wraps a table in the
 * same glass surface as .hm-card but padding-less, so the header band and rows
 * run edge to edge inside rounded corners. Put a .table-responsive (for
 * horizontal scroll on small screens) between it and the <table>. */
.hm-table-card {
    overflow: hidden;
    border: 1px solid var(--hm-line);
    border-radius: 22px;
    background: var(--hm-surface);
}

/* Global table styling (Helheim recipe) — replaces Bootstrap's flat .table with
 * the app's micro-label header band, hairline row dividers and a soft hover
 * wash. Not built on .table/.table-hover so every border/padding is ours.
 * Cells stay on a single line (nowrap) so rows keep their compact height; the
 * views wrap the table in .table-responsive for horizontal overflow. */
.hm-table {
    width: 100%;
    margin-bottom: 0;
    color: var(--hm-text);
    font-size: 0.85rem;
    white-space: nowrap;
}

.hm-table thead th {
    padding: 13px 20px;
    color: var(--hm-muted);
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.09em;
    text-align: left;
    text-transform: uppercase;
    white-space: nowrap;
    background: var(--hm-surface-soft);
    border-bottom: 1px solid var(--hm-line-strong);
}

.hm-table tbody td {
    padding: 14px 20px;
    border-bottom: 1px solid var(--hm-line);
    vertical-align: middle;
}

.hm-table tbody tr:last-child td {
    border-bottom: 0;
}

.hm-table tbody tr {
    transition: background var(--hm-ease);
}

.hm-table tbody tr:hover {
    background: var(--hm-primary-tint-soft);
}

/* Secondary/muted cell (dates etc.) inside a .hm-table. */
.hm-table__cell {
    color: var(--hm-muted);
    font-size: 0.82rem;
}

/* Column-width helper (mirrors the template's th-1 recipe): width:1% makes the
 * column hug its content (badges, dates, action buttons) instead of stretching,
 * so the primary text column expands and right-aligned columns sit properly.
 * Combine with .text-end for the right-aligned columns. */
.hm-table th.hm-col-min,
.hm-table td.hm-col-min {
    width: 1%;
    white-space: nowrap;
}

/* Counterpart of .hm-col-min: makes a column the expanding left content column
 * (Role/Description/User …). width:100% lets it absorb all leftover space, so a
 * table that has two content columns stays stable instead of the browser juggling
 * widths from the row data ("randomly adjusted" columns). */
.hm-table th.hm-col-grow,
.hm-table td.hm-col-grow {
    width: 100%;
}

/* Table avatars render at 46px — consistent with the app's other 46px avatar
 * sizes (sidebar items, chat header). The shared avatar partial only applies
 * context classes to the <img> — its initial fallback is a bare
 * .hm-avatar-initial that sizes itself from a wrapper, so pin the dimensions
 * here by the table. */
.hm-table .hm-avatar-initial,
.hm-table__avatar {
    width: 46px;
    height: 46px;
    flex: 0 0 auto;
}

.hm-table__avatar {
    border-radius: 50%;
    object-fit: cover;
}

/* Stacked identity cell used by the Users + Sessions tables: avatar, then the
 * display name over the mono user id (the design reserves mono for IDs). */
.hm-table__user-name {
    color: var(--hm-navy);
    font-size: 0.9rem;
    font-weight: 700;
}

.hm-table__user-id {
    color: var(--hm-faint);
    font-family: var(--hm-font-mono);
    font-size: 0.75rem;
}

/* Soft status pill (Helheim badge recipe) — a tinted, uppercase micro-label.
 * Semantic tints via modifiers; the palette stays in the --hm-* tokens. */
.hm-badge {
    display: inline-flex;
    align-items: center;
    padding: 4px 10px;
    border-radius: 999px;
    font-size: 0.66rem;
    font-weight: 700;
    letter-spacing: 0.05em;
    line-height: 1.4;
    text-transform: uppercase;
    white-space: nowrap;
}

.hm-badge--green {
    color: color-mix(in srgb, var(--ms-success) 55%, var(--hm-text));
    background: var(--hm-success-tint);
}

.hm-badge--blue {
    color: var(--hm-blue-strong);
    background: color-mix(in srgb, var(--hm-blue-strong) 14%, transparent);
}

.hm-badge--muted {
    color: var(--hm-muted);
    background: var(--hm-muted-tint);
}

.hm-badge--warning {
    color: color-mix(in srgb, var(--ms-warning) 55%, var(--hm-text));
    background: var(--hm-warning-tint);
}

.hm-badge--red {
    color: color-mix(in srgb, var(--ms-danger) 55%, var(--hm-text));
    background: var(--hm-danger-tint-strong);
}

.hm-badge--builtin {
    color: var(--hm-blue-strong);
    background: color-mix(in srgb, var(--hm-blue-strong) 14%, transparent);
}

/* Global pagination (Helheim look) — the app's one pagination component.
 * Rendered by the shared partials/pagination.php (CI4 Pager template 'hm_full'),
 * so every admin listing reuses this instead of styling its own pager.
 * Structure: an optional "Showing X–Y of Z" summary on the left and the page
 * controls on the right; the controls wrap on narrow screens. */
.hm-pagination {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    padding: 16px 20px;
    border-top: 1px solid var(--hm-line);
}

.hm-pagination__summary {
    margin: 0;
    color: var(--hm-muted);
    font-size: 0.78rem;
}

/* The controls always sit at the right edge of the footer (with or without a
 * summary); on narrow screens the whole footer wraps. */
.hm-pagination__controls {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
    margin-left: auto;
}

/* Page button — the compact sibling of the .btn family (38px rounded square,
 * muted text on a white glass wash, hairline border). The .is-active state
 * keeps the bright green accent with dark ink (a small page chip, not a
 * labeled CTA); the disabled nav ends mirror the .btn:disabled recipe. */
.hm-pagination__link {
    display: inline-grid;
    place-items: center;
    min-width: 38px;
    height: 38px;
    padding: 0 12px;
    border: 1px solid var(--hm-line);
    border-radius: 12px;
    color: var(--hm-muted);
    font-size: 0.82rem;
    font-weight: 700;
    text-decoration: none;
    background: color-mix(in srgb, var(--ms-surface) 72%, transparent);
    cursor: pointer;
    transition:
        color var(--hm-ease),
        background var(--hm-ease),
        border-color var(--hm-ease),
        transform var(--hm-ease);
}

.hm-pagination__link:hover,
.hm-pagination__link:focus-visible {
    color: var(--hm-navy);
    border-color: var(--hm-line-strong);
    background: var(--ms-surface);
    outline: none;
}

.hm-pagination__link.is-active {
    border-color: transparent;
    color: var(--hm-on-primary);
    background: var(--hm-primary-bg);
    cursor: default;
}

.hm-pagination__link.is-disabled {
    color: var(--hm-faint);
    border-color: var(--hm-line);
    background: color-mix(in srgb, var(--ms-surface) 45%, transparent);
    box-shadow: none;
    cursor: default;
}

/* Icon-only prev/next buttons — square instead of pill so the chevron centers. */
.hm-pagination__link--nav {
    width: 38px;
    padding: 0;
}

/* Non-interactive ellipsis between the numbered groups on long lists. */
.hm-pagination__dots {
    display: inline-grid;
    place-items: center;
    min-width: 22px;
    height: 38px;
    color: var(--hm-faint);
    font-size: 0.82rem;
    letter-spacing: 0.08em;
    -webkit-user-select: none;
    /* Safari 15 and older */
    user-select: none;
}

/* Global content header (Helheim "panel-header" recipe) — the standard header
 * for content pages (Profile, Preferences, admin…): title, optional subtitle
 * and a mobile-only back chevron. */
.hm-page-header {
    position: relative;
    z-index: 2;
    display: flex;
    align-items: center;
    gap: 12px;
    min-height: var(--hm-header-height);
    padding: 10px 20px;
    border-bottom: 1px solid var(--hm-line);
    background: var(--ms-surface);
}

/* Back chevron, used by both the generic page header and the chat panel
 * header (5.2): on desktop the sidebar provides navigation, so it only
 * appears on small screens (see the mobile media query). */
.hm-page-header__back,
.messenger-chat__back {
    display: none;
    /* Compact back arrow — match admin-sidebar__close styling:
     * 36×36px circle, transparent bg, shows bg-color on hover/focus. */
    width: 36px;
    height: 36px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: var(--hm-faint);
    cursor: pointer;
    transition: color var(--hm-ease), background var(--hm-ease);
    flex: 0 0 auto;
}

.hm-page-header__back:hover,
.hm-page-header__back:focus-visible,
.messenger-chat__back:hover,
.messenger-chat__back:focus-visible {
    color: var(--hm-navy);
    background: var(--hm-primary-tint);
}

.hm-page-header__copy {
    min-width: 0;
}

.hm-page-header__title {
    margin: 0;
    color: var(--hm-navy);
    font-size: 1.04rem;
    font-weight: 700;
}

.hm-page-header__subtitle {
    margin: 3px 0 0;
    color: var(--hm-muted);
    font-size: 0.74rem;
}

/* On mobile, hide page header subtitles to save vertical space. */
@media (max-width: 575.98px) {
    .hm-page-header__subtitle {
        display: none;
    }
}

/* Global toggle switch (Helheim "switch" recipe): a 46×26 track whose knob
 * slides right; the ON track is the configured toggle color, the OFF track a
 * pale wash of the same hue. Built on a native checkbox with appearance:none
 * + role="switch", so it stays keyboard-focusable and accessible while
 * looking like the template's custom control. */
.hm-switch {
    position: relative;
    flex: 0 0 auto;
    width: 46px;
    height: 26px;
    margin: 0;
    border: 0;
    border-radius: 999px;
    background: var(--hm-toggle-track);
    cursor: pointer;
    appearance: none;
    -webkit-appearance: none;
    transition: background 160ms ease;
}

.hm-switch::after {
    content: "";
    position: absolute;
    top: 4px;
    left: 4px;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--hm-surface-solid);
    transition:
        transform 160ms ease,
        background 160ms ease;
}

.hm-switch:checked {
    background: var(--hm-toggle);
}

.hm-switch:checked::after {
    transform: translateX(20px);
    background: var(--hm-surface-solid);
}

.hm-switch:focus-visible {
    box-shadow: none;
}

html.hm-focus-border .hm-switch:focus-visible {
    outline: 2px solid #000;
    outline-offset: 2px;
}

/* 5 · Main panel */
/* The app's single main-content backdrop. Shared by the chat, profile,
 * preferences and admin layouts, so panels only need to be transparent
 * to show the same background everywhere. */
.messenger-main {
    flex: 1;
    display: flex;
    flex-direction: column;
    min-width: 0;
    /* A flex column's default min-height is `auto`, which keeps children at
     * their content height — that makes the inner scroll containers
     * (.messenger-profile__body, .messenger-chat__messages…) unable to
     * shrink, so their overflow never engages and the panel's bottom content
     * is cut off instead of scrollable (the logout section at the end of
     * Profile/Preferences was unreachable). `min-height: 0` lets them size
     * to the viewport and scroll internally. */
    min-height: 0;
}

/* 5.1 · Empty state */

/* Empty state — shown when no conversation is open. A centered identity
 * moment built from the shared tokens: the rune-circle avatar recipe
 * (--hm-avatar-bg + strong hairline) holds an SVG chat glyph, under a
 * navy title and muted description. Purely presentational — visibility is
 * toggled from messenger.js via #emptyState. */
.messenger-empty {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 2rem;
    text-align: center;
}

.messenger-empty__icon {
    width: 84px;
    height: 84px;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-bottom: 1.25rem;
    border: 1px solid var(--hm-line-strong);
    border-radius: 50%;
    color: var(--hm-blue-strong);
    background: var(--hm-avatar-bg);
}

.messenger-empty__title {
    margin: 0 0 0.5rem;
    color: var(--hm-navy);
    font-size: 1.35rem;
    font-weight: 700;
    letter-spacing: 0.01em;
}

.messenger-empty__text {
    max-width: 320px;
    margin: 0;
    color: var(--hm-muted);
    font-size: 0.86rem;
    line-height: 1.55;
}

/* 5.2 · Chat panel & header */

.messenger-chat {
    flex: 1;
    display: flex;
    flex-direction: column;
    min-height: 0;
    /* Transparent here: the panel shows the shared layout surface behind it. */
}

.messenger-chat__header {
    position: relative;
    z-index: 10;
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: var(--hm-header-height);
    padding: 9px 14px;
    border-bottom: 1px solid var(--hm-line);
    background: var(--ms-surface);
}

/* Back arrow — its look is shared with .hm-page-header__back (defined in
 * 4.5 Shared components); hidden on desktop, revealed on mobile. */

.messenger-chat__avatar {
    position: relative;
    width: 46px;
    height: 46px;
    border-radius: 50%;
    flex-shrink: 0;
    display: block;
}

.messenger-chat__avatar img {
    width: 100%;
    height: 100%;
    border-radius: 50%;
    object-fit: cover;
    display: block;
}

/* Presence dot on the header avatar (mirrors the sidebar avatar dot); the
 * ring matches the avatar's secondary fill so the dot reads as a notch. */
.messenger-chat__avatar::after {
    content: "";
    position: absolute;
    right: 1px;
    bottom: 1px;
    width: 11px;
    height: 11px;
    border: 2px solid var(--hm-secondary-bg);
    border-radius: 50%;
    background: var(--hm-green);
}

.messenger-chat__avatar.is-offline::after {
    background: var(--hm-faint);
}

/* The account-free offline shell starts with an EMPTY chat header avatar
 * (.is-empty, see chat_panel.php — the cached conversation fills it after
 * boot): hide the presence dot too, so the blank slot never reads as a
 * placeholder while loading. messenger.js drops .is-empty the moment the
 * real avatar is rendered. */
.messenger-chat__avatar.is-empty::after {
    display: none;
}

.messenger-chat__title-wrap {
    flex: 1;
    min-width: 0;
}

.messenger-chat__title {
    overflow: hidden;
    margin: 0;
    color: var(--hm-navy);
    font-size: 1rem;
    font-weight: 700;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.messenger-chat__status {
    margin: 3px 0 0;
    color: var(--hm-green);
    font-size: 0.71rem;
}

.messenger-chat__status.is-offline {
    color: var(--hm-faint);
}

.messenger-chat__timer-badge {
    display: grid;
    place-items: center;
    flex-shrink: 0;
    padding: 0;
    border: 0;
    color: var(--hm-red);
    background: transparent;
    cursor: default;
    transition: color 150ms ease;
}

.messenger-chat__timer-badge:hover,
.messenger-chat__timer-badge:focus-visible {
    color: var(--hm-red-deep);
    outline: none;
}

/* Shared floating-glass hover tooltip. Any element carrying a data-tooltip
 * attribute (timer badge, mute toggle, chat menu, new-chat button, sidebar
 * muted bell, image-preview download) shows this popup on hover/focus — the
 * same recipe the disappearing-messages badge used, generalized so every icon
 * button gets the identical tooltip instead of a native title attribute.
 * The element anchors the popup via position: relative (added by the recipe);
 * placement defaults to below-right and can be overridden per component.
 * The server templates AND the JS label writers (messenger.js, messenger-utils.js)
 * keep data-tooltip in sync with the button's state (e.g. mute/unmute). */
[data-tooltip] {
    position: relative;
}

[data-tooltip]::after {
    content: attr(data-tooltip);
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    z-index: 200;
    width: max-content;
    max-width: 240px;
    padding: 8px 12px;
    border: 1px solid var(--hm-line-strong);
    border-radius: 10px;
    color: var(--hm-text);
    font-size: 0.72rem;
    font-weight: 600;
    line-height: 1.6;
    background: var(--ms-surface);
    opacity: 0;
    transform: translateY(-4px);
    pointer-events: none;
    transition:
        opacity 150ms ease,
        transform 150ms ease;
}

[data-tooltip]:hover::after,
[data-tooltip]:focus-visible::after {
    opacity: 1;
    transform: translateY(0);
}

/* Never render an empty bubble when the tooltip text is blank (e.g. before
 * window.lang has loaded). The hover rule only touches opacity/transform, so
 * display: none here stays in effect while the element is hovered. */
[data-tooltip=""]::after {
    display: none;
}

/* The sidebar muted bell sits at the bottom of a scrolling row list; open its
 * tooltip upward so it never overlaps the row below or clips against the
 * list's overflow edge. */
.conversation-item__muted::after {
    top: auto;
    bottom: calc(100% + 8px);
    right: 0;
}

.messenger-chat__actions {
    display: flex;
    align-items: center;
    gap: 2px;
    flex-shrink: 0;
}

/* Per-chat sound mute toggle (header bell). Swaps to the bell-off glyph and
 * the timer-badge accent while muted, so the state reads at a glance. */
.messenger-chat__mute .messenger-chat__mute-bell-off,
.messenger-chat__mute.is-muted .messenger-chat__mute-bell {
    display: none;
}

.messenger-chat__mute.is-muted .messenger-chat__mute-bell-off {
    display: block;
}

.messenger-chat__mute.is-muted {
    color: var(--hm-red);
}

.messenger-chat__mute.is-muted:hover,
.messenger-chat__mute.is-muted:focus-visible {
    color: var(--hm-red-deep);
}

/* Busy state for mute button (icon-button) — show only spinner, no label,
 * keep 42×42 circle, center the spinner. */
.messenger-chat__mute:has(.hm-btn-busy__spinner) {
    display: grid;
    place-items: center;
}

.messenger-chat__mute .hm-btn-busy__spinner {
    width: 1.25em;
    height: 1.25em;
    border-width: 2px;
}

.messenger-chat__mute .hm-btn-busy__label {
    display: none;
}

/* 5.3 · Messages & loading */

.messenger-chat__messages {
    position: relative;
    flex: 1;
    overflow-y: auto;
    /* Vertical-only scroll (long words/images must never add a horizontal
     * scrollbar inside the chat panel). */
    overflow-x: hidden;
    /* Prepend-and-restore handles positioning in loadOlder(); disable the
     * browser's native scroll anchoring so it doesn't double-adjust. */
    overflow-anchor: none;
    /* Keep our existing message padding (Phase 4 decision: don't inflate it
     * to match the template's airier spacing). */
    padding: 1.25rem;
    /* Same PWA-safe bottom as .messenger-profile__body: the newest message
     * stays reachable above the browser's standalone bottom chrome. */
    padding-bottom: calc(1.25rem + env(safe-area-inset-bottom, 0px));
    display: flex;
    flex-direction: column;
    overscroll-behavior: contain;
}

/* Native-scrollbar compensation: on desktop a classic scrollbar (~17px) sits
 * at the container's right edge, so when the stream overflows it eats into
 * the otherwise symmetric padding and the right side reads wider than the
 * left. Chat.js toggles `.has-scrollbar` when a vertical scrollbar is
 * actually shown and reserves that width (measured into --hm-scrollbar by
 * messenger-utils.js) out of the right padding, keeping the visual inset
 * equal on both sides. Gated to the desktop layout (hover-capable pointer
 * AND the desktop width breakpoint, where the base right padding is the
 * 1.25rem this calc pairs with): touch hides the scrollbar entirely and the
 * mobile layout switches to a 10px right padding, so neither should be
 * touched. */
@media (hover: hover) and (min-width: 768px) {
    .messenger-chat__messages.has-scrollbar {
        padding-right: calc(1.25rem - var(--hm-scrollbar, 0px));
    }
}

/* Loading indicator — shown while a conversation's message history loads.
 * Positioned as an overlay over the message area (anchored by the relative
 * container) so it stays dead-centre regardless of scroll position and never
 * shifts the layout when it appears or fades out. The ring reuses the global
 * HM busy-spin keyframes so every loading state in the app shares one rhythm.
 * No entrance/exit animation: a smooth fade on open looked like a blink on
 * fast conversations, so the loader simply appears and disappears. */
.messenger-loading {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    inset: 0;
    z-index: 6;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 16px;
    padding: 1.25rem;
    pointer-events: none;
}

/* Chat-open skeleton — a few message-shaped placeholder bubbles (theirs /
 * mine alternation, like a real thread) with a soft shimmer sweeping across.
 * Reads as "the thread is loading" at a glance, far more alive than a
 * spinner ring. */
.messenger-skeleton {
    display: flex;
    flex-direction: column;
    gap: 14px;
    width: min(420px, 88%);
}

.messenger-skeleton__row {
    display: flex;
}

.messenger-skeleton__row--theirs {
    justify-content: flex-start;
}

.messenger-skeleton__row--mine {
    justify-content: flex-end;
}

.messenger-skeleton__bubble {
    height: 38px;
    border-radius: 14px;
    background-color: var(--hm-secondary-bg);
    background-image: linear-gradient(100deg, transparent 30%, color-mix(in srgb, var(--ms-surface) 70%, transparent) 50%, transparent 70%);
    background-size: 200% 100%;
    animation: hm-skeleton-shimmer 1.5s ease-in-out infinite;
}

.messenger-skeleton__row--mine .messenger-skeleton__bubble {
    background-color: color-mix(in srgb, var(--ms-accent) 26%, transparent);
}

@keyframes hm-skeleton-shimmer {
    from {
        background-position: 200% 0;
    }

    to {
        background-position: -200% 0;
    }
}

.messenger-loading__text {
    margin: 0;
    color: var(--hm-muted);
    font-size: 0.78rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    text-align: center;
}

/* Scroll-up variant — shown at the top edge of the message area while older
 * history is being fetched. Compact horizontal row (spinner + label) so it
 * never covers the messages; pinned to the container top via inset overrides
 * so it stays visible while the prepend-and-restore keeps the scroll
 * anchored. The soft wash behind it keeps the label readable over bubbles. */
.messenger-loading--top {
    top: 0;
    right: 0;
    bottom: auto;
    left: 0;
    inset: 0 0 auto 0;
    z-index: 5;
    flex-direction: row;
    gap: 10px;
    /* Generous top spacing so the row breathes off the pane's edge instead
     * of hugging it; the wash keeps it legible over bubbles without a
     * fading edge (the old fade-to-transparent gradient is gone). */
    padding: 22px 1.25rem 14px;
    background: color-mix(in srgb, var(--ms-surface) 96%, transparent);
}

/* The scroll-up variant dropped its spinner — the three bouncing dots
 * (same .hm-loading-dots as the typing indicator) carry the activity. */

/* Day divider — separates messages by calendar date. */
.day-divider {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    margin: 1px 0 18px;
    color: var(--hm-faint);
    font-size: 0.63rem;
    font-weight: 700;
    letter-spacing: 0.13em;
    text-transform: uppercase;
}

.day-divider::before,
.day-divider::after {
    content: "";
    width: min(17vw, 96px);
    height: 1px;
    background: var(--hm-line-strong);
}

.day-divider::after {
    transform: rotate(180deg);
}

/* 5.4 · Message bubbles */

.messenger-bubble {
    position: relative;
    max-width: min(68%, 620px);
    margin-bottom: 0.45rem;
    padding: 0.55rem 0.85rem;
    border: 0;
    border-radius: 16px;
    line-height: 1.43;
    font-size: 0.9rem;
    /* Smooth wash out after a reply-target highlight clears (and in, when
     * the class lands). Only background-color animates; nothing else on the
     * bubble changes by transition. */
    transition: background-color 300ms ease;
}

/* WhatsApp-style hover action: a small down-chevron button anchored to the
 * bubble's bottom-right corner. Hidden until the bubble is hovered/focused
 * (hover-capable pointers only — see the media query below); clicking it
 * opens the same reply dropdown as the old tap-to-open (chat.js). The
 * timestamp fades out on hover so the chevron takes its place, exactly like
 * WhatsApp. On touch devices there is no hover, so the chevron stays hidden
 * and the bubble-tap handler keeps opening the menu instead. */
.messenger-bubble__actions {
    position: absolute;
    right: 2px;
    bottom: 0;
    z-index: 2;
    display: grid;
    place-items: center;
    width: 26px;
    height: 26px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    color: var(--hm-blue-strong);
    background: transparent;
    cursor: pointer;
    opacity: 0;
    pointer-events: none;
    transition:
        opacity 120ms ease,
        color 120ms ease,
        transform 120ms ease;
}

.messenger-bubble__actions:hover,
.messenger-bubble__actions:focus-visible {
    outline: none;
    color: var(--hm-navy);
    transform: scale(1.12);
}

/* Outgoing bubbles sit on the solid primary fill, so the actions chevron
 * uses the on-primary color (white), dimmed slightly on hover. No background
 * chip on either side: the arrow speaks for itself. */
.messenger-bubble--mine .messenger-bubble__actions {
    color: var(--hm-on-primary);
}

.messenger-bubble--mine .messenger-bubble__actions:hover,
.messenger-bubble--mine .messenger-bubble__actions:focus-visible {
    color: color-mix(in srgb, var(--hm-on-primary) 80%, transparent);
}

@media (hover: hover) {

    .messenger-bubble:hover .messenger-bubble__actions,
    .messenger-bubble__actions:focus-visible {
        opacity: 1;
        pointer-events: auto;
    }

    .messenger-bubble:hover .messenger-bubble__time {
        opacity: 0;
    }
}

.messenger-bubble__text {
    color: var(--hm-text);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

/* Outgoing bubbles are the solid primary fill, so their text, links,
 * timestamp and read ticks all flip to the on-primary color. */
.messenger-bubble--mine .messenger-bubble__text {
    color: var(--hm-on-primary);
}

/* The per-attachment caption sits directly on the bubble fill, so on an
 * outgoing bubble it must switch to the on-primary color like the message
 * text — the base rule's dark ink would read as unreadable dark-on-indigo. */
.messenger-bubble--mine .messenger-bubble__caption {
    color: var(--hm-on-primary);
}

.messenger-bubble--mine .messenger-bubble__text a {
    color: var(--hm-on-primary);
}

.messenger-bubble--mine .messenger-bubble__text a:hover,
.messenger-bubble--mine .messenger-bubble__text a:focus {
    color: color-mix(in srgb, var(--hm-on-primary) 80%, transparent);
}

.messenger-bubble--mine .messenger-bubble__time {
    color: color-mix(in srgb, var(--hm-on-primary) 72%, transparent);
}

.messenger-bubble--mine .messenger-bubble__ticks.is-read {
    color: var(--hm-on-primary);
}

.messenger-bubble--mine .messenger-bubble__timer-icon {
    color: color-mix(in srgb, var(--hm-on-primary) 72%, transparent);
}

.messenger-bubble__text a {
    color: var(--hm-blue);
    text-decoration: underline;
    text-underline-offset: 4px;
    text-decoration-thickness: 1px;
}

.messenger-bubble__text a:hover,
.messenger-bubble__text a:focus {
    color: var(--hm-blue-strong);
    text-decoration-thickness: 2px;
}

.messenger-bubble--mine {
    margin-left: auto;
    border-bottom-right-radius: 5px;
    background: var(--hm-primary-bg);
}

.messenger-bubble--theirs {
    margin-right: auto;
    border-bottom-left-radius: 5px;
    background: var(--hm-secondary-bg);
}

/* Chat photos sit on a light matte frame (6px padding + hairline + soft
 * surface) so they read as a media card on any bubble — a dark primary
 * "mine" bubble, a secondary "theirs" bubble, or a future dark theme —
 * instead of blending into the background behind transparent pixels. */
.messenger-bubble img.attachment {
    display: block;
    max-width: 100%;
    max-height: 260px;
    margin: 0;
    padding: 4px;
    border-radius: 14px;
    background: var(--hm-surface-soft);

    /* Clickable: opens the full-size preview modal (chat.js). */
    cursor: pointer;
    transition:
        filter var(--hm-ease);
}

.messenger-bubble img.attachment:hover {
    filter: brightness(1.04);
}

/* Single-photo renders (the classic inline photo and a group-of-one tile)
 * reserve a photo-shaped box while the bytes are still loading (chat.js
 * toggles the is-loading class; it is removed on load/error). Without
 * intrinsic dimensions an unloaded <img> renders as a sliver and the whole
 * bubble jumps to full size the moment the photo lands — these minimums
 * keep the pre-load state a proper, soft-surfaced photo card instead.
 * The class is scoped to the NON-grid renders on purpose: grid tiles
 * already reserve their square via aspect-ratio, and a min-width there
 * would break the 2-column layout. */
.messenger-bubble__photo img.attachment.is-loading,
.messenger-bubble.is-media-group:not(.is-media-grid) .messenger-media__item img.attachment.is-loading {
    min-width: 200px;
    min-height: 140px;
}

/* Chat photo wrapper (inline image messages): the image renders normally
 * while online; while offline the encrypted attachment bytes cannot be
 * fetched (the service worker never caches them by design), so the image is
 * hidden and a themed "photo not available offline" placeholder takes its
 * place — a broken-image glyph never appears. The wrapper + placeholder are
 * rendered identically by the server template (chat_panel.php) and the
 * client builder (chat.js buildMsgEl) so online/offline renders match. */
.messenger-bubble__photo {
    position: relative;
}

.messenger-bubble__photo-offline {
    display: none;
}

body.is-offline .messenger-bubble__photo img.attachment {
    display: none;
}

/* The placeholder sits on whichever bubble it appears in (including the
 * solid primary "mine" bubbles), so it carries its own light surface to stay
 * readable on both sides, with the accent icon and navy/muted text. */
body.is-offline .messenger-bubble__photo-offline {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    min-width: 150px;
    padding: 22px 18px;
    border: 1px dashed var(--hm-line-strong);
    border-radius: 14px;
    color: var(--hm-blue);
    background: color-mix(in srgb, var(--ms-surface) 92%, transparent);
    text-align: center;
    cursor: default;
}

.messenger-bubble__photo-offline svg {
    margin-bottom: 4px;
    opacity: 0.9;
}

.messenger-bubble__photo-offline-title {
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--hm-navy);
}

.messenger-bubble__photo-offline-note {
    font-size: 0.68rem;
    line-height: 1.3;
    color: var(--hm-muted);
}

/* 5.x · Media groups (WhatsApp-style photo runs)

 * Consecutive same-sender photos (no caption, no reply quote, same day) render
 * as one bubble holding a tile grid: a single photo keeps the classic inline
 * look, two or more become a 2-column grid (a 3-photo run spans its third
 * tile across the full row, like WhatsApp), and beyond four photos only the
 * first four tiles exist in the DOM with a "+N" overlay on the last — the rest
 * live in the group's gallery and load lazily from the preview modal. Every
 * tile owns its own expiry (disappearing messages vanish per photo), and the
 * fixed aspect-ratio means a tile's <img> load never changes the layout, so
 * scroll anchoring stays exact. Rendered identically by chat_panel.php and
 * chat.js. */
.messenger-bubble.is-media-group {
    padding: 5px;
}

/* Single photo (group of 1) — mirrors the classic inline photo render: the
 * matte-framed image with the same hover wash. */
.messenger-media__item {
    position: relative;
    display: block;
    width: 100%;
    padding: 0;
    border: 0;
    border-radius: 14px;
    background: transparent;
    cursor: pointer;
}

.messenger-media__item img.attachment {
    display: block;
    max-width: 100%;
    max-height: 260px;
    padding: 4px;
    border-radius: 14px;
    background: var(--hm-surface-soft);
    cursor: pointer;
    transition: filter var(--hm-ease);
}

.messenger-media__item img.attachment:hover {
    filter: brightness(1.04);
}

/* A single lazy photo (a run of one image at/above the lazy threshold) has no
 * inline <img> — its only child is the absolutely-positioned "View" box, so
 * without an intrinsic height the button collapses to a sliver. Give it a
 * 16/10 box, matching the classic single-photo render. (Grid tiles already
 * size themselves via aspect-ratio.) */
.messenger-bubble.is-media-group:not(.is-media-grid) .messenger-media__item.is-lazy {
    aspect-ratio: 16 / 10;
    overflow: hidden;
    border-radius: 14px;
    background: var(--hm-surface-blue);
}

/* Two or more photos: a 2-column grid of square tiles, flush against the
 * bubble (the reduced group padding above). No overflow:hidden on the bubble
 * itself — that would clip the reply menu — so each tile clips itself. */

/* Multi-photo bubbles are content-sized (the bubble shrink-fits its photos),
 * so before any tile's bytes arrive they have no intrinsic width and the
 * whole album collapses to a sliver. This reserves a photo-row minimum so
 * the run renders as proper square tiles from first paint; the bubble still
 * grows to the photos' natural size once they load. min() caps it at the
 * panel width, so a very narrow screen can never overflow. */
.messenger-bubble.is-media-grid {
    min-width: min(280px, 100%);
}

.messenger-bubble.is-media-grid .messenger-media {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 3px;
}

.messenger-bubble.is-media-grid .messenger-media__item {
    aspect-ratio: 1 / 1;
    overflow: hidden;
    border-radius: 10px;
    background: var(--hm-surface-soft);
}

.messenger-bubble.is-media-grid .messenger-media__item img.attachment {
    width: 100%;
    height: 100%;
    max-width: none;
    max-height: none;
    padding: 0;
    margin: 0;
    border-radius: 0;
    object-fit: cover;
}

/* A 3-photo run: the third tile spans the full row (WhatsApp-style). */
.messenger-bubble.is-media-grid .messenger-media__item:nth-child(3):last-child {
    grid-column: 1 / -1;
    aspect-ratio: 16 / 10;
}

/* Lazy "View" placeholder tile: large photos (>= the lazy threshold) never
 * fetch their bytes for the grid — the tile is a themed card and the photo
 * loads only when opened in the preview modal. */
.messenger-media__lazy {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 5px;
    color: var(--hm-blue-strong);
    background: var(--hm-surface-blue);
}

.messenger-media__lazy-label {
    font-size: 0.7rem;
    font-weight: 700;
}

/* "+N" overlay on the last tile when a run has more than 4 photos. The tile
 * is clickable and opens the gallery at the first hidden photo. */
.messenger-media__more {
    position: absolute;
    inset: 0;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    font-size: 1.5rem;
    font-weight: 700;
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
    background: rgba(0, 0, 0, 0.45);
    cursor: pointer;
}

/* Offline placeholder inside a tile: same rule as the classic photo wrapper —
 * attachment bytes are encrypted and never cached, so while offline the tile
 * hides its <img> and shows a compact themed "not available offline" fill. */
.messenger-media__offline {
    display: none;
}

body.is-offline .messenger-media__item img.attachment {
    display: none;
}

body.is-offline .messenger-media__offline {
    position: absolute;
    inset: 0;
    z-index: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    padding: 8px;
    border-radius: 0;
    color: var(--hm-blue);
    background: color-mix(in srgb, var(--ms-surface) 92%, transparent);
    text-align: center;
}

/* The fill is laid out in-flow on EVERY media-group tile while offline: the
 * <img> is hidden, so an absolutely positioned fill would contribute no
 * intrinsic width and the whole grid bubble would collapse to a sliver (and
 * clip its text). A static fill lets each tile size to its content instead. */
body.is-offline .messenger-bubble.is-media-group .messenger-media__offline {
    position: static;
}

/* A run of ONE photo keeps the classic full-width offline card look (dashed
 * frame, roomy padding, readable text), exactly like the pre-grouping render. */
body.is-offline .messenger-bubble.is-media-group:not(.is-media-grid) .messenger-media__item {
    aspect-ratio: auto;
    background: transparent;
}

body.is-offline .messenger-bubble.is-media-group:not(.is-media-grid) .messenger-media__offline {
    padding: 22px 18px;
    border: 1px dashed var(--hm-line-strong);
    border-radius: 14px;
}

/* Grid tiles: relax the square so the placeholder is never clipped by the
 * tile's overflow, and let each tile stay tall enough to read. */
body.is-offline .messenger-bubble.is-media-grid .messenger-media__item {
    aspect-ratio: auto;
    min-height: 96px;
}

body.is-offline .messenger-media__offline svg {
    flex-shrink: 0;
    margin-bottom: 2px;
    opacity: 0.9;
}

body.is-offline .messenger-bubble.is-media-grid .messenger-media__offline svg {
    width: 20px;
    height: 20px;
}

body.is-offline .messenger-bubble.is-media-group:not(.is-media-grid) .messenger-media__offline-title {
    font-size: 0.78rem;
}

body.is-offline .messenger-media__offline-title {
    font-size: 0.74rem;
    font-weight: 600;
    line-height: 1.25;
    color: var(--hm-navy);
}

body.is-offline .messenger-media__offline-note {
    font-size: 0.68rem;
    line-height: 1.3;
    color: var(--hm-muted);
    overflow-wrap: anywhere;
}

/* Per-attachment caption — the text a sender linked to a photo/file in the
 * caption modal, rendered under the media like WhatsApp. Plain message text,
 * sized to sit between the media and the timestamp. */
.messenger-bubble__caption {
    margin-top: 5px;
    color: var(--hm-text);
    font-size: 0.84rem;
    line-height: 1.4;
    overflow-wrap: anywhere;
    white-space: pre-wrap;
}

.messenger-bubble__time {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 3px;
    margin-top: 5px;
    color: var(--hm-faint);
    font-size: 0.59rem;
    line-height: 1;
    transition: opacity 120ms ease;
}

/* Read receipt ticks on outgoing bubbles (WhatsApp-style): a single check
 * while the message is only sent, a second (offset) check fades in once the
 * other participant has read it (server stamps `read` from their
 * last_read_msg_id cursor). The unread state shows only the first check in
 * the muted time color; the read state turns both checks blue. Lives inside
 * the message time row so hover (which fades the time out to reveal the
 * chevron) treats the ticks like part of the timestamp. */
.messenger-bubble__ticks {
    display: inline-flex;
    align-items: center;
    margin-top: -1px;
}

.messenger-bubble__ticks-icon {
    display: block;
    width: 16px;
    height: auto;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* The base check (always visible) is the RIGHT tick, so the single "sent"
 * state sits flush against the timestamp with no gap on its right. The
 * second check is drawn to the LEFT of it — hidden until the message is
 * read, then it fades in next to the first in the accent blue. Because the
 * right tick never moves, the receipt state change is a fade, not a shift. */
.messenger-bubble__ticks-check--second {
    opacity: 0;
    transition: opacity 150ms ease;
}

.messenger-bubble__ticks.is-read {
    color: var(--hm-blue-strong);
}

.messenger-bubble__ticks.is-read .messenger-bubble__ticks-check--second {
    opacity: 1;
}

/* Reply quote card inside a bubble — the quoted original's author + preview.
 * Clickable (scrolls to the original); the left accent bar ties it to the
 * quoted side. The .is-gone variant is the muted placeholder shown once the
 * original has expired (its content must vanish with it). Rendered identically
 * by chat_panel.php and chat.js buildMsgEl. */
.messenger-bubble__reply {
    display: flex;
    flex-direction: column;
    gap: 2px;
    width: 100%;
    margin-bottom: 7px;
    padding: 7px 10px;
    border: 1px solid var(--hm-line);
    border-left: 3px solid var(--hm-blue-soft);
    border-radius: 10px;
    color: var(--hm-text);
    font-size: 0.78rem;
    font-family: inherit;
    text-align: left;
    /* Solid surface — same reasoning as the file card: a translucent white
     * over the solid primary fill read as a muddy dark panel with dark text;
     * a solid card keeps the quote readable on both bubble sides. */
    background: var(--ms-surface);
    cursor: pointer;
}

.messenger-bubble__reply-name {
    color: var(--hm-blue-strong);
    font-size: 0.72rem;
    font-weight: 700;
}

.messenger-bubble__reply-text {
    overflow: hidden;
    color: var(--hm-muted);
    line-height: 1.35;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.messenger-bubble__reply.is-gone {
    border-left-color: var(--hm-line);
    color: var(--hm-faint);
    font-size: 0.72rem;
    font-style: italic;
    background: color-mix(in srgb, var(--ms-surface) 35%, transparent);
    cursor: default;
}

/* Landing highlight on the original message after a quote card scrolls to
 * it. Instead of a quick blink, the message stays marked for ~3s: an accent
 * ring fades in fast, holds steady, then fades out, while the bubble itself
 * takes a soft accent wash (tint on incoming, lighten on outgoing) — so
 * there is no doubt which message the quote pointed at. The ring is a
 * TEMPORARY box-shadow (never a resting shadow) drawn outside the bubble;
 * the wash is class-scoped background-color with the base transition below
 * animating the fade-out when the class clears. Both use single-level
 * color-mix against --ms-accent — Chrome cannot interpolate a NESTED
 * color-mix in an animated box-shadow, so the ring is derived here rather
 * than reusing a derived token. */
@keyframes messenger-reply-highlight {

    0% {
        box-shadow: none;
    }

    12%,
    85% {
        box-shadow: 0 0 0 3px color-mix(in srgb, var(--ms-accent) 45%, transparent);
    }

    100% {
        box-shadow: none;
    }
}

.messenger-bubble.is-reply-target {
    animation: messenger-reply-highlight 2.9s ease;
}

/* Incoming wash: a soft accent tint over the grey fill. */
.messenger-bubble--theirs.is-reply-target {
    background-color: color-mix(in srgb, var(--ms-accent) 13%, var(--hm-secondary-bg));
}

/* Outgoing wash: lighten the solid primary fill so the highlight reads. */
.messenger-bubble--mine.is-reply-target {
    background-color: color-mix(in srgb, var(--ms-surface) 30%, var(--hm-primary-bg));
}

/* Tapped-bubble context menu — a popover anchored to the message stream
 * (position: absolute inside the scroller, placed by chat.js at the tapped
 * bubble's edge). Solid surface (no glass — a menu over the message stream
 * reads crisper flat), a hairline border and a soft lift shadow; it scrolls
 * with the bubble it belongs to, and its tail points at the anchor. */
.messenger-bubble-menu {
    position: absolute;
    z-index: 300;
    min-width: 132px;
    padding: 6px;
    border: 1px solid var(--hm-line-strong);
    border-radius: 14px;
    background: var(--ms-surface);
    box-shadow: 0 10px 24px -10px rgba(15, 23, 42, 0.22);
}

.messenger-bubble-menu[hidden] {
    display: none;
}

/* Opening pop — a quick fade with a 3px rise. Safe here: the menu is
 * JS-positioned, so there is no Popper transform to fight. */
.messenger-bubble-menu:not([hidden]) {
    animation: hm-menu-rise 140ms ease-out;
}

@keyframes hm-menu-rise {
    from {
        opacity: 0;
        transform: translateY(3px);
    }

    to {
        opacity: 1;
        transform: translateY(0);
    }
}

@keyframes hm-menu-fade {
    from {
        opacity: 0;
    }

    to {
        opacity: 1;
    }
}

/* Tail — a rotated square pointing at the anchor. Default (opening above)
 * hangs from the bottom edge pointing down; .is-below flips it to the top.
 * Its horizontal position comes from chat.js via --tail-x. */
.messenger-bubble-menu::before {
    content: "";
    position: absolute;
    bottom: -6px;
    left: var(--tail-x, 50%);
    width: 10px;
    height: 10px;
    background: inherit;
    border-right: 1px solid var(--hm-line-strong);
    border-bottom: 1px solid var(--hm-line-strong);
    transform: translateX(-50%) rotate(45deg);
}

.messenger-bubble-menu.is-below::before {
    top: -6px;
    bottom: auto;
    border: 0;
    border-left: 1px solid var(--hm-line-strong);
    border-top: 1px solid var(--hm-line-strong);
}

.messenger-bubble-menu__item {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: 9px;
    width: 100%;
    padding: 10px 12px;
    border: 0;
    border-radius: 9px;
    color: var(--hm-text);
    font-size: 0.85rem;
    font-weight: 600;
    font-family: inherit;
    line-height: 1;
    text-align: left;
    background: transparent;
    cursor: pointer;
    transition: background var(--hm-ease);
}

.messenger-bubble-menu__item:hover,
.messenger-bubble-menu__item:focus-visible {
    outline: none;
    background: var(--hm-primary-tint);
}

.messenger-bubble__timer-icon {
    flex-shrink: 0;
    color: var(--hm-faint);
}

/* Offline outbox pending bubble (Phase 3c): rendered for messages that are
 * queued locally but not yet confirmed by the server. It is slightly
 * translucent with a warning-accent pending label so the user can tell at a
 * glance it has not been delivered. */
.messenger-bubble.is-pending {
    opacity: 0.78;
    background: color-mix(in srgb, var(--ms-warning) 10%, transparent);
}

.messenger-bubble.is-pending .messenger-bubble__pending-label {
    color: var(--hm-warning);
    font-weight: 600;
}

.messenger-bubble.is-pending .messenger-bubble__pending-label::before {
    content: "";
    width: 7px;
    height: 7px;
    margin-right: 5px;
    border-radius: 50%;
    background: currentColor;
    animation: offline-pending-pulse 1.6s var(--hm-ease) infinite;
}

@keyframes offline-pending-pulse {

    0%,
    100% {
        opacity: 0.45;
    }

    50% {
        opacity: 1;
    }
}

.messenger-system-message {
    margin: 1px 0 14px;
    color: var(--hm-faint);
    font-size: 0.72rem;
    font-weight: 600;
    text-align: center;
}

/* 5.5 · File attachments */

/* File attachment card — designed to feel native to the HM look while
 * keeping download functionally identical. Fills its bubble (max-width 100%)
 * and the pill action is pushed to the card's right end, WhatsApp-style. */
.messenger-file-attachment {
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
    max-width: 100%;
    padding: 10px 12px;
    border: 1px solid var(--hm-line);
    border-radius: 14px;
    /* Solid surface card — on a primary "mine" bubble the old translucent
     * white mixed into a muddy dark wash; a solid card reads crisp on both
     * the indigo fill and the grey incoming fill (WhatsApp does the same). */
    background: var(--ms-surface);
}

.messenger-file-attachment__icon {
    display: grid;
    place-items: center;
    width: 34px;
    height: 34px;
    flex: 0 0 auto;
    border-radius: 10px;
    color: var(--hm-blue-strong);
    background: var(--hm-surface-blue);
}

.messenger-file-attachment__name {
    /* Grows to use the widened card, so the right-parked action pill leaves
     * no dead gap; min-width: 0 + ellipsis keep long names in check. */
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    color: var(--hm-navy);
    font-size: 0.8rem;
    font-weight: 700;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Both file-card actions share one pill-button recipe: "Download" triggers a
 * real download, while "View" (lazy-loaded large images, see chat.js) opens
 * the image preview modal instead — the cursor is the only difference.
 * margin-left: auto parks the pill at the card's right end. */
.messenger-file-attachment__download,
.messenger-file-attachment__view {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    flex-shrink: 0;
    margin-left: auto;
    padding: 6px 10px;
    border: 0;
    border-radius: 999px;
    color: var(--hm-blue-strong);
    font-size: 0.68rem;
    font-weight: 700;
    text-decoration: none;
    background: color-mix(in srgb, var(--hm-blue-strong) 8%, transparent);
    transition:
        color 150ms ease,
        background 150ms ease;
}

/* "View" is interactive (opens the preview), so it alone gets a pointer
 * cursor on top of the shared recipe. */
.messenger-file-attachment__view {
    cursor: pointer;
}

.messenger-file-attachment__download:hover,
.messenger-file-attachment__view:hover {
    color: var(--hm-navy);
    background: color-mix(in srgb, var(--hm-blue-strong) 14%, transparent);
}

/* Download in flight (blob fetch): swap the button's icon for a spinning
 * loader until the save starts, reusing the app's hm-btn-busy-spin keyframes.
 * The label stays put; the icon (and only the icon) yields to the spinner.
 * pointer-events: none also blocks repeat clicks (plus the JS in-flight guard). */
.messenger-file-attachment__download.is-busy,
.image-preview__download.is-busy {
    pointer-events: none;
}

.messenger-file-attachment__download.is-busy>svg,
.image-preview__download.is-busy>svg {
    display: none;
}

.messenger-file-attachment__download.is-busy::before,
.image-preview__download.is-busy::before {
    content: '';
}

/* Shared "activity" dots — the WhatsApp-style three tiny bubbles that
 * bounce in sequence. Used by the typing indicator and the load-older
 * row, so every "something is happening" state speaks one language. */
.hm-loading-dots {
    display: inline-flex;
    align-items: center;
    gap: 3px;
}

.hm-loading-dots span {
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background: var(--hm-muted);
    animation: hm-typing-dot 1.2s ease-in-out infinite;
}

.hm-loading-dots span:nth-child(2) {
    animation-delay: 0.15s;
}

.hm-loading-dots span:nth-child(3) {
    animation-delay: 0.3s;
}

@keyframes hm-typing-dot {

    0%,
    60%,
    100% {
        transform: translateY(0);
        opacity: 0.45;
    }

    30% {
        transform: translateY(-3px);
        opacity: 1;
    }
}

/* 5.6 · Typing indicator */

/* A WhatsApp-style typing bubble: an incoming-message pill (secondary fill,
 * the same bottom-left notch as .messenger-bubble--theirs) holding the three
 * bouncing dots — no text, just the animation, aligned left like a message. */
.messenger-typing {
    display: flex;
    justify-content: flex-start;
    /* Equal breathing room above (from the last message) and below (from
     * the composer) — the typing state should float, not hug either edge. */
    padding: 10px 1.25rem;
    min-height: 0;
    transition: opacity 0.2s;
}

.messenger-typing__bubble {
    display: inline-flex;
    align-items: center;
    padding: 11px 15px;
    border-radius: 16px;
    border-bottom-left-radius: 5px;
    background: var(--hm-secondary-bg);
}

/* 5.7 · Composer */

.messenger-composer {
    position: relative;
    z-index: 20;
    width: 100%;
    padding:
        9px 12px 9px;
    border-top: 1px solid var(--hm-line);
    background: var(--ms-surface);
}

/* Upload-in-flight status row above the composer field: spinner, file name,
 * live percentage and a slim progress track. Shown while an attachment POST
 * is in flight (chat.js), hidden otherwise. */
.messenger-composer__upload {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    max-width: 1050px;
    margin: 0 auto 8px;
    padding: 7px 12px;
    border: 1px solid var(--hm-line);
    border-radius: 12px;
    background: var(--ms-surface);
    font-size: 0.8rem;
    color: var(--hm-muted);
}

.messenger-composer__upload[hidden] {
    display: none;
}

/* Reply-to bar above the composer field: who is being replied to + a preview
 * of the quoted message, with ✕ to cancel. Mirrors the upload-status row's
 * placement and look, so reply mode reads as a sibling state of an upload. */
.messenger-composer__reply {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    max-width: 1050px;
    margin: 0 auto 8px;
    padding: 8px 12px;
    border: 1px solid var(--hm-line);
    border-radius: 12px;
    background: var(--ms-surface);
    color: var(--hm-muted);
    font-size: 0.78rem;
}

.messenger-composer__reply[hidden] {
    display: none;
}

.messenger-composer__reply-copy {
    display: flex;
    flex-direction: column;
    gap: 1px;
    flex: 1;
    min-width: 0;
}

.messenger-composer__reply-label {
    color: var(--hm-blue-strong);
    font-size: 0.72rem;
    font-weight: 700;
}

.messenger-composer__reply-text {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.messenger-composer__reply-close {
    display: grid;
    place-items: center;
    flex: 0 0 auto;
    width: 26px;
    height: 26px;
    padding: 0;
    border: 1px solid transparent;
    border-radius: 50%;
    color: var(--hm-muted);
    background: transparent;
    cursor: pointer;
    transition: color var(--hm-ease), background var(--hm-ease);
}

.messenger-composer__reply-close:hover,
.messenger-composer__reply-close:focus-visible {
    outline: none;
    color: var(--hm-navy);
    background: var(--hm-primary-tint);
}

/* Pending-attachment strip above the composer field: images wait as
 * thumbnails (files as icon cards) until the Send button is pressed — they
 * are no longer transmitted the moment they are picked. Mirrors the reply
 * bar's row placement and look. Each tile carries a remove ✕; a counter chip
 * (e.g. 2/4) sits at the right end. */
.messenger-composer__attachments {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    max-width: 1050px;
    margin: 0 auto 8px;
}

.messenger-composer__attachments[hidden] {
    display: none;
}

.messenger-composer__attachments-list {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    flex: 1;
    min-width: 0;
}

.messenger-composer__attachment {
    position: relative;
    width: 58px;
    height: 58px;
    flex: 0 0 auto;
    overflow: hidden;
    border: 1px solid var(--hm-line-strong);
    border-radius: 12px;
    background: var(--hm-surface-soft);
    /* Whole tile is a tap target: tapping makes this attachment the caption
     * target — the composer field then edits its caption. The ✕ stays a
     * nested button. */
    cursor: pointer;
    -webkit-user-select: none;
    /* Safari 15 and older */
    user-select: none;
    transition:
        border-color var(--hm-ease);
}

.messenger-composer__attachment:hover {
    border-color: var(--hm-line-strong);
}

html.hm-focus-border .messenger-composer__attachment:focus-visible {
    border-color: #000;
}

/* Active tile: strong ring marks the attachment whose caption the composer
 * field is currently editing (the corner pencil badge was removed — the ring
 * plus the editing affordance in the field is enough). */
.messenger-composer__attachment.is-active {
    border-color: var(--hm-blue-strong);
}

.messenger-composer__attachment img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* Non-image attachments queue as a compact icon card (file glyph + name). */
.messenger-composer__attachment--file {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    width: 96px;
    padding: 6px 5px;
    color: var(--hm-blue-strong);
}

.messenger-composer__attachment-name {
    max-width: 86px;
    overflow: hidden;
    color: var(--hm-muted);
    font-size: 0.62rem;
    line-height: 1.2;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.messenger-composer__attachment-remove {
    position: absolute;
    top: 3px;
    right: 3px;
    display: grid;
    place-items: center;
    width: 20px;
    height: 20px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    color: var(--hm-on-primary);
    /* Dark scrim chip — sits ON the attachment's image, so it stays dark in
     * both themes (see --hm-scrim-base). */
    background: color-mix(in srgb, var(--hm-scrim-base) 80%, transparent);
    cursor: pointer;
    transition:
        background var(--hm-ease),
        transform var(--hm-ease);
}

.messenger-composer__attachment-remove:hover,
.messenger-composer__attachment-remove:focus-visible {
    outline: none;
    background: color-mix(in srgb, var(--hm-scrim-base) 95%, transparent);
    transform: scale(1.1);
}

/* Counter chip (e.g. 2/4) at the right end of the strip. */
.messenger-composer__attachments-count {
    flex: 0 0 auto;
    padding: 3px 9px;
    border: 1px solid var(--hm-line);
    border-radius: 999px;
    color: var(--hm-muted);
    font-size: 0.68rem;
    font-weight: 700;
    background: var(--hm-surface-soft);
}

/* At the 4-item cap the attach button is dimmed — there is no more room. */
.messenger-composer.is-attach-full .messenger-composer__attach {
    opacity: 0.45;
    pointer-events: none;
}

.messenger-composer__upload-spin {
    width: 14px;
    height: 14px;
    flex: none;
    border: 2px solid var(--hm-blue);
    border-right-color: transparent;
    border-radius: 50%;
    animation: hm-btn-busy-spin 0.7s linear infinite;
}

.messenger-composer__upload-label {
    flex: 1;
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.messenger-composer__upload-label strong {
    font-weight: 600;
    color: var(--hm-text);
}

.messenger-composer__upload-pct {
    flex: none;
    font-variant-numeric: tabular-nums;
}

.messenger-composer__upload-track {
    position: relative;
    display: block;
    width: 64px;
    height: 4px;
    flex: none;
    border-radius: 999px;
    background: var(--hm-line);
    overflow: hidden;
}

.messenger-composer__upload-bar {
    display: block;
    height: 100%;
    width: 0%;
    border-radius: 999px;
    background: var(--hm-primary-bg);
    transition: width 150ms ease;
}

/* While an upload is in flight the attach button is dimmed (the spinner row
 * is the feedback); the in-flight guard already blocks double selects. */
.messenger-composer.is-uploading .messenger-composer__attach {
    opacity: 0.45;
    pointer-events: none;
}

.messenger-chat__unavailable {
    position: relative;
    z-index: 20;
    width: 100%;
    padding:
        16px 20px;
    border-top: 1px solid var(--hm-line);
    background: var(--ms-surface);
    color: var(--hm-muted);
    font-size: 0.85rem;
    text-align: center;
}

.messenger-composer__inner {
    display: flex;
    align-items: flex-end;
    gap: 8px;
    width: 100%;
    max-width: 1050px;
    margin: 0 auto;
}

/* The attach action is a filled secondary circle — the mirror image of the
 * filled primary send button, so the two round controls flank the field as a
 * balanced pair (Telegram-style). The icon uses the deep accent at rest and
 * goes navy on hover, exactly like the other filled controls. */
.messenger-composer__attach {
    width: 48px;
    height: 48px;
    flex: 0 0 48px;
    border: 0;
    border-radius: 50%;
    color: var(--hm-blue-strong);
    background: var(--hm-secondary-bg);
    cursor: pointer;
    transition:
        color var(--hm-ease),
        background var(--hm-ease),
        transform var(--hm-ease);
}

.messenger-composer__attach:hover,
.messenger-composer__attach:focus-visible {
    outline: none;
    color: var(--hm-navy);
    background: var(--hm-secondary-hover);
    transform: scale(1.05);
}

.messenger-composer__form {
    display: flex;
    align-items: flex-end;
    gap: 8px;
    flex: 1;
    min-width: 0;
}

.messenger-composer__field {
    display: flex;
    align-items: flex-end;
    min-width: 0;
    min-height: 48px;
    flex: 1;
    padding:
        5px 8px 5px 13px;
    border: 1px solid transparent;
    border-radius: 24px;
    /* Filled pill in the secondary color — the same recipe as the sidebar
     * search field, so the two text entry points share one visual language.
     * Borderless at rest; the black border appears only on focus below. */
    background: var(--hm-secondary-bg);
    transition:
        border-color 150ms ease;
}

.messenger-composer__input {
    display: block;
    width: 100%;
    min-width: 0;
    min-height: 36px;
    max-height: 140px;
    padding:
        8px 2px 6px;
    resize: none;
    overflow-y: auto;
    border: 0;
    outline: 0;
    color: var(--hm-text);
    line-height: 1.4;
    font-size: 0.9rem;
    background: transparent;
    /* Double-tap zoom on the field itself is never useful while typing, and
     * it fights the keyboard-resize handling on mobile (see the iOS rule
     * below). Letting single taps through immediately also feels crisper. */
    touch-action: manipulation;
}

.messenger-composer__input::placeholder {
    color: var(--hm-faint);
}

/* iOS Safari auto-enlarges a focused form field whose font is under 16px
 * and zooms the page to keep the caret visible — the composer "jumps" and
 * the chat above it ends up hidden until the user scrolls back. Touch-only
 * WebKit (iPhone/iPad/iOS Chrome) gets the platform-standard 16px so no
 * zoom is needed; Android Chrome does not zoom on focus (it resizes the
 * viewport instead), so it keeps the smaller size. */
@media (hover: none) and (pointer: coarse) {
    @supports (-webkit-touch-callout: none) {
        .messenger-composer__input {
            font-size: 16px;
        }
    }
}

.messenger-composer__send {
    flex: 0 0 48px;
}

.send-button {
    display: grid;
    place-items: center;
    width: 48px;
    height: 48px;
    flex: 0 0 48px;
    border: 0;
    border-radius: 50%;
    color: var(--hm-on-primary);
    background: var(--hm-primary-bg);
    cursor: pointer;
    transition:
        background 150ms ease,
        transform 150ms ease;
}

/* The send button is a filled primary control, so it darkens to the same
 * hover shade as every other primary button. */
.send-button:hover,
.send-button:focus-visible {
    outline: none;
    background: var(--hm-primary-hover);
}

/* 6 · Profile panel */
.messenger-profile {
    flex: 1;
    display: flex;
    flex-direction: column;
    min-height: 0;

    /* Transparent here: the panel shows the shared layout surface behind it. */
}

.messenger-profile__body {
    flex: 1;
    overflow-y: auto;
    /* Vertical-only scroll: cards (e.g. the 2FA setup card) must never add
     * a horizontal scrollbar on narrow screens. */
    overflow-x: hidden;
    padding: 2.5rem 2rem;
    /* In standalone PWA mode the browser's bottom chrome (home indicator /
     * navigation bar) can overlap the scroll container; this guarantees the
     * last card (e.g. Log out of all devices) is always scrollable into view. */
    padding-bottom: calc(2.5rem + env(safe-area-inset-bottom, 0px));
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1.25rem;
}

/* Profile cards carry only their page layout here; the shared card look
 * (surface, border, radius, shadow) comes from the .hm-card component. */
.messenger-profile__card {
    width: 100%;
    max-width: 580px;
}

/* Theme picker (Preferences) — a segmented control: a quiet secondary track
 * with the active option on the solid surface. Constant 1px borders keep
 * the active state from shifting the layout; the whole control re-tints
 * from the --hm-* tokens in both themes. */
.theme-switch {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    padding: 3px;
    border-radius: 999px;
    background: var(--hm-secondary-bg);
    flex-shrink: 0;
}

.theme-switch__option {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 14px;
    border: 1px solid transparent;
    border-radius: 999px;
    font: inherit;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--hm-muted);
    background: transparent;
    cursor: pointer;
    transition:
        color var(--hm-ease),
        background var(--hm-ease),
        border-color var(--hm-ease);
}

.theme-switch__option:hover {
    color: var(--hm-navy);
}

.theme-switch__option.is-active {
    color: var(--hm-navy);
    background: var(--hm-surface-solid);
    border-color: var(--hm-line);
}

/* On small screens the theme picker drops to its own line under the label
 * and stretches full width, with the two options split evenly — the iOS
 * settings pattern for a segmented control inside a narrow card. */
@media (max-width: 479.98px) {
    .pref-theme-row {
        flex-direction: column;
        /* !important needed: Bootstrap's gap-3 / align-items-center
         * utilities are themselves !important, so a plain override loses. */
        align-items: stretch !important;
        gap: 0.75rem !important;
    }

    .pref-theme-row .theme-switch {
        width: 100%;
    }

    .pref-theme-row .theme-switch__option {
        flex: 1 1 0;
        justify-content: center;
    }
}

.messenger-profile__top {
    display: flex;
    align-items: center;
    gap: 1.25rem;
}

.messenger-profile__avatar-wrap {
    position: relative;
    flex-shrink: 0;
}

.messenger-profile__avatar {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 92px;
    height: 92px;
    border-radius: 50%;
    overflow: hidden;
    border: 3px solid var(--hm-surface-solid);
    background: var(--hm-secondary-bg);
    flex-shrink: 0;
}

.messenger-profile__avatar img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* The 92px profile avatar is roughly double the list/header avatars, so its
 * initial letter scales up to match (2.25rem ≈ 36px on a 92px circle). */
.messenger-profile__avatar .hm-avatar-initial {
    font-size: 2.25rem;
}

/* Avatar upload-in-flight: the disc dims and a spinner sits on top while the
 * new photo POSTs (profile.js toggles .is-uploading on the wrap). */
.messenger-profile__avatar-wrap.is-uploading .messenger-profile__avatar {
    opacity: 0.75;
}

.messenger-profile__avatar-wrap.is-uploading::before {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 30px;
    height: 30px;
    margin: -15px 0 0 -15px;
    border: 3px solid rgba(255, 255, 255, 0.4);
    border-top-color: #ffffff;
    border-radius: 50%;
    animation: hm-btn-busy-spin 0.7s linear infinite;
    z-index: 3;
}

.messenger-profile__avatar-wrap.is-uploading::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: 50%;
    /* Upload dim veil — stays dark in both themes (see --hm-scrim-base). */
    background: color-mix(in srgb, var(--hm-scrim-base) 35%, transparent);
}

.messenger-profile__avatar-wrap.is-uploading .messenger-profile__avatar-edit {
    display: none;
}

/* Avatar photo menu: a primary-fill disc pinned to the avatar's corner
 * (raised by the light ring) that opens a "…" dropdown with Change/Remove
 * photo actions. The menu look comes from the global HM dropdown component
 * (see the .dropdown-menu / .dropdown-item rules above .messenger-sidebar). */
.messenger-profile__avatar-edit {
    position: absolute;
    right: -4px;
    bottom: -4px;
}

.messenger-profile__avatar-edit-btn {
    display: grid;
    place-items: center;
    width: 32px;
    height: 32px;
    border: 3px solid #ffffff;
    border-radius: 50%;
    color: var(--hm-on-primary);
    background: var(--hm-primary-bg);
    cursor: pointer;
    padding: 0;
    transition:
        transform var(--hm-ease);
}

.messenger-profile__avatar-edit-btn:hover,
.messenger-profile__avatar-edit-btn.show {
    background: var(--hm-primary-hover);
}

.messenger-profile__info {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: .25rem;
}

.messenger-profile__name-line {
    display: flex;
    align-items: baseline;
    gap: .45rem;
    min-width: 0;
}

.messenger-profile__name {
    font-size: 1.25rem;
    font-weight: 700;
    color: var(--ms-text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Role hint beside the full name — lighter weight, muted, non-bold. */
.messenger-profile__role {
    font-size: .95rem;
    font-weight: 400;
    color: var(--hm-muted);
    white-space: nowrap;
}

.messenger-profile__id {
    font-size: .8rem;
    color: var(--ms-text-muted);
    font-family: monospace;
}

.messenger-profile__name-text {
    font-size: .95rem;
    font-weight: 500;
    color: var(--ms-text);
}

/* 7 · Helheim Modals
 * The template's modal recipe on top of Bootstrap's dialog: a narrow
 * 430px solid panel (22px radius, strong hairline, a deep soft shadow that
 * makes it float over the navy overlay) with a rune + title + icon-button
 * close header. All chrome is token-driven; the old red/emoji styling is
 * gone. */

/* 7.1 · Modal chrome (dialog, header, body, footer) */

/* Panel width + the template's 0 30px 90px shadow feel. */
.helheim-modal .modal-dialog {
    max-width: 430px;
}

/* Bootstrap animates the dialog with a 300ms slide; a shorter 180ms
 * ease-out-expo (plus a quick backdrop fade) makes the modal feel instant
 * and premium — a soft rise with a 3% settle, no heavy backdrop blur
 * (blur was what was delaying the open). */
.helheim-modal.modal.fade .modal-dialog {
    transform: translateY(-14px) scale(0.97);
    transition: transform 180ms cubic-bezier(0.16, 1, 0.3, 1);
}

.helheim-modal.modal.show .modal-dialog {
    transform: none;
}

.helheim-modal .modal-content {
    border: 1px solid var(--hm-line-strong);
    border-radius: 22px;
    color: var(--hm-text);
    background: var(--ms-surface);
    /* The deep shadow that makes the panel float — the biggest floating
     * surface in the app, so it gets the deepest of the lift shadows. */
    box-shadow: 0 24px 60px -18px rgba(15, 23, 42, 0.35);
}

/* Header: title (flex:1) + close button; optional description sits under
 * the title. Every modal leads with a .modal-rune rune-circle badge holding
 * an SVG glyph (the app's icon language — no emoji). */
.helheim-modal .modal-header {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 22px 24px 0;
    border-bottom: 0;
}

.helheim-modal .modal-rune {
    display: grid;
    place-items: center;
    width: 42px;
    height: 42px;
    flex: 0 0 auto;
    overflow: hidden;
    border: 1px solid transparent;
    border-radius: 50%;
    color: var(--hm-blue-strong);
    background: var(--hm-primary-tint);
}

.helheim-modal .modal-rune svg {
    display: block;
}

.helheim-modal .modal-title {
    flex: 1;
    margin: 0;
    color: var(--hm-navy);
    font-size: 1.05rem;
    font-weight: 700;
}

.helheim-modal .modal-description {
    margin-top: 4px;
    color: var(--hm-muted);
    font-size: 0.76rem;
    line-height: 1.45;
}

/* Close button — the shared icon-button recipe (42px circle, hairline on
 * hover, navy ink), holding an SVG ✕. */
.helheim-modal .modal-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 42px;
    height: 42px;
    flex: 0 0 auto;
    border: 1px solid transparent;
    border-radius: 50%;
    color: var(--hm-blue);
    background: transparent;
    cursor: pointer;
    transition:
        color var(--hm-ease),
        background var(--hm-ease),
        border-color var(--hm-ease),
        transform var(--hm-ease);
}

.helheim-modal .modal-close:hover {
    color: var(--hm-navy);
    border-color: var(--hm-line);
    background: color-mix(in srgb, var(--ms-surface) 76%, transparent);
}

.helheim-modal .modal-body {
    padding: 18px 24px 22px;
}

.helheim-modal .modal-body p {
    margin: 0;
    color: var(--hm-muted);
    font-size: 0.82rem;
    line-height: 1.5;
}

/* When a field follows the description paragraph (logout-all password, timer /
 * disappearing selects), keep the 12px rhythm without adding a trailing margin
 * to paragraphs that end the body (delete-confirm modals, remove photo). */
.helheim-modal .modal-body p+.field {
    margin-top: 12px;
}

/* The last .field in a modal body drops its bottom margin — the body's own
 * padding closes the gap to the footer. Matches the trailing field even when a
 * helper element (e.g. the hidden #addUserError div) follows it. */
.helheim-modal .modal-body .field:not(:has(+ .field)) {
    margin-bottom: 0;
}

.helheim-modal .modal-footer {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
    padding: 0 24px 24px;
    border-top: 0;
}

.helheim-modal .modal-footer>* {
    margin: 0;
}

/* 7.2 · Modal form fields */

/* Form fields — one HM input recipe for the canonical .field wrappers used by
 * every modal body (all modals migrated; the .helheim-input/.helheim-select
 * and modal-scoped .form-control/.form-select aliases are gone). */
/* Modal bodies reuse the canonical .field / .form-control recipe above; only
 * the textarea needs a modal-specific shape (rows drive its height, with real
 * top/bottom padding instead of the pill's horizontal-only padding). */
.helheim-modal .field textarea {
    height: auto;
    min-height: 88px;
    padding: 12px 15px;
    line-height: 1.5;
    resize: vertical;
}

/* New chat composer: sits below the search results (revealed once a user is
 * selected) and keeps the same 12px rhythm as stacked modal fields. The Start
 * button is a full-width CTA — the modal has no footer of its own. */
#userMessageWrap {
    margin-top: 12px;
}

#userMessageWrap .btn {
    margin-top: 12px;
    width: 100%;
}

/* Helper text — HM muted recipe (replaces Bootstrap's .form-text look) for
 * the .form-text divs that sit inside .field wrappers in modal bodies. */
.helheim-modal .form-text {
    margin-top: 6px;
    color: var(--hm-muted);
    font-size: 0.74rem;
    line-height: 1.5;
}

/* Confirmation-target chip — shows the record you're about to act on inside a
 * delete confirm (user / role / invitation / session). */
.modal-target {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.6rem 0.75rem;
    margin-bottom: 0.9rem;
    background: var(--hm-surface-soft);
    border: 1px solid var(--hm-line);
    border-radius: 10px;
}

/* Modal footer actions always use the global button recipes: .btn-primary
 * for the confirm action, .btn-outline-secondary for dismissive ones, and
 * .btn-danger for destructive confirms — same 48px height in the same row. */

/* 7.3 · New-chat results list */

/* Results list inside the new-chat modal */
.helheim-modal .helheim-results {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    margin-top: 0.5rem;
    max-height: 280px;
    overflow-y: auto;
    border: 1px solid var(--hm-line);
    border-radius: 12px;
    background: color-mix(in srgb, var(--ms-surface) 86%, transparent);
}

/* An empty results box would otherwise draw an empty bordered pill between the
 * search input and the footer — hide it until the JS fills it with rows. */
.helheim-modal .helheim-results:empty {
    display: none;
}

.helheim-modal .helheim-results .helheim-result-item {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    width: 100%;
    padding: 0.45rem 0.7rem;
    border: 1px solid transparent;
    border-radius: 10px;
    color: var(--hm-text);
    font-size: 0.9rem;
    font-family: inherit;
    text-align: left;
    text-decoration: none;
    background: none;
    cursor: pointer;
    transition: background var(--hm-ease), border-color var(--hm-ease);
}

.helheim-modal .helheim-results .helheim-result-item:hover {
    background: var(--hm-primary-tint);
}

.helheim-modal .helheim-results .helheim-result-item.is-selected {
    border-color: var(--hm-line-strong);
    background: var(--hm-primary-tint-strong);
}

.helheim-modal .helheim-results .helheim-result-item__avatar {
    width: 46px;
    height: 46px;
    flex-shrink: 0;
    border-radius: 50%;
}

.helheim-modal .helheim-results .helheim-result-item__avatar img {
    display: block;
    width: 100%;
    height: 100%;
    border-radius: 50%;
    object-fit: cover;
}

.helheim-modal .helheim-results .helheim-result-item .result-name {
    font-size: 0.9rem;
    font-weight: 600;
}

/* 7.4 · Modal backdrop */

/* Override Bootstrap modal backdrop — the template's navy overlay. A plain
 * tint (no backdrop blur) so opening the modal never lags. The background is
 * pinned on the base element (not just .show): Bootstrap's own default
 * backdrop is solid black (#000), so scoping the tint to .show only made the
 * backdrop flash black for a frame while it faded out on close. The dark
 * image-viewer tint is applied globally to every modal. */
.modal-backdrop {
    /* Overlay scrim — fixed dark in both themes (see --hm-scrim-base). */
    background: color-mix(in srgb, var(--hm-scrim-base) 74%, transparent);
}

.modal-backdrop.show {
    opacity: 1;
}

/* On mobile, modal gets closer */
@media (max-width: 767.98px) {
    .helheim-modal .modal-dialog {
        margin: auto;
        padding: 0.75rem;
    }

    .helheim-modal .modal-header {
        padding: 18px 18px 0;
    }

    .helheim-modal .modal-body {
        padding: 16px 18px 18px;
    }

    .helheim-modal .modal-footer {
        padding: 0 18px 18px;
    }
}

/* 7.5 · Image preview (lightbox) */

/* Full-size image preview — a lightbox instead of a dialog. It stays a
 * Bootstrap modal (so Esc, backdrop click and the close button dismiss it and
 * the page scroll locks for free), but the --image modifier strips every piece
 * of modal chrome into a full-viewport stage: no panel/header/body, just the
 * image centered on the dark overlay with the filename floating top-left and a
 * circular close button top-right. Placed after the mobile block so it wins on
 * every breakpoint. */
.helheim-modal--image .modal-dialog {
    width: 100%;
    height: 100%;
    margin: 0;
    max-width: none;
}

.helheim-modal--image .modal-content {
    position: relative;
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 56px 24px;
    border: 0;
    border-radius: 0;
    background: transparent;
    box-shadow: none;
    /* Only the image and the two floating controls are interactive; clicks on
     * the surrounding stage fall through to the .modal, so backdrop-click
     * dismissal keeps working. */
    pointer-events: none;
}

/* The fade slide (translate -50px) is for small dialogs; a full-screen stage
 * uses a subtle zoom-in on the image + fade for controls, keeping the feel
 * consistent with other helheim modals. */
.helheim-modal--image.modal.fade .modal-dialog {
    transform: none;
}

.helheim-modal--image.modal.fade #imagePreviewImage {
    transform: scale(0.92);
    opacity: 0;
    transition: transform 180ms cubic-bezier(0.16, 1, 0.3, 1), opacity 180ms ease;
}

.helheim-modal--image.modal.show #imagePreviewImage {
    transform: scale(1);
    opacity: 1;
}

.helheim-modal--image.modal.fade .image-preview__filename,
.helheim-modal--image.modal.fade .image-preview__close,
.helheim-modal--image.modal.fade .image-preview__download,
.helheim-modal--image.modal.fade .image-preview__nav,
.helheim-modal--image.modal.fade .image-preview__count {
    opacity: 0;
    transform: translateY(-8px);
    transition: opacity 180ms ease 30ms, transform 180ms cubic-bezier(0.16, 1, 0.3, 1) 30ms;
}

.helheim-modal--image.modal.show .image-preview__filename,
.helheim-modal--image.modal.show .image-preview__close,
.helheim-modal--image.modal.show .image-preview__download,
.helheim-modal--image.modal.show .image-preview__nav,
.helheim-modal--image.modal.show .image-preview__count {
    opacity: 1;
    transform: translateY(0);
}

.helheim-modal--image #imagePreviewImage {
    display: block;
    width: auto;
    height: auto;
    max-width: 100%;
    max-height: calc(100vh - 120px);
    max-height: calc(100dvh - 120px);
    border-radius: 12px;
    background: var(--hm-surface-soft);
    pointer-events: auto;
    /* Horizontal swipes are handled by chat.js (prev/next in a photo run);
     * only vertical pans stay with the browser so the gesture is never
     * hijacked by overscroll or page navigation. */
    touch-action: pan-y;
}

.helheim-modal--image .image-preview__filename {
    position: absolute;
    top: 18px;
    left: 24px;
    z-index: 2;
    max-width: calc(100% - 140px);
    margin: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    /* White on the dark stage in BOTH themes — the filename floats over the
     * scrimmed lightbox, not over a theme surface (the --ms-surface mix
     * would flip to dark text in dark mode and disappear). */
    color: color-mix(in srgb, #ffffff 95%, transparent);
    font-size: 0.95rem;
    font-weight: 600;
    text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
    pointer-events: auto;
}

/* The two floating controls over the preview image share one frosted-circle
 * recipe; only the horizontal offset and a couple of element-specific
 * details differ. A dark translucent scrim keeps the white icons readable on
 * ANY photo — a light-filled image would swallow a white-on-white control. */
.helheim-modal--image .image-preview__close,
.helheim-modal--image .image-preview__download {
    position: absolute;
    top: 14px;
    z-index: 2;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border: 0;
    border-radius: 50%;
    color: #fff;
    background: rgba(0, 0, 0, 0.45);
    transition:
        background var(--hm-ease),
        transform var(--hm-ease);
    pointer-events: auto;
}

/* Close is a real button: pointer cursor and the × glyph sizing. */
.helheim-modal--image .image-preview__close {
    right: 16px;
    font-size: 1.05rem;
    line-height: 1;
    cursor: pointer;
}

/* Download stays a link so the browser performs a real file download from the
 * app's authenticated endpoint — hence the underline removal. */
.helheim-modal--image .image-preview__download {
    right: 64px;
    text-decoration: none;
}

.helheim-modal--image .image-preview__close:hover,
.helheim-modal--image .image-preview__download:hover {
    background: rgba(0, 0, 0, 0.6);
}

.helheim-modal--image .image-preview__close:active,
.helheim-modal--image .image-preview__download:active {
    transform: scale(0.94);
}

/* Prev/next arrows for a media group's photos — dark frosted circles flanking
 * the image, shown only when the open preview belongs to a gallery of more
 * than one photo (chat.js toggles them). The dark scrim keeps the arrows
 * visible over light photos too (same recipe as the count pill below).
 * Hidden arrows (first/last photo) render inert. */
.helheim-modal--image .image-preview__nav {
    position: absolute;
    top: 50%;
    z-index: 2;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    color: #fff;
    background: rgba(0, 0, 0, 0.45);
    cursor: pointer;
    pointer-events: auto;
    transform: translateY(-50%);
    transition:
        background var(--hm-ease),
        transform var(--hm-ease);
}

.helheim-modal--image .image-preview__nav:hover {
    background: rgba(0, 0, 0, 0.6);
}

.helheim-modal--image .image-preview__nav:active {
    transform: translateY(-50%) scale(0.94);
}

/* Edge arrows (first/last photo) are visibly inert: dimmed, lighter
 * scrim, and immune to the hover/active feedback of enabled arrows.
 * The .show qualifier is needed — .modal.show .image-preview__nav
 * (4 classes) would otherwise force opacity: 1 and win the cascade. */
.helheim-modal--image.modal.show .image-preview__nav[disabled],
.helheim-modal--image .image-preview__nav[disabled] {
    opacity: 0.35;
    background: rgba(0, 0, 0, 0.22);
    cursor: default;
}

.helheim-modal--image .image-preview__nav[disabled]:hover,
.helheim-modal--image .image-preview__nav[disabled]:active {
    background: rgba(0, 0, 0, 0.22);
    transform: translateY(-50%);
}

.helheim-modal--image .image-preview__nav--prev {
    left: 16px;
}

.helheim-modal--image .image-preview__nav--next {
    right: 16px;
}

.helheim-modal--image .image-preview__nav[hidden] {
    display: none;
}

.helheim-modal--image .image-preview__count {
    position: absolute;
    bottom: 18px;
    left: 50%;
    z-index: 2;
    padding: 4px 12px;
    border-radius: 999px;
    color: #fff;
    background: rgba(0, 0, 0, 0.45);
    font-size: 0.8rem;
    font-weight: 600;
    transform: translateX(-50%);
    pointer-events: auto;
}

.helheim-modal--image .image-preview__count[hidden] {
    display: none;
}

/* On mobile, profile panel behaves like chat panel (full width) */
@media (max-width: 767.98px) {
    .messenger-layout.messenger--profile-open .messenger-sidebar {
        display: none;
    }

    .messenger-profile__body {
        padding: 1.5rem 1rem;
        /* Keep the PWA bottom safe-area (see the base rule) under the mobile
         * shorthand, which would otherwise reset padding-bottom to 1rem. */
        padding-bottom: calc(1.5rem + env(safe-area-inset-bottom, 0px));
    }

    .messenger-profile__card {
        padding: 1.25rem;
    }

    .messenger-profile__avatar {
        width: 78px;
        height: 78px;
    }
}

/* 7.9 · ?chat= deep-link first-paint guard */

/* html.has-chat-param is set by an inline head script in layout.php when the
 * URL carries a ?chat= param — the offline shell is precached by the service
 * worker at its bare URL (no query), so the markup alone cannot know a
 * conversation is being restored. These rules hide the "select a chat" empty
 * state and show the chat panel's loading view from the very first frame;
 * messenger.js then restores the cached thread (or falls back to the empty
 * state when nothing is cached). Mirrors the JS pre-show, applied via CSS so
 * no frame can ever flash the wrong screen. */
html.has-chat-param #emptyState {
    display: none !important;
}

html.has-chat-param #chatPanel {
    display: flex !important;
}

/* 7.10 · Reconnect banner (offline shell) */

/* Shown by messenger.js when connectivity returns while the account-free
 * offline shell is on screen: the page was served by the service worker
 * without a live server render/session, so going online needs a reload
 * instead of a silent flip. Fixed top bar, visible wherever the user
 * scrolled. */
.messenger-reconnect {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 1035;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px 16px;
    padding-top: calc(12px + env(safe-area-inset-top, 0px));
    padding-right: calc(16px + env(safe-area-inset-right, 0px));
    padding-left: calc(16px + env(safe-area-inset-left, 0px));
    color: var(--hm-on-primary);
    background: var(--hm-blue-strong);
    font-size: 0.85rem;
    line-height: 1.4;
}

/* The hidden attribute must win over the flex display above. */
.messenger-reconnect[hidden] {
    display: none !important;
}

.messenger-reconnect__icon {
    flex: none;
}

.messenger-reconnect__text {
    flex: 1;
    min-width: 0;
}

.messenger-reconnect__reload {
    flex: none;
    padding: 7px 16px;
    border: 0;
    border-radius: 999px;
    color: var(--hm-blue-strong);
    background: var(--ms-surface);
    font-size: 0.8rem;
    font-weight: 700;
    cursor: pointer;
    transition: transform var(--hm-ease);
}

.messenger-reconnect__reload:hover {
    transform: translateY(-1px);
}

.messenger-reconnect__dismiss {
    flex: none;
    display: grid;
    place-items: center;
    width: 26px;
    height: 26px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    color: rgba(255, 255, 255, 0.85);
    background: rgba(255, 255, 255, 0.16);
    font-size: 0.75rem;
    line-height: 1;
    cursor: pointer;
    transition: background var(--hm-ease);
}

.messenger-reconnect__dismiss:hover {
    background: rgba(255, 255, 255, 0.28);
}

/* Narrow phones: the single-row layout (icon + text + Reload + dismiss)
 * cramps the message and squeezes the buttons, so the banner stacks — the
 * text takes the first row, the Reload button a full-width second row and
 * the ✕ dismiss a full-width bar underneath. The leading icon is dropped
 * (no room for it next to the message), and the dismiss becomes a proper
 * full-width button instead of the small circle used in the one-row bar. */
@media (max-width: 479.98px) {
    .messenger-reconnect {
        flex-wrap: wrap;
        row-gap: 8px;
    }

    .messenger-reconnect__icon {
        display: none;
    }

    .messenger-reconnect__text {
        flex: 1 1 auto;
    }

    .messenger-reconnect__reload {
        flex: 1 1 100%;
        text-align: center;
    }

    .messenger-reconnect__dismiss {
        flex: 1 1 100%;
        width: 100%;
        height: 32px;
        border-radius: 999px;
        font-size: 0.8rem;
    }
}

/* 8 · Responsive: mobile shows one pane at a time */
@media (max-width: 767.98px) {
    .messenger-layout {
        max-width: none;
    }

    .messenger-sidebar {
        width: 100%;
        min-width: 0;
    }

    .messenger-layout.messenger--chat-open .messenger-sidebar {
        display: none;
    }

    /* Same single-pane intent for the head-guard class (see §7.9), applied
     * before the JS adds messenger--chat-open on the first frame. */
    html.has-chat-param .messenger-sidebar {
        display: none;
    }

    /* On mobile, when the sidebar covers the screen, the empty state
     * ("Select a chat") must not peek through behind it. It becomes
     * visible again once a chat is opened (.messenger--chat-open). */
    .messenger-layout:not(.messenger--chat-open) .messenger-empty {
        display: none;
    }

    .messenger-chat__back {
        display: grid;
    }

    /* Global content header: back chevron appears (the sidebar is hidden on
     * mobile) and the header padding tightens to match the template. */
    .hm-page-header__back {
        display: grid;
    }

    .hm-page-header {
        padding-left: 8px;
        padding-right: 14px;
    }

    /* The chat header tightens on narrow screens; the sidebar header keeps
     * its steady 9px 14px box at every width. */
    .messenger-chat__header {
        padding-left: 6px;
        padding-right: 8px;
    }

    .messenger-chat__messages {
        padding: 14px 10px;
        /* Same PWA-safe bottom under the mobile shorthand. */
        padding-bottom: calc(14px + env(safe-area-inset-bottom, 0px));
    }

    .messenger-bubble {
        max-width: 86%;
    }

    .messenger-composer {
        /* A touch more breathing room for the thumb + the PWA home indicator
         * (safe-area; 0 on browsers without a notch). Deliberately modest. */
        padding: 10px 8px calc(12px + env(safe-area-inset-bottom, 0px));
    }

    .messenger-typing {
        padding-left: 10px;
        padding-right: 10px;
    }
}

/* 9 · Responsive: sidebar compact */
@media (max-width: 480px) {
    .messenger-sidebar__avatar {
        width: 46px;
        height: 46px;
        font-size: 0.95rem;
    }

    .messenger-sidebar__search {
        padding: 10px 10px 7px;
    }

    .conversation-item {
        min-height: 64px;
        padding: 8px;
    }

    .conversation-item__avatar,
    .conversation-item__avatar-wrap,
    .conversation-item__avatar-wrap .hm-avatar-initial {
        width: 44px;
        height: 44px;
    }

    .messenger-sidebar__dropdown .icon-button,
    .messenger-sidebar__security {
        width: 36px;
        height: 36px;
    }
}

/* 10 · Authentication (Phase 1)
 * Standalone login / account-activation page, built on the --hm-*
 * tokens above. The backdrop is scoped to .auth-page so the rest of
 * the app keeps its own background until it is migrated later. */

/* 10.1 · Backdrop & layout */

/* Full-bleed solid backdrop — the layered gradient and woven-hatch texture
 * are gone; the page is a calm, flat neutral. */
.auth-page {
    width: 100%;
    height: 100%;
    margin: 0;
    overflow: hidden;
    color: var(--hm-text);
    font-family: var(--hm-font-sans);
    -webkit-font-smoothing: antialiased;
    background: var(--hm-bg);
}

/* Buttons on the auth page must not keep the browser's default chrome
 * border/background (the client template resets this globally; here we
 * scope it so the rest of the app stays untouched). */
.auth-page button {
    border: 0;
    font-family: inherit;
    cursor: pointer;
}

/* Centered auth viewport; scrolls internally on short screens. */
.auth-screen {
    display: grid;
    place-items: center;
    width: 100%;
    height: 100vh;
    height: 100svh;
    padding: 22px 16px;
    overflow-y: auto;
    /* Vertical-only scroll: the OTP digit row (and every other stage) must
     * never add a horizontal scrollbar on narrow screens. */
    overflow-x: hidden;
}

.auth-shell {
    width: min(100%, 400px);
    text-align: center;
}

/* 10.2 · Logo & wordmark */

/* Auth logo shell: centers the dark logo card whose PNG mark sits on a
 * custom deep-navy backdrop — a deliberate contrast moment above the pale
 * flat backdrop and the sans wordmark. */
.auth-logo-shell {
    display: flex;
    justify-content: center;
    margin-bottom: 18px;
}

.auth-logo-card {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 104px;
    height: 104px;
}

.auth-logo-image {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
    filter: drop-shadow(0 8px 18px rgba(0, 0, 0, 0.35));
}

/* Wordmark — a single-word brand renders large and grand, in the same
 * sans face as the rest of the app. */
.auth-title {
    color: var(--hm-navy);
    font-size: clamp(2.15rem, 8vw, 3.7rem);
    font-weight: 600;
    line-height: 1;
    letter-spacing: 0.045em;
    text-indent: 0.045em;
    text-transform: uppercase;
}

.auth-subtitle {
    margin-top: 14px;
    color: var(--hm-muted);
    font-size: clamp(1rem, 2.8vw, 1.16rem);
    font-weight: 400;
    line-height: 1.35;
    letter-spacing: 0.018em;
    text-transform: none;
}

/* 10.3 · Auth card & stages */

/* The solid card holding the auth stages — same surface language as every
 * other panel in the app (flat, hairline-edged); no frosted glass. */
.auth-card {
    margin-top: 26px;
    padding: 20px;
    border: 1px solid var(--hm-line);
    border-radius: 24px;
    text-align: left;
    background: var(--ms-surface);
}

/* Stages swap by toggling .active (see login.js). */
.auth-stage {
    display: none;
}

.auth-stage.active {
    display: block;
}

.auth-heading {
    margin-bottom: 7px;
    color: var(--hm-navy);
    font-size: 1rem;
    font-weight: 700;
}

.auth-description {
    margin-bottom: 15px;
    color: var(--hm-muted);
    font-size: 0.82rem;
    line-height: 1.45;
}

/* 10.4 · Form fields */

/* Form fields */
.field {
    margin-bottom: 12px;
}

.field label {
    display: block;
    margin-bottom: 7px;
    color: var(--hm-muted);
    font-size: 0.74rem;
    font-weight: 700;
}

/* Bootstrap .form-label override — the HM card-list kicker (uppercase) used
 * by the Profile / Preferences rows. */
.form-label {
    display: block;
    margin-bottom: .5rem;
    color: var(--hm-muted);
    font-size: .72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .06em;
}

/* One canonical HM input recipe shared by the .field wrappers (auth, modal
 * bodies) and Bootstrap's form-control / form-select classes. Height 44px
 * matches the .btn buttons so an input + button row aligns flush. */
.field input,
.field select,
.field textarea,
.form-control,
.form-select {
    display: block;
    width: 100%;
    height: 44px;
    padding: 0 15px;
    border: 1px solid var(--hm-line);
    border-radius: 14px;
    outline: 0;
    color: var(--hm-text);
    font-size: 0.9rem;
    font-family: inherit;
    background: color-mix(in srgb, var(--ms-surface) 86%, transparent);
    transition:
        border-color var(--hm-ease),
        background var(--hm-ease);
}

.field input::placeholder,
.field textarea::placeholder,
.form-control::placeholder {
    color: var(--hm-faint);
}

/* Stacked .form-control fields without a .field wrapper (e.g. the profile
 * password card) get the same 12px rhythm between fields. */
.form-control+.form-control {
    margin-top: 12px;
}

.field input:focus,
.field select:focus,
.field textarea:focus,
.form-control:focus,
.form-select:focus {
    background: var(--ms-surface);
    box-shadow: none;
    /* Pin the resting border on focus. Bootstrap's own .form-control:focus
       paints a BLUE border-color (its $input-focus-border-color) — silencing
       only the box-shadow leaves that blue border behind. Keeping the resting
       --hm-line color here means a focused field never changes color on its
       own; the only border the toggle can add is the gated black one below. */
    border-color: var(--hm-line);
}

/* Black focus border — gated on the focusBorder toggle like the other
 * focus sites (see the search pill above). */
html.hm-focus-border .field input:focus,
html.hm-focus-border .field select:focus,
html.hm-focus-border .field textarea:focus,
html.hm-focus-border .form-control:focus,
html.hm-focus-border .form-select:focus {
    border-color: #000;
}

/* Bootstrap paints blue/red/green focus halos on fields, checkboxes and
 * buttons — the app's focus state is the single black border, so silence
 * every Bootstrap halo (including the validation variants). */
.form-control.is-invalid:focus,
.form-select.is-invalid:focus,
.form-control.is-valid:focus,
.form-select.is-valid:focus,
.form-check-input:focus,
.btn:focus,
.btn:focus-visible {
    box-shadow: none;
}

/* Selects: native arrow removed; the HM chevron is an overlay element
 * (.position-relative wrap + .position-absolute svg in the markup) — a
 * background-image on <select> shifts when the dropdown opens because the
 * dropdown scrolls the box. Right padding keeps text clear of the overlay. */
.field select,
.form-select {
    cursor: pointer;
    -webkit-appearance: none;
    appearance: none;
    padding-right: 2.25rem;
}

.field select option,
.form-select option {
    color: var(--hm-text);
    background: var(--ms-surface);
}

/* 10.5 · Alerts */

/* ---- Alerts (Bootstrap .alert overrides) ----
 * Every alert type shares one HM recipe: a tinted surface, a hairline border
 * and a strong text on the brand palette — Bootstrap's flat blue / red /
 * yellow fills are replaced with the Helheim tints. */
.alert {
    border-radius: 12px;
    font-size: 0.82rem;
    line-height: 1.5;
}

/* Alerts inside modals use the compact recipe — Bootstrap's 1rem padding and
 * 1rem bottom margin would bulk up a modal body. */
.helheim-modal .alert {
    padding: 0.5rem 0.75rem;
    margin-top: 0.5rem;
    margin-bottom: 0;
}

.alert-success {
    background: var(--hm-success-alert-bg);
    border-color: var(--hm-success-alert-line);
    color: var(--hm-success-alert-text);
}

.alert-danger {
    background: var(--hm-danger-alert-bg);
    border-color: var(--hm-danger-alert-line);
    color: var(--hm-red);
}

.alert-warning {
    background: var(--hm-warning-alert-bg);
    border-color: var(--hm-warning-alert-line);
    color: var(--hm-warning-alert-text);
}

.alert-info {
    background: var(--hm-info-alert-bg);
    border-color: var(--hm-info-alert-line);
    color: var(--hm-blue-strong);
}

/* Bootstrap danger text aligns with the HM danger family (the brown used by
 * .alert-danger, .auth-status.error and logout items). */
.text-danger {
    color: var(--hm-red);
}

/* Bootstrap muted text (used across the admin views) aligns with the HM
 * muted token, so every muted sentence shares the app's one quiet grey.
 * !important matches Bootstrap's own text utilities, which are !important. */
.text-muted {
    color: var(--hm-muted) !important;
}

/* 10.6 · Buttons & busy state */

/* Buttons — the HM primary / secondary / danger recipes applied to Bootstrap's
 * own semantic button classes (.btn-primary / .btn-outline-secondary /
 * .btn-danger), so markup uses standard Bootstrap buttons instead of custom
 * classes. ONE height (44px) for every text button — no per-context sizes, the
 * .btn-sm modifier covers compact spots. Specificity (.btn.btn-primary = 0,3,0)
 * + load order beat Bootstrap's .btn-primary (0,1,0); every state is re-pinned
 * so Bootstrap's default blue / gray / reset colors never leak through. */
.btn.btn-primary,
.btn.btn-outline-secondary,
.btn.btn-danger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    border-radius: 14px;
    font-family: inherit;
    --bs-btn-active-shadow: none;
    --bs-btn-disabled-bg: transparent;
    cursor: pointer;
}

.btn.btn-primary,
.btn.btn-danger {
    padding: 0 18px;
    border: 0;
    font-weight: 700;
    transition:
        transform var(--hm-ease);
}

/* Primary: labels sit on the brand green in the configured on-primary color
 * (--hm-on-primary → onPrimaryColor in app/Config/Messenger.php). */
.btn.btn-primary {
    color: var(--hm-on-primary);
    background: var(--hm-primary-bg);
}

.btn.btn-primary:hover,
.btn.btn-primary:focus-visible,
.btn.btn-primary.show {
    background: var(--hm-primary-hover);
    color: var(--hm-on-primary);
}

.btn.btn-primary:active,
.btn.btn-primary.active {
    background: var(--hm-primary-hover);
    box-shadow: none;
}

.btn.btn-danger {
    color: var(--hm-on-primary);
    background: var(--hm-danger-bg);
}

.btn.btn-danger:hover,
.btn.btn-danger:focus-visible {
    background: var(--hm-danger-hover);
    color: var(--hm-on-primary);
}

.btn.btn-danger:active,
.btn.btn-danger.active {
    background: var(--hm-danger-hover);
    box-shadow: none;
}

.btn.btn-outline-secondary {
    padding: 0 16px;
    border: 0;
    color: var(--hm-navy);
    font-weight: 700;
    background: var(--hm-secondary-bg);
    transition:
        background var(--hm-ease),
        transform var(--hm-ease);
}

.btn.btn-outline-secondary:hover,
.btn.btn-outline-secondary:focus-visible {
    background: var(--hm-secondary-hover);
    color: var(--hm-navy);
}

.btn.btn-outline-secondary:active,
.btn.btn-outline-secondary.active {
    background: var(--hm-secondary-hover);
    box-shadow: none;
}

.btn.btn-primary:disabled,
.btn.btn-outline-secondary:disabled,
.btn.btn-danger:disabled {
    opacity: 0.65;
    box-shadow: none;
    cursor: default;
}

/* Busy-state loading indicator injected by MessengerUtils.setButtonBusy.
 * Self-contained (no Bootstrap dependency) so it works on the auth screen,
 * which loads without the Bootstrap stylesheet. The button is a flex
 * container, so the spinner and label are flex items: the gap rule supplies
 * the spacing (a text node between them would be collapsed by flexbox). */
.hm-btn-busy__spinner,
.messenger-file-attachment__download.is-busy::before,
.image-preview__download.is-busy::before {
    width: 1em;
    height: 1em;
    border: 2px solid currentColor;
    border-right-color: transparent;
    border-radius: 50%;
    flex: none;
    animation: hm-btn-busy-spin 0.7s linear infinite;
}

.hm-btn-busy__label {
    margin-left: 8px;
    flex: none;
}

@keyframes hm-btn-busy-spin {
    to {
        transform: rotate(360deg);
    }
}

/* Small size — Bootstrap's .btn-sm modifier at HM scale, for compact table row
 * actions and the profile/preferences change chips. Same recipes, smaller
 * footprint (no lift shadow at this size). */
.btn.btn-primary.btn-sm,
.btn.btn-outline-secondary.btn-sm,
.btn.btn-danger.btn-sm {
    min-height: 32px;
    padding: 0 12px;
    border-radius: 10px;
    font-size: 0.8rem;
    box-shadow: none;
}

/* Split-button caret half — Bootstrap's .dropdown-toggle-split at HM scale; the
 * shared .btn.btn-primary padding would otherwise leave the caret half too
 * wide. Bootstrap's own .btn-group rules handle the corner squaring. */
.btn-group>.btn.dropdown-toggle-split {
    padding-right: 13px;
    padding-left: 13px;
}

/* Global press interaction — one grouped :active rule gives every clickable
 * control the same subtle push-down while the pointer is down (transform from
 * --hm-press). A translate is used instead of a scale so text never renders
 * thinner. The old per-component :active transforms live here now, so adding a
 * new control to the app's interaction language means extending this list, not
 * writing another page-local rule. */
.dropdown-item:active,
.btn-primary:active,
.btn-outline-secondary:active,
.btn-danger:active,
.icon-button:active,
.messenger-profile__avatar-edit-btn:active,
.messenger-sidebar__archives-btn:active,
.messenger-search__new:active,
.send-button:active,
.conversation-item:active,
.admin-sidebar__link:active,
.admin-stat:active,
.modal-close:active,
.hm-pagination__link:not(.is-disabled):not(.is-active):active {
    transform: var(--hm-press);
}

/* In the auth card the primary button always spans the full width. */
.auth-card .btn-primary {
    width: 100%;
}

/* Ghost "Back" link under primary buttons. */
.auth-back {
    width: 100%;
    margin-top: 9px;
    padding: 10px;
    color: var(--hm-muted);
    background: transparent;
    font-family: inherit;
    font-size: 0.82rem;
    border: 0;
    cursor: pointer;
}

.auth-back:hover {
    color: var(--hm-text);
}

/* 10.6b · Two-factor code entry (login step) */

/* Six digit boxes — one character each, chained by login.js (auto-advance,
 * backspace, arrows and paste). Same recipe as the .field inputs (--hm-line
 * border, 14px radius, resting-on-focus) but square, centered and a touch
 * taller so the code reads like a code. */
.otp-inputs {
    display: flex;
    gap: 8px;
    margin-bottom: 14px;
    width: 100%;
}

.otp-input {
    flex: 1 1 0;
    min-width: 0;
    width: 0;
    height: 52px;
    padding: 0;
    border: 1px solid var(--hm-line);
    border-radius: 14px;
    outline: 0;
    color: var(--hm-text);
    background: color-mix(in srgb, var(--ms-surface) 86%, transparent);
    font-family: inherit;
    font-size: 1.3rem;
    font-weight: 700;
    text-align: center;
    transition: border-color var(--hm-ease), background var(--hm-ease);
}

.otp-input:focus {
    border-color: var(--hm-line);
    background: var(--ms-surface);
    box-shadow: none;
}

html.hm-focus-border .otp-input:focus {
    border-color: #000;
}

/* 10.7 · Status lines & footer */

/* Per-stage status line. Hidden while empty so it never reserves space;
 * appears only once it actually carries a message. */
.auth-status {
    margin-top: 11px;
    color: var(--hm-muted);
    font-size: 0.74rem;
    text-align: center;
}

.auth-status:empty {
    display: none;
}

.auth-status.error {
    color: var(--hm-red);
}

.auth-status.success {
    color: var(--hm-green);
}

/* Account-blocked notice on the login card (landed here via forced sign-out). */
.auth-status--blocked {
    margin-top: 0;
    margin-bottom: 2px;
    padding: 9px 12px;
    border: 1px solid var(--hm-red);
    border-radius: 6px;
    background: var(--hm-red);
    color: var(--hm-on-primary);
    font-weight: 600;
    line-height: 1.35;
}

/* Demo-access footer, rendered only when credentials are configured. */
.demo-access {
    margin-top: 17px;
    padding-top: 14px;
    border-top: 1px solid var(--hm-line);
    color: var(--hm-muted);
    font-size: 0.77rem;
    line-height: 1.7;
    text-align: center;
}

.demo-title {
    color: var(--hm-blue);
    font-weight: 700;
}

.demo-access strong {
    color: var(--hm-navy);
    font-weight: 700;
}

/* Small helper used to hide things without Bootstrap (login page). */
.hidden {
    display: none !important;
}

/* 10.8 · Two-factor authentication settings page */

.hm-card-title {
    margin: 0 0 6px;
    color: var(--hm-navy);
    font-size: 1rem;
    font-weight: 700;
}

/* Numbered setup steps — the circled numbers use the primary tint so the
 * steps read as part of the product accent without shouting. */
.twofa-steps {
    display: flex;
    flex-direction: column;
    gap: 14px;
    margin: 18px 0 0;
    padding: 0;
    list-style: none;
}

.twofa-steps__item {
    display: flex;
    align-items: flex-start;
    gap: 12px;
}

.twofa-steps__num {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    background: color-mix(in srgb, var(--ms-accent) 12%, transparent);
    color: var(--ms-accent);
    font-size: 0.78rem;
    font-weight: 700;
}

.twofa-steps__item strong {
    display: block;
    margin-bottom: 2px;
    color: var(--hm-text);
    font-size: 0.86rem;
    font-weight: 700;
}

/* QR section — a clean white pad so the black QR modules stay crisp on both
 * light and dark surfaces. Spacing comes from margins (no divider lines). */
.twofa-qr {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-top: 20px;
}

.twofa-qr__label {
    margin-bottom: 10px;
    text-align: center;
}

.twofa-qr__frame {
    padding: 12px;
    border: 1px solid var(--hm-line);
    border-radius: 18px;
    background: #fff;
}

.twofa-qr__frame svg,
.twofa-qr__frame img {
    display: block;
    width: 192px;
    height: 192px;
}

.twofa-qr__hint {
    margin-top: 10px;
    max-width: 340px;
    text-align: center;
}

/* Secret key — monospace, one-tap-select, ellipsis when too narrow. */
.twofa-secret {
    margin-top: 18px;
    padding: 14px;
    border: 1px solid var(--hm-line);
    border-radius: 14px;
    background: color-mix(in srgb, var(--ms-surface) 60%, transparent);
}

.twofa-secret__row {
    display: flex;
    align-items: stretch;
    gap: 10px;
    margin-top: 10px;
}

/* The value box matches the 44px button height next to it, so the copy row
 * reads as one control. */
.twofa-secret__value {
    display: flex;
    align-items: center;
    flex: 1;
    min-width: 0;
    height: 44px;
    padding: 0 12px;
    border: 1px solid var(--hm-line);
    border-radius: 14px;
    background: var(--hm-surface);
    color: var(--hm-text);
    font-family: var(--hm-font-mono);
    font-size: 0.9rem;
    letter-spacing: 0.08em;
    user-select: all;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Confirm / disable section — flows directly in the card (no nested surface),
 * with the code field and action button spanning the full card width. When it
 * is the only content of a card (enabled state) it starts flush at the top. */
.twofa-verify {
    margin-top: 20px;
}

.hm-card>.twofa-verify:first-child {
    margin-top: 0;
}

.twofa-verify__hint {
    margin-bottom: 12px;
}

.twofa-verify__field {
    margin-bottom: 12px;
}

.twofa-verify .btn {
    width: 100%;
}

.twofa-status {
    margin-top: 11px;
    color: var(--hm-muted);
    font-size: 0.78rem;
}

.twofa-status:empty {
    display: none;
}

.twofa-status.error {
    color: var(--hm-red);
}

.twofa-status.success {
    color: var(--hm-green);
}

.twofa-regenerate {
    margin-top: 4px;
    text-align: center;
}

/* 10.9 · Two-factor settings page responsive */

/* The card content adapts on narrow screens: the QR shrinks so it never
 * overflows the card, the secret row (value + copy) stacks full-width, and
 * the confirm panel keeps its full-width button. */
@media (max-width: 480px) {

    .twofa-qr__frame svg,
    .twofa-qr__frame img {
        width: 156px;
        height: 156px;
    }

    .twofa-secret__row {
        flex-direction: column;
    }

    .twofa-secret__row .btn {
        width: 100%;
    }
}

/* 10.9 · Security Protocol page — the admin-authored overview card. The
 * content typography deliberately mirrors the admin editor's (admin.css),
 * so what the admin formats is exactly what users read. */

.security-protocol {
    font-size: 0.95rem;
    line-height: 1.6;
}

.security-protocol h2 {
    margin: 1.2em 0 0.5em;
    font-size: 1.25rem;
    font-weight: 700;
    line-height: 1.3;
}

.security-protocol h3 {
    margin: 1.1em 0 0.45em;
    font-size: 1.05rem;
    font-weight: 700;
    line-height: 1.35;
}

.security-protocol h2:first-child,
.security-protocol h3:first-child {
    margin-top: 0;
}

.security-protocol p {
    margin: 0 0 0.8em;
}

.security-protocol p:last-child {
    margin-bottom: 0;
}

.security-protocol ul,
.security-protocol ol {
    margin: 0 0 0.8em;
    padding-left: 1.4em;
}

.security-protocol li {
    margin: 0.25em 0;
}

.security-protocol blockquote {
    margin: 0.8em 0;
    padding: 0.5em 1em;
    border-left: 3px solid var(--ms-accent);
    border-radius: 0 10px 10px 0;
    background: color-mix(in srgb, var(--ms-accent) 6%, transparent);
    color: var(--hm-navy-soft);
}

.security-protocol a {
    color: var(--ms-accent);
    text-decoration: underline;
    text-underline-offset: 2px;
}

.security-protocol code,
.security-protocol pre {
    font-family: ui-monospace, "Cascadia Code", Consolas, monospace;
    font-size: 0.85em;
}

.security-protocol code {
    padding: 0.15em 0.35em;
    border-radius: 6px;
    background: color-mix(in srgb, var(--ms-surface) 76%, transparent);
}

.security-protocol pre {
    margin: 0.8em 0;
    padding: 0.9em 1em;
    overflow-x: auto;
    border-radius: 10px;
    background: color-mix(in srgb, var(--hm-navy) 5%, transparent);
}

/* Empty state — a centered shield over the "not published yet" copy. */
.security-protocol__empty {
    padding: 18px 6px;
    text-align: center;
}

.security-protocol__empty-icon {
    display: grid;
    place-items: center;
    width: 68px;
    height: 68px;
    margin: 0 auto 14px;
    border-radius: 50%;
    color: var(--ms-accent);
    background: color-mix(in srgb, var(--ms-accent) 10%, transparent);
}

.security-protocol__empty .hm-card-title {
    margin-bottom: 8px;
}

/* 11 · Authentication responsive tweaks */
@media (max-width: 480px) {
    .auth-screen {
        padding: 20px 13px;
    }

    .auth-logo-card {
        width: 90px;
        height: 90px;
        border-radius: 24px;
    }

    .auth-title {
        font-size: clamp(2rem, 11vw, 3rem);
    }

    .auth-card {
        padding: 17px;
        border-radius: 22px;
    }

    /* The six TOTP boxes on the login step tighten on small screens so they
     * always fit the card without wrapping. */
    .otp-inputs {
        gap: 6px;
        margin-bottom: 12px;
    }

    .otp-input {
        height: 46px;
        border-radius: 12px;
        font-size: 1.15rem;
    }
}

/* On short viewports (phones with the keyboard open, landscape) the auth
 * stages must fit without scrolling: the logo shrinks and vertical spacing
 * tightens so the OTP step never pushes content past 100svh. */
@media (max-height: 720px) {
    .auth-screen {
        padding: 14px 16px;
    }

    .auth-logo-shell {
        margin-bottom: 10px;
    }

    .auth-logo-card {
        width: 76px;
        height: 76px;
    }

    .auth-title {
        font-size: clamp(1.6rem, 5vw, 2.4rem);
    }

    .auth-subtitle {
        margin-top: 8px;
        font-size: 0.95rem;
    }

    .auth-card {
        margin-top: 14px;
        padding: 16px;
    }
}

/* 12 · Toasts */

/* Top-right toast stack. The container is pointer-events:none so the gaps
   between toasts never block the page; each toast re-enables them. */
.hm-toast-container {
    position: fixed;
    top: 20px;
    right: 20px;
    z-index: 2000;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 10px;
    width: min(356px, calc(100vw - 40px));
    max-width: calc(100vw - 40px);
    pointer-events: none;
}

.hm-toast {
    position: relative;
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    padding: 14px 12px 14px 14px;
    overflow: hidden;
    border: 1px solid var(--hm-line-strong);
    border-radius: 16px;
    background: var(--ms-surface);
    pointer-events: auto;
    animation: hm-toast-in 260ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
}

/* Leave transition, driven by JS (adds .is-leaving after the enter
   animation's fill has been released). */
.hm-toast.is-leaving {
    opacity: 0;
    transform: translateX(24px);
    transition:
        opacity 150ms ease,
        transform 260ms cubic-bezier(0.2, 0.8, 0.2, 1);
}

@keyframes hm-toast-in {
    from {
        opacity: 0;
        transform: translateX(24px);
    }

    to {
        opacity: 1;
        transform: translateX(0);
    }
}

/* Colored rune tile, tinted per variant. */
.hm-toast__icon {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    border-radius: 10px;
}

.hm-toast.is-success .hm-toast__icon {
    color: var(--hm-green);
    background: var(--hm-success-tint-soft);
}

.hm-toast.is-error .hm-toast__icon {
    color: var(--hm-red);
    background: color-mix(in srgb, var(--ms-danger) 12%, transparent);
}

.hm-toast.is-info .hm-toast__icon {
    color: var(--hm-blue);
    background: color-mix(in srgb, var(--hm-blue-strong) 12%, transparent);
}

.hm-toast.is-warning .hm-toast__icon {
    color: var(--hm-warning);
    background: var(--hm-warning-tint-mid);
}

.hm-toast__content {
    flex: 1 1 auto;
    min-width: 0;
    padding-top: 2px;
}

.hm-toast__body {
    color: var(--hm-muted);
    font-size: 0.8rem;
    line-height: 1.45;
    overflow-wrap: anywhere;
}

.hm-toast__close {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    margin: -2px -4px 0 0;
    padding: 0;
    border: 0;
    border-radius: 7px;
    color: var(--hm-faint);
    background: transparent;
    cursor: pointer;
    transition:
        color var(--hm-ease),
        background var(--hm-ease);
}

.hm-toast__close:hover {
    color: var(--hm-navy);
    background: var(--hm-surface-blue);
}

/* Countdown bar along the bottom edge, width driven by JS. */
.hm-toast__progress {
    position: absolute;
    left: 14px;
    right: 14px;
    bottom: 0;
    height: 2px;
    border-radius: 2px 2px 0 0;
    background: color-mix(in srgb, var(--hm-navy) 10%, transparent);
    overflow: hidden;
}

.hm-toast__progress>span {
    display: block;
    height: 100%;
    background: var(--hm-blue-strong);
}

.hm-toast.is-success .hm-toast__progress>span {
    background: var(--hm-green);
}

.hm-toast.is-error .hm-toast__progress>span {
    background: var(--hm-red);
}

.hm-toast.is-info .hm-toast__progress>span {
    background: var(--hm-blue-strong);
}

.hm-toast.is-warning .hm-toast__progress>span {
    background: var(--hm-warning);
}

@media (max-width: 480px) {
    .hm-toast-container {
        top: 12px;
        right: 12px;
        width: min(320px, calc(100vw - 24px));
        max-width: calc(100vw - 24px);
    }

    .hm-toast {
        padding: 12px 10px 12px 12px;
        gap: 10px;
    }
}

/* 13 · Offline restriction styling (Phase 3b + plan E)
 * body.is-offline (toggled by messenger.js) hides the controls that would
 * hit the server while a connection is missing, rather than disabling and
 * dimming them: the "+" new-chat button, the whole chat menu (its
 * timer/delete items are all server actions), the Security Protocol shield
 * (its page is rendered from a server setting), and the account dropdown's
 * server-dependent items (Profile / Preferences / Admin and Logout — the
 * logout POST cannot reach the server offline). Keeping them visible but
 * inert is both cluttered and ambiguous — hidden is clearer.
 * The Messenger module also disables them and installs a capture-phase
 * click guard as belt-and-braces (hidden elements are neither clickable
 * nor focusable, so nothing becomes unreachable). Clear Local Data stays
 * visible: it is a pure local action that must stay available offline, and
 * so does the Archives entry — switching to the archived-chats view is a
 * local, cache-backed action (see sidebar.js switchSidebarList). The search
 * box stays visible too: typing filters the loaded/cached list client-side
 * (the remote server search is withheld offline), so it is a pure local
 * action. Only the "+" new-chat button stays hidden — it drives server
 * searches/creates. Cached conversations stay clickable so saved chats
 * remain openable. */
body.is-offline .messenger-search__new,
body.is-offline .messenger-chat__actions,
body.is-offline .messenger-bubble__actions,
body.is-offline .conversation-item__archive,
body.is-offline .messenger-sidebar__security,
body.is-offline .messenger-sidebar__dropdown .dropdown-item:not(#clearCacheBtn),
body.is-offline .messenger-sidebar__dropdown li:has(#logoutBtn) {
    display: none !important;
}

/* The swipe gesture is disabled while offline (see sidebar.js — both revealed
 * actions are server round trips). Let the browser handle gestures normally
 * and never paint the action layer, so a horizontal touch cannot tease
 * actions that would just fail. */
body.is-offline .conversation-item-wrap {
    touch-action: auto;
}

body.is-offline .conversation-item__actions {
    display: none;
}

/* The reply chevron is hidden offline (above), so the timestamp must not
 * fade out on hover — with nothing taking its place, fading would leave the
 * time blank. Keeping it at full opacity preserves the readable stream. */
body.is-offline .messenger-bubble:hover .messenger-bubble__time {
    opacity: 1;
}

/* Same trade-off for the conversation rows: the hover reveal replaces the
 * time/badge column with the archive button, but that button is hidden
 * offline — so the meta column must not fade out on hover either. */
body.is-offline .conversation-item:hover .conversation-item__meta {
    opacity: 1;
}

/* While offline nothing can be composed (no offline queue), so the whole
 * composer is hidden rather than merely dimmed — the chat is read-only. The
 * empty-state panel and the blocked notice use the same hide/show approach. */
body.is-offline .messenger-composer {
    display: none !important;
}

/* 14 · Launch splash
 * The app's cover page: a flat dark neutral backdrop, a floating logo and an
 * uppercase wordmark that types itself in — the old vignette, film grain,
 * ambient glows and metallic gradient wordmark are gone, leaving a calm
 * dark cover that hands off to the light app. The once-per-tab gate is
 * unchanged: splash.js marks the visit as seen in sessionStorage, so in-app
 * page loads (login, messenger, admin, profile, preferences) do not replay
 * it. When a tab already saw it, partials/splash.php adds hm-splash-skip to
 * <html> and the overlay never paints. */

:root {
    /* Same svh rationale as .messenger-app: the splash must never extend
     * under the standalone bottom bar (dvh does not resize for it). */
    --app-height: 100vh;
    --app-height: 100svh;
    --safe-top: env(safe-area-inset-top, 0px);
    --safe-bottom: env(safe-area-inset-bottom, 0px);
}

body:not(.app-ready) {
    background: var(--ms-splash, #0d1110) !important;
}

/* Keep the app behind the splash until splash.js marks the page ready. The
 * client targets #messenger; this project's shell uses #messengerLayout, so
 * both are covered. */
body:not(.app-ready) #authScreen,
body:not(.app-ready) #messenger,
body:not(.app-ready) #messengerLayout {
    visibility: hidden;
    opacity: 0;
    pointer-events: none;
}

/* Repeat visits (refresh / in-tab reload / soft nav): the head script already
 * added hm-splash-skip to <html> before the first paint, so override the
 * app-ready gate entirely — the page beneath must render normally and never
 * flash a dark splash frame even though splash.js hasn't run yet. */
html.hm-splash-skip body:not(.app-ready) {
    background: var(--ms-bg, #f1f4f6) !important;
}

html.hm-splash-skip body:not(.app-ready) #authScreen,
html.hm-splash-skip body:not(.app-ready) #messenger,
html.hm-splash-skip body:not(.app-ready) #messengerLayout {
    visibility: visible;
    opacity: 1;
    pointer-events: auto;
}

#splashScreen {
    position: fixed !important;
    top: 0 !important;
    right: 0 !important;
    bottom: 0 !important;
    left: 0 !important;
    inset: 0 !important;
    z-index: 10000 !important;
    display: grid !important;
    place-items: center !important;
    width: 100% !important;
    height: 100vh !important;
    height: var(--app-height, 100svh) !important;
    min-height: 100vh !important;
    min-height: 100svh !important;
    padding:
        calc(24px + var(--safe-top)) clamp(20px, 4vw, 48px) calc(28px + var(--safe-bottom)) !important;
    overflow: hidden !important;
    isolation: isolate !important;
    color: #fff !important;
    background: var(--ms-splash, #0d1110) !important;
    opacity: 1 !important;
    visibility: visible !important;
    transition:
        opacity 950ms cubic-bezier(.22, .76, .22, 1),
        visibility 950ms linear !important;
}

#splashScreen.is-leaving {
    opacity: 0 !important;
    visibility: hidden !important;
    pointer-events: none !important;
}

/* Repeat visits: the tab already saw the splash (sessionStorage flag set in
 * partials/splash.php at parse time), so it must never paint. */
html.hm-splash-skip #splashScreen {
    display: none !important;
}

.final-splash-inner {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: min(100%, 680px);
    margin: 0 auto;
    text-align: center;
    transform: translateY(-1.5vh);
}

.final-splash-logo-wrap {
    position: relative;
    width: clamp(220px, 19.2vw, 316px);
    height: auto;
    margin: 0 auto;
    background: transparent !important;
    border: 0 !important;
    border-radius: 0 !important;
    box-shadow: none !important;
    overflow: visible !important;
    opacity: 0;
    transform: translateY(14px) scale(.965);
    filter: blur(9px) brightness(.66);
    animation: finalLogoReveal 1.95s cubic-bezier(.16, 1, .3, 1) .18s forwards,
        finalFloat 8s ease-in-out 2.4s infinite alternate;
}

.final-splash-logo {
    display: block !important;
    width: 100% !important;
    height: auto !important;
    max-width: none !important;
    object-fit: contain !important;
    object-position: center !important;
    background: transparent !important;
    border: 0 !important;
    border-radius: 0 !important;
    box-shadow: none !important;
    user-select: none !important;
    -webkit-user-drag: none !important;
    filter: drop-shadow(0 22px 30px rgba(0, 0, 0, .38)) !important;
}

.final-splash-wordmark {
    width: min(566px, 78vw);
    margin: clamp(38px, 5.1vh, 52px) 0 0;
}

.final-splash-wordmark svg {
    display: block;
    width: 100%;
    height: auto;
    overflow: visible;
    filter: drop-shadow(0 10px 14px rgba(0, 0, 0, .24));
}

.letter {
    opacity: 0;
    transform: translateY(7px);
    transform-box: fill-box;
    transform-origin: center;
    filter: blur(5px);
    animation: finalLetterReveal 1.25s cubic-bezier(.16, 1, .3, 1) forwards;
}

/* Stagger the per-letter reveal so the wordmark types itself in sequence,
 * starting once the logo has settled in. Matches the 7-letter SVG (HELHEIM);
 * the client's original class names (l1..l7) and timings are preserved. */
.l1 {
    animation-delay: 1.08s;
}

.l2 {
    animation-delay: 1.15s;
}

.l3 {
    animation-delay: 1.22s;
}

.l4 {
    animation-delay: 1.29s;
}

.l5 {
    animation-delay: 1.36s;
}

.l6 {
    animation-delay: 1.43s;
}

.l7 {
    animation-delay: 1.50s;
}

/* Mobile proportions */
@media (max-width: 700px) {
    #splashScreen {
        padding-left: 20px !important;
        padding-right: 20px !important;
    }

    .final-splash-inner {
        transform: translateY(-1vh);
    }

    .final-splash-logo-wrap {
        width: clamp(205px, 57vw, 270px);
    }

    .final-splash-wordmark {
        width: min(510px, 88vw);
        margin-top: 36px;
    }
}

/* Short landscape screens: compact everything so it fits above the fold. */
@media (max-height: 700px) and (min-width: 701px) {
    .final-splash-logo-wrap {
        width: 250px;
    }

    .final-splash-wordmark {
        width: 510px;
        margin-top: 30px;
    }
}

@media (prefers-reduced-motion: reduce) {

    #splashScreen,
    .final-splash-logo-wrap,
    .letter {
        animation: none !important;
    }

    .final-splash-logo-wrap {
        opacity: 1 !important;
        transform: none !important;
        filter: none !important;
    }

    .letter {
        opacity: 1 !important;
        transform: none !important;
        filter: none !important;
    }

    .messenger-bubble__ticks-check--second {
        transition: none;
    }

    .conversation-item__ticks-check--second,
    .conversation-item__ticks.is-flipping .conversation-item__ticks-check--second {
        animation: none;
    }

    .dropdown-menu.show,
    .messenger-bubble-menu:not([hidden]) {
        animation: none;
    }

    .helheim-modal.modal.fade .modal-dialog {
        transform: none;
        transition: none;
    }

    .hm-loading-dots span {
        animation: none;
        opacity: 0.6;
    }

    .messenger-skeleton__bubble {
        animation: none;
        background-image: none;
    }

    /* Reply-highlight: no animation — a single static ring + wash still
     * mark the quoted message (the class is removed by JS after the reveal
     * settles, so the highlight always clears). */
    .messenger-bubble.is-reply-target {
        animation: none;
        box-shadow: 0 0 0 3px color-mix(in srgb, var(--ms-accent) 45%, transparent);
    }
}

@keyframes finalLogoReveal {
    0% {
        opacity: 0;
        transform: translateY(14px) scale(.965);
        filter: blur(9px) brightness(.66);
    }

    58% {
        opacity: .90;
    }

    100% {
        opacity: 1;
        transform: translateY(0) scale(1);
        filter: blur(0) brightness(1);
    }
}

@keyframes finalFloat {

    0%,
    100% {
        transform: translateY(0);
    }

    50% {
        transform: translateY(-4px);
    }
}

@keyframes finalLetterReveal {
    0% {
        opacity: 0;
        transform: translateY(7px);
        filter: blur(5px);
    }

    100% {
        opacity: 1;
        transform: translateY(0);
        filter: blur(0);
    }
}

/* 15 · iOS input-zoom guard
 * iOS Safari auto-zooms the whole page whenever an input/textarea
 * whose font-size is below 16px gains focus — the classic iPhone
 * "zoom on type". With the app's fixed-height shell that zoom reads
 * as the layout jumping out of the screen (the sidebar search and
 * composer were 0.9rem ≈ 14.4px, right under the threshold). Forcing
 * a 16px font on touch (coarse-pointer) devices keeps every field —
 * sidebar search, composer, modal forms, auth inputs — at the
 * no-zoom size. Desktop (fine pointer) keeps the designed sizes.
 * !important is deliberate: it must beat the component font sizes. */
@media (pointer: coarse) {

    input[type="text"],
    input[type="search"],
    input[type="email"],
    input[type="password"],
    input[type="tel"],
    input[type="url"],
    input[type="number"],
    input:not([type]),
    textarea,
    select {
        font-size: 16px !important;
    }

    /* On touch devices the scrollbar is a fat, hard-to-miss visual line that
     * eats into the already narrow screen. Keep scrolling fully functional
     * but hide the bar itself — the scroll position stays obvious from the
     * content. */
    * {
        scrollbar-width: none;
        -ms-overflow-style: none;
    }

    *::-webkit-scrollbar {
        display: none;
        width: 0;
        height: 0;
    }
}