/* ============================================================
   Responses — picker modal (messenger compose)
   ============================================================ */
.response-picker-list {
    display: flex;
    flex-direction: column;
    gap: 6px;
    max-height: 60vh;
    overflow-y: auto;
}
.response-picker-item {
    display: flex;
    flex-direction: column;
    gap: 4px;
    text-align: left;
    background: var(--color-bg-subtle, rgba(0,0,0,0.03));
    border: 1px solid var(--color-border);
    border-radius: 10px;
    padding: 10px 12px;
    cursor: pointer;
    transition: background 0.12s var(--ease-out), border-color 0.12s var(--ease-out), transform 100ms var(--ease-out);
    width: 100%;
}
.response-picker-item:hover  { background: var(--color-bg); border-color: var(--color-primary); }
.response-picker-item:active { transform: scale(0.99); }
.response-picker-title {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    font-weight: 600;
    font-size: var(--font-size-body);
    color: var(--color-text);
}
.response-picker-preview {
    font-size: var(--font-size-small);
    color: var(--color-text-light);
    line-height: 1.4;
    overflow: hidden;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}
.response-att-badge {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: 11px;
    font-weight: 500;
    color: var(--color-text-light);
    background: var(--color-bg);
    border-radius: 999px;
    padding: 2px 8px;
}
.response-att-badge .fi { font-size: 11px; }

/* ============================================================
   Responses — admin list (Messenger Management section)
   ============================================================ */
.response-admin-row {
    display: flex;
    align-items: flex-start;
    gap: 14px;
    padding: 12px 0;
    border-bottom: 1px solid var(--color-border-subtle, rgba(0,0,0,0.06));
}
.response-admin-row:last-child { border-bottom: none; }
.response-admin-row-main { flex: 1; min-width: 0; }
.response-admin-row-reorder {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    flex-shrink: 0;
    padding-top: 2px;
}
.response-reorder-btn {
    background: none;
    border: none;
    cursor: pointer;
    color: var(--color-text-light);
    width: 22px;
    height: 18px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    font-size: 12px;
    line-height: 1;
    transition: color 0.12s var(--ease-out);
}
.response-reorder-btn:hover:not(:disabled) { color: var(--color-primary); }
.response-reorder-btn:active:not(:disabled) {
    transform: scale(0.78);
    transition: transform 80ms var(--ease-out);
}
.response-reorder-btn:disabled { opacity: 0.25; cursor: default; }

/* Brief highlight pulse on the row the user just moved — paired with the
   FLIP slide animation in moveResponse(). 700ms total. */
@keyframes responseRowMovedPulse {
    0%   { background: rgba(76, 175, 145, 0); }
    25%  { background: rgba(76, 175, 145, 0.18); }
    100% { background: rgba(76, 175, 145, 0); }
}
.response-row-moved {
    animation: responseRowMovedPulse 700ms ease-out;
    border-radius: 8px;
}
.response-admin-row-title {
    font-weight: 600;
    font-size: var(--font-size-body);
    color: var(--color-text);
    margin-bottom: 2px;
}
.response-admin-row-preview {
    font-size: var(--font-size-small);
    color: var(--color-text-light);
    line-height: 1.4;
    margin-bottom: 4px;
    overflow: hidden;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}
.response-admin-row-actions {
    display: flex;
    gap: 6px;
    flex-shrink: 0;
}

/* ============================================================
   Responses — auto-response cards (sub-sub-tab "Auto Response")
   ============================================================ */
.response-auto-card {
    background: var(--color-bg-subtle, rgba(0,0,0,0.03));
    border: 1px solid var(--color-border);
    border-radius: 10px;
    padding: 14px 16px;
    margin-bottom: 12px;
}
.response-auto-card:last-child { margin-bottom: 0; }
.response-auto-card-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 10px;
    margin-bottom: 8px;
}
.response-auto-card-title {
    font-weight: 600;
    font-size: var(--font-size-body);
    color: var(--color-text);
    margin-bottom: 4px;
    display: flex;
    align-items: center;
    gap: 8px;
}
.response-auto-card-rule {
    font-size: var(--font-size-small);
    color: var(--color-text-light);
    line-height: 1.45;
    display: block;
}
.response-auto-card-rule .fi {
    color: var(--color-primary);
    margin-right: 6px;
    vertical-align: -1px;
}
.response-auto-card-rule strong {
    color: var(--color-text);
}
.response-auto-card-body {
    font-size: var(--font-size-small);
    color: var(--color-text-light);
    line-height: 1.45;
    padding-top: 8px;
    border-top: 1px dashed var(--color-border-subtle, rgba(0,0,0,0.06));
    overflow: hidden;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}
.response-auto-badge {
    display: inline-flex;
    align-items: center;
    font-size: var(--font-size-small);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    padding: 2px 8px;
    border-radius: 999px;
}
.response-auto-badge--on  { background: var(--color-success-bg); color: var(--color-success); }
.response-auto-badge--off { background: var(--color-bg-subtle); color: var(--color-text-light); }

/* Firing-rule banner inside the editor modal — shown only when editing an auto row. */
.response-editor-rule-banner {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: 10px 14px;
    margin-bottom: 14px;
    background: var(--color-success-bg);
    border: 1px solid var(--color-success);
    border-radius: 8px;
    font-size: var(--font-size-small);
    color: var(--color-text);
    line-height: 1.45;
}
.response-editor-rule-banner .fi {
    flex-shrink: 0;
    margin-top: 3px;
    color: var(--color-primary);
}

/* ============================================================
   Responses — editor modal
   ============================================================ */
.response-chip-row {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-bottom: 6px;
}
.response-placeholder-chip {
    background: var(--color-bg-subtle, rgba(0,0,0,0.04));
    border: 1px solid var(--color-border);
    border-radius: 999px;
    padding: 3px 10px;
    font-size: var(--font-size-small);
    font-family: var(--font-mono, monospace);
    color: var(--color-text);
    cursor: pointer;
    transition: background 0.12s var(--ease-out), border-color 0.12s var(--ease-out);
}
.response-placeholder-chip:hover {
    background: var(--color-bg);
    border-color: var(--color-primary);
    color: var(--color-primary);
}
.response-editor-preview {
    background: var(--color-bg-subtle, rgba(0,0,0,0.03));
    border: 1px dashed var(--color-border);
    border-radius: 8px;
    padding: 10px 12px;
    min-height: 56px;
    font-size: var(--font-size-body);
    color: var(--color-text);
    white-space: pre-wrap;
    line-height: 1.5;
}
#responseEditorBody {
    font-family: inherit;
    font-size: var(--font-size-body);
    line-height: 1.5;
}
.response-att-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px;
    border: 1px solid var(--color-border);
    border-radius: 8px;
    background: var(--color-bg-subtle, rgba(0,0,0,0.02));
}
.response-att-meta { flex: 1; min-width: 0; }
.response-att-name {
    font-size: var(--font-size-body);
    color: var(--color-text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Reply preview */
.msgr-reply-preview {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 10px;
    margin-bottom: 6px;
    background: rgba(0,0,0,0.04);
    border-left: 3px solid var(--color-primary);
    border-radius: 4px;
    font-size: var(--font-size-label);
}
.msgr-reply-text { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--color-text-light); }
.msgr-reply-close { background: none; border: none; cursor: pointer; font-size: var(--font-size-body); color: var(--color-text-light); padding: 0 4px; }

/* Media preview */
.msgr-media-preview {
    display: flex;
    gap: 8px;
    padding: 6px 0;
    flex-wrap: wrap;
    /* Containing block for the FLIP lift in _msgrAnimateThumbRemoval — the
       exiting thumb goes position:absolute and is offset within this row. */
    position: relative;
}
.msgr-media-thumb {
    position: relative;
    width: 64px;
    height: 64px;
    border-radius: 8px;
    overflow: hidden;
    border: 1px solid var(--color-border);
}
/* Thumbnail grid inside the /report modal (reuses .msgr-media-thumb tiles). */
.msgr-report-images {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 10px;
}
/* On mobile the /report modal goes full-screen (modal-fullscreen-mobile); make the
   modal a flex column so the description grows to fill all the spare height, giving
   the reporter maximum room to type. Desktop keeps the natural-height centred modal. */
@media (max-width: 767px) {
    #msgrReportModal .modal {
        display: flex;
        flex-direction: column;
        position: relative; /* anchor for the absolutely-positioned close button */
    }
    /* .modal-close uses float:right, which is IGNORED on a flex item, so on the
       full-screen mobile report modal the x landed inline at the top (full-width,
       centred glyph) instead of the top-right. Pin it to the top-right corner,
       clear of the status bar / notch. (bug: report-modal-keyboard-ios) */
    #msgrReportModal .modal-close {
        position: absolute;
        top: calc(env(safe-area-inset-top, 0px) + 20px);
        right: 20px;
        float: none;
        margin: 0;
        z-index: 2;
    }
    html.cap-inset-always #msgrReportModal .modal-close {
        top: 20px; /* native shell already insets the webview; env()=0 there */
    }
    /* Keep the title clear of the absolute close button so it never runs under it. */
    #msgrReportModal .modal-title {
        padding-right: 44px;
    }
    #msgrReportModal .msgr-report-desc-group {
        flex: 1 1 auto;
        display: flex;
        flex-direction: column;
        /* No min-height:0 here. With the keyboard up the modal shrinks to
           100svh - kb-inset, and min-height:0 let this group collapse below its
           content so the 120px textarea + guidance spilled over the Screenshots
           section. min-height:auto floors it at content height, so the modal
           (overflow-y:auto, inherited from .modal) scrolls cleanly instead of
           overlapping. (bug: report-modal-keyboard-ios) */
    }
    #msgrReportModal .msgr-report-desc-group textarea {
        flex: 1 1 auto;
        min-height: 120px;
        resize: none;
    }
}
.msgr-media-thumb img,
.msgr-media-thumb video { width: 100%; height: 100%; object-fit: cover; }
.msgr-media-thumb.msgr-media-file {
    width: auto;
    height: auto;
    padding: 6px 8px;
    font-size: var(--font-size-small);
    display: flex;
    align-items: center;
    gap: 4px;
}
.msgr-media-hint {
    font-size: var(--font-size-small);
    color: var(--color-text-light);
    align-self: center;
    margin-left: auto;
    white-space: nowrap;
    flex-shrink: 0;
}
.msgr-media-remove {
    position: absolute;
    top: 4px;
    right: 4px;
    background: rgba(0,0,0,0.55);
    color: #fff;
    border: 1.5px solid rgba(255,255,255,0.9);
    border-radius: 50%;
    width: 22px;
    height: 22px;
    font-size: 13px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    line-height: 1;
    box-shadow: 0 1px 4px rgba(0,0,0,0.35);
    transition: background 0.15s ease, transform 0.15s ease;
}
.msgr-media-remove:hover { background: rgba(0,0,0,0.82); transform: scale(1.08); }
.msgr-media-remove:active { transform: scale(0.94); }

/* Document/video chip remove button - flows inline at right, not a corner badge */
.msgr-media-remove-inline {
    position: static;
    top: auto;
    right: auto;
    width: 20px;
    height: 20px;
    font-size: 11px;
    background: rgba(0,0,0,0.15);
    border: none;
    box-shadow: none;
    flex-shrink: 0;
}
.dark .msgr-media-remove-inline,
[data-theme="dark"] .msgr-media-remove-inline {
    background: rgba(255,255,255,0.15);
}

/* Edit inline */
.msgr-msg-editing {
    max-width: 100%;
    width: 100%;
}
.msgr-msg-editing .msgr-msg-bubble {
    background: var(--color-bg) !important;
    color: var(--color-text) !important;
    border: 1px solid var(--color-border);
}
.msgr-edit-input {
    width: 100%;
    padding: 6px 8px;
    border: 1px solid var(--color-primary);
    border-radius: 6px;
    font-size: var(--font-size-body);
    line-height: 1.5;
    color: var(--color-text);
    background: var(--color-bg);
    resize: none;
    height: auto;
    max-height: calc(10 * 1.5em + 12px);
    overflow-y: auto;
    font-family: inherit;
    box-sizing: border-box;
}
.msgr-edit-input:focus { outline: none; }
.msgr-edit-actions {
    display: flex;
    gap: 6px;
    margin-top: 3px;
    justify-content: flex-end;
}

/* Tab dot */
.msgr-tab-dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--color-pulse-dot);
    animation: livePulse 1.5s ease-in-out infinite;
    margin-left: 4px;
    vertical-align: middle;
}

/* ── Search Results ── */

.msgr-search-results { overflow-y: auto; flex: 1; }
.msgr-search-result {
    padding: 10px 14px;
    cursor: pointer;
    border-bottom: 1px solid var(--color-border);
    transition: background 0.15s;
}
.msgr-search-result:hover { background: var(--color-surface-hover, rgba(0,0,0,0.03)); }
.msgr-search-result-header { display: flex; justify-content: space-between; gap: 6px; }
.msgr-search-result-sender { font-weight: 600; font-size: var(--font-size-label); }
.msgr-search-result-time { font-size: var(--font-size-small); color: var(--color-text-light); }
.msgr-search-result-text { font-size: var(--font-size-label); color: var(--color-text); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.msgr-search-result-conv { font-size: var(--font-size-small); color: var(--color-text-light); margin-top: 2px; }

/* ── Modals: member picker, DM picker ── */

.msgr-member-picker, .msgr-dm-picker {
    max-height: 200px;
    overflow-y: auto;
    margin-top: 8px;
}
.msgr-member-pick-item {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 10px;
    cursor: pointer;
    font-size: var(--font-size-body);
    border-bottom: 1px solid var(--color-border);
}
.msgr-member-pick-item:last-child { border-bottom: none; }
.msgr-member-pick-item:hover { background: var(--color-surface-hover, rgba(0,0,0,0.03)); }
.msgr-member-pick-item input[type="checkbox"] { flex-shrink: 0; }
.msgr-member-role { font-size: var(--font-size-small); color: var(--color-text-light); margin-left: auto; }
.msgr-member-role--admin { color: var(--color-primary); }

.msgr-dm-pick-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 10px 12px;
    cursor: pointer;
    font-size: var(--font-size-body);
    border-bottom: 1px solid var(--color-border);
}
.msgr-dm-pick-item:last-child { border-bottom: none; }
.msgr-dm-pick-item:hover { background: var(--color-surface-hover, rgba(0,0,0,0.03)); }

.msgr-selected-members {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    min-height: 28px;
}
.msgr-chip {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    background: var(--color-primary);
    color: #fff;
    padding: 4px 10px;
    border-radius: 12px;
    font-size: var(--font-size-label);
    font-weight: 500;
    cursor: pointer;
    user-select: none;
}
.msgr-chip-x {
    color: rgba(255,255,255,0.65);
    font-size: 14px;
    line-height: 1;
}

/* ── New Message modal animations ── */

/* FB1 - Choice button press feel */
.msgr-new-choice-btn {
    transition: background 0.15s var(--ease-out), border-color 0.15s var(--ease-out), transform 0.1s var(--ease-out);
}
.msgr-new-choice-btn:active { transform: scale(0.97); }

/* FB2 - DM / member list item press feel */
.msgr-dm-pick-item,
.msgr-member-pick-item {
    transition: background 0.1s var(--ease-out), transform 0.1s var(--ease-out);
}
.msgr-dm-pick-item:active,
.msgr-member-pick-item:active { transform: scale(0.99); }

/* FB3 - Row flash on checkbox check */
@keyframes msgrRowCheck {
    0%   { background: color-mix(in srgb, var(--color-primary) 14%, transparent); }
    100% { background: transparent; }
}
.msgr-row-checked { animation: msgrRowCheck 350ms var(--ease-out) forwards; }

/* SC1 - Chip enters with spring */
@keyframes msgrChipEnter {
    0%   { transform: scale(0); opacity: 0; }
    60%  { transform: scale(1.1); opacity: 1; }
    100% { transform: scale(1); opacity: 1; }
}
.msgr-chip-enter { animation: msgrChipEnter 220ms var(--ease-spring) forwards; }

/* SC2 - Chip exits */
@keyframes msgrChipExit {
    to { transform: scale(0); opacity: 0; }
}
.msgr-chip-exit {
    animation: msgrChipExit 150ms var(--ease-out) forwards;
    pointer-events: none;
}

/* SC3 - Selected members section rises in */
#msgrSelectedSection {
    opacity: 0;
    transform: translateY(-6px);
    transition: opacity 200ms var(--ease-out), transform 200ms var(--ease-out);
    pointer-events: none;
    margin-bottom: 12px;
}
#msgrSelectedSection.msgr-chips-visible {
    opacity: 1;
    transform: translateY(0);
    pointer-events: auto;
}

/* ── Messenger micro-animations (Disney pass) ── */

/* MA1 - Message bubble entrance - direction-aware spring pop */
@keyframes msgrMsgIn {
    from { opacity: 0; transform: translateY(18px); }
    to   { opacity: 1; transform: translateY(0); }
}
/* MA4 - Image materialises inside bubble 60ms after the shell arrives */
@keyframes msgrImgReveal {
    from { opacity: 0; }
    to   { opacity: 1; }
}
/* Outgoing: bubble rises from bottom-right (where you typed) */
.msgr-msg-out.msgr-msg-enter {
    animation: msgrMsgIn 300ms var(--ease-spring) both;
    transform-origin: bottom right;
}
/* Incoming: bubble rises from bottom-left (the other person's side) */
.msgr-msg-in.msgr-msg-enter {
    animation: msgrMsgIn 300ms var(--ease-spring) both;
    transform-origin: bottom left;
}
/* System messages: simple centre fade */
.msgr-msg-system.msgr-msg-enter {
    animation: msgrMsgIn 180ms var(--ease-out) both;
    transform-origin: center bottom;
}
/* Image inside a new bubble materialises 60ms after the shell.
   Skip pending placeholders (they own their shimmer) AND images mid reveal
   (.msgr-img-reveal) - in both cases the `animation` shorthand here would
   otherwise clobber the placeholder's / reveal's own choreography. */
.msgr-msg-enter .msgr-msg-img:not(.msgr-img-pending):not(.msgr-img-reveal) {
    animation: msgrImgReveal 200ms var(--ease-out) 60ms both;
}

/* Optimistic / sending state - dimmed until confirmed by DB */
.msgr-msg-sending { opacity: 0.55; }

/* MA-Upload - real-progress overlay (dark scrim + circular ring + live %) painted
   on the sending image. Driven by actual XHR upload-progress bytes (see
   _msgrUploadWithProgress), so the number is definitive — the sender watches the
   full file climb to 100% and can long-press → Delete a send that's crawling. */
.msgr-msg-sending .msgr-msg-media {
    border-radius: 10px; /* clip overlay corners to match image */
}
.msgr-upload-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 6px;
    background: rgba(0, 0, 0, 0.34);
    z-index: 3;
    pointer-events: none;
    transition: opacity 240ms ease-out;
}
.msgr-upload-overlay-done { opacity: 0; }
.msgr-upload-ring {
    width: 46px;
    height: 46px;
    transform: rotate(-90deg); /* start the arc at 12 o'clock */
}
.msgr-upload-ring-track { fill: none; stroke: rgba(255, 255, 255, 0.32); stroke-width: 4; }
.msgr-upload-ring-fill {
    fill: none;
    stroke: #fff;
    stroke-width: 4;
    stroke-linecap: round;
    transition: stroke-dashoffset 200ms ease-out;
}
.msgr-upload-pct {
    color: #fff;
    font-size: var(--font-size-small);
    font-weight: 700;
    letter-spacing: 0.2px;
    font-variant-numeric: tabular-nums;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
}

/* MA-Sent - spring pop on media wrapper when upload confirms delivery */
@keyframes msgrMediaSentPop {
    0%   { transform: scale(1); }
    40%  { transform: scale(1.025); }
    100% { transform: scale(1); }
}
.msgr-media-sent {
    animation: msgrMediaSentPop 380ms var(--ease-spring) forwards;
}

/* MA2 - Message delete: anticipation "gather" then dissolve into dust.
   GPU-only (transform + opacity) - the old filter:blur implosion was the jank
   source (filter repaints every frame, fights the 150 animating motes). */
@keyframes msgrMsgDisintegrate {
    0%   { opacity: 1; transform: scale(1)    translateY(0);    }
    22%  { opacity: 1; transform: scale(1.05) translateY(0);    }  /* gather / inhale */
    100% { opacity: 0; transform: scale(0.94) translateY(-8px); }  /* lift + fade with the dust */
}
.msgr-msg-exit { animation: msgrMsgDisintegrate 380ms var(--ease-out) forwards; pointer-events: none; }

/* MA2-collapse - once the bubble is dust, glide the now-empty row closed so the
   messages below slide up to fill the gap instead of snapping. A single-row
   layout transition is acceptable for a one-shot delete and far less jarring. */
.msgr-msg-collapse {
    transition: height 260ms var(--ease-out),
                margin 260ms var(--ease-out),
                padding 260ms var(--ease-out);
}

/* MA2-particle - motes lift off the bubble, rise + drift outward on a faint
   wind, then thin out and fade (Thanos ash). Per-mote delay + duration are set
   inline so the cloud disperses unevenly instead of all snapping at once. */
@keyframes msgrParticleFly {
    0%   { opacity: 1; transform: translate(0, 0) scale(1) rotate(0deg); }
    30%  { opacity: 1; }
    50%  { transform: translate(var(--pmx), var(--pmy)) scale(0.7) rotate(calc(var(--pr) * 0.4)); }
    100% { opacity: 0; transform: translate(var(--px), var(--py)) scale(0.12) rotate(var(--pr)); }
}
.msgr-particle {
    position: fixed;
    pointer-events: none;
    z-index: 9999;
    border-radius: 1px;
    will-change: transform, opacity;
    animation: msgrParticleFly 460ms var(--ease-out) both;
}

/* MA3 - Conversation / search item stagger entrance */
@keyframes msgrConvIn {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: translateY(0); }
}
.msgr-conv-enter, .motion-list-enter { animation: msgrConvIn 200ms var(--ease-out) both; } /* .motion-list-enter = orion_motion 'list-stagger' */
.msgr-search-enter { animation: msgrConvIn 200ms var(--ease-out) both; }

/* MA4 - Reply preview bar slide in / out */
@keyframes msgrReplyIn {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: translateY(0); }
}
@keyframes msgrReplyOut {
    from { opacity: 1; transform: translateY(0); }
    to   { opacity: 0; transform: translateY(6px); }
}
.msgr-reply-enter { animation: msgrReplyIn 180ms var(--ease-out) both; }
.msgr-reply-exit  { animation: msgrReplyOut 150ms var(--ease-out) forwards; pointer-events: none; }

/* MA5 - Typing indicator fade */
@keyframes msgrTypingIn {
    from { opacity: 0; transform: translateY(4px); }
    to   { opacity: 1; transform: translateY(0); }
}
.msgr-typing-enter { animation: msgrTypingIn 200ms var(--ease-out) both; }

/* MA7 - Read receipt tick pulse on change */
@keyframes msgrTickPulse {
    0%   { transform: scale(1); }
    50%  { transform: scale(1.3); }
    100% { transform: scale(1); }
}
.msgr-tick-pulse, .motion-tick-pulse { display: inline-block; animation: msgrTickPulse 300ms var(--ease-spring); } /* .motion-tick-pulse = orion_motion 'tick-pulse' */

/* MA8 - Unread badge spring pop-in */
@keyframes msgrBadgeIn {
    0%   { transform: scale(0); opacity: 0; }
    60%  { transform: scale(1.15); opacity: 1; }
    100% { transform: scale(1); opacity: 1; }
}
.msgr-badge-enter { animation: msgrBadgeIn 250ms var(--ease-spring) both; }

/* MA10 - Send button pop when content entered */
@keyframes msgrSendPop {
    0%   { transform: scale(0.85); }
    60%  { transform: scale(1.08); }
    100% { transform: scale(1); }
}
.msgr-send-pop { animation: msgrSendPop 200ms var(--ease-spring); }

/* MA11 - Media preview thumbnail entrance */
@keyframes msgrThumbIn {
    from { opacity: 0; transform: scale(0.9); }
    to   { opacity: 1; transform: scale(1); }
}
.msgr-thumb-enter { animation: msgrThumbIn 180ms var(--ease-out) both; }

/* MA11 - thumbnail exit (removal): the removed thumb shrinks + fades while its
   surviving siblings glide over to close the gap (FLIP in
   _msgrAnimateThumbRemoval). */
@keyframes msgrThumbOut {
    from { opacity: 1; transform: scale(1); }
    to   { opacity: 0; transform: scale(0.8); }
}
.msgr-thumb-exit { animation: msgrThumbOut 160ms var(--ease-out) both; }

/* MA9 — Mobile thread slide-in / slide-out
   Restored in v2.96 after L163 identified the real cause of the "two taps after
   swipe-back" bug (iOS WebKit click suppression, fixed via touchend delegation
   in admin-messenger.js — nothing animation-related). The animations were
   guilty by association during the long debug.

   Slide-out deliberately has NO `forwards` fill — at animation end, transform
   reverts to translateX(0) naturally, which lands in the same paint frame as
   onDone's conv-open removal (→ display:none). No flicker, no compositor layer
   cached at translateX(100%). */
@keyframes msgrThreadSlideIn {
    from { transform: translateX(100%); }
    to   { transform: translateX(0); }
}
@keyframes msgrThreadSlideOut {
    from { transform: translateX(0); }
    to   { transform: translateX(100%); }
}
/* Timing matches Telegram iOS exactly: its navigation push (open a chat) and pop
   (swipe/back) both run 0.5s with kCAMediaTimingFunctionSpring: a genuine spring,
   not a cubic ease. We reproduce that spring with a linear() easing sampled from a
   damped-spring simulation (~1.5% overshoot, the gently-damped iOS settle), and fall
   back to an iOS-style cubic-bezier on engines without linear() support. Both
   directions share one duration and one curve, exactly as Telegram does. */
/* The generic motion-swipe-* aliases (orion_motion 'swipe-spring' primitive)
   share this one declaration + linear() curve, so the spring is written once.
   Existing messenger JS keeps adding .msgr-thread-*; new uses add .motion-swipe-*. */
.msgr-thread-enter, .motion-swipe-enter { animation: msgrThreadSlideIn 500ms cubic-bezier(0.32, 0.72, 0, 1) both; }
.msgr-thread-exit,  .motion-swipe-exit  { animation: msgrThreadSlideOut 500ms cubic-bezier(0.32, 0.72, 0, 1); }
/* Messenger ↔ dashboard transition — a SAME-DOCUMENT fade + splash bridge that
   replaces the old cross-document ::view-transition (which did not animate
   reliably in the iOS webview). It matches the in-dashboard tab switch because it
   uses the SAME motion family: the panel-fade primitive's motionPanelFadeOut
   (300ms, var(--ease-out)), so leaving the messenger feels like changing tabs.
   OUT (messages.html): html.msgr-leaving fades the .app-container out over the
   #msgrReturnSplash painted behind it, then admin-messenger.js navigates on
   animationend. ARRIVE (dashboard.html): html.msgr-incoming neutralises the
   browser's default cross-document page-nav view-transition so it can't fade or
   nudge the splash on top of the fade that just played; the splash + the JS fade
   own the arrival. Mobile only — desktop is a plain navigation. */
@media (max-width: 600px) {
    /* Leave: messenger fades out to reveal the splash behind it. */
    html.msgr-leaving #msgrReturnSplash { z-index: 1; }   /* behind the fading messenger (overrides the 100000 arrival z-index) */
    html.msgr-leaving .app-container {
        position: relative;
        z-index: 2;
        animation: motionPanelFadeOut var(--dur-transition) var(--ease-out) forwards;
    }
    /* Arrive: suppress the default cross-doc page-nav VT (it would fade + nudge
       the splash on top of the fade). The splash + JS fade own the arrival. */
    html.msgr-incoming::view-transition-old(root),
    html.msgr-incoming::view-transition-new(root) { animation: none !important; }
}
@supports (animation-timing-function: linear(0, 1)) {
    .msgr-thread-enter, .motion-swipe-enter,
    .msgr-thread-exit,  .motion-swipe-exit { animation-timing-function: linear(0, 0.072, 0.228, 0.407, 0.574, 0.714, 0.822, 0.899, 0.951, 0.984, 1.003, 1.012, 1.015, 1.015, 1.013, 1.010, 1.007, 1.005, 1.003, 1.002, 1); }
}

/* ── End Messenger micro-animations ── */

/* ============================================================
   MOTION LIBRARY  (orion_motion canonical primitives)
   SoT registry: js/animations.js (ANIMATION_REGISTRY).
   Human index:   docs/animation-library.md.
   Parity guard:  /orion_audit_visual Cat 9g (keyframes <-> registry).
   Reduced motion is handled by the global @media block (search
   "prefers-reduced-motion") + the JS helper early-return; do NOT add a
   redundant block here.
   Wire these via playAnimation() / staggerAnimation() in js/animations.js;
   never set the inline `animation` shorthand (it would beat the @supports
   linear() upgrade on .motion-swipe-*).
   Reused primitives (swipe-spring, list-stagger, tick-pulse) live with their
   messenger declarations above via shared selector groups, NOT duplicated here.
   Add a primitive: new @keyframes + .motion-* class below (animate only
   transform/opacity/filter), a --dur-* token in :root if a new tier is needed,
   an ANIMATION_REGISTRY entry, one docs line, then run the Cat 9g parity check.
   ============================================================ */

/* panel-fade - view / tab swap. Add the class synchronously after inserting
   content; both-fill hides it from frame 0 so there is no flash-then-fade.
   The exit pair fades a leaving panel out; html.msgr-leaving .app-container
   (the messenger-to-dashboard exit) shares the motionPanelFadeOut keyframes. */
@keyframes motionPanelFade {
    from { opacity: 0; }
    to   { opacity: 1; }
}
@keyframes motionPanelFadeOut {
    from { opacity: 1; }
    to   { opacity: 0; }
}
.motion-panel-enter { animation: motionPanelFade var(--motion-dur, var(--dur-transition)) var(--ease-out) both; }
.motion-panel-exit  { animation: motionPanelFadeOut var(--motion-dur, var(--dur-transition)) var(--ease-out) both; }

/* card-enter - staggered card lift-in (via staggerAnimation) */
@keyframes motionCardEnter {
    from { opacity: 0; transform: translateY(12px) scale(0.99); }
    to   { opacity: 1; transform: translateY(0) scale(1); }
}
.motion-card-enter { animation: motionCardEnter var(--motion-dur, var(--dur-enter)) var(--ease-out) both; }

/* react-pop - emoji reaction / badge pop: up then settle back to rest */
@keyframes motionReactPop {
    0%   { opacity: 0; transform: scale(0); }
    60%  { opacity: 1; transform: scale(1.25); }
    100% { opacity: 1; transform: scale(1); }
}
.motion-react-pop { animation: motionReactPop var(--motion-dur, var(--dur-feedback)) var(--ease-spring); }

/* value-bump - a displayed number / total just changed */
@keyframes motionValueBump {
    0%   { transform: scale(1); }
    50%  { transform: scale(1.2); }
    100% { transform: scale(1); }
}
.motion-value-bump { display: inline-block; animation: motionValueBump var(--motion-dur, var(--dur-feedback)) var(--ease-spring); }

/* success-pop - rare celebratory confirm (use sparingly, never loop) */
@keyframes motionSuccessPop {
    0%   { opacity: 0; transform: scale(0.6); }
    60%  { opacity: 1; transform: scale(1.12); }
    100% { opacity: 1; transform: scale(1); }
}
.motion-success-pop { animation: motionSuccessPop var(--motion-dur, var(--dur-celebrate)) var(--ease-spring); }

/* press-give - CSS-only tactile press (no JS); add .motion-press to a control */
.motion-press { transition: transform var(--dur-micro) var(--ease-out); }
.motion-press:active { transform: scale(0.97); }

/* fly-to - parameterized launch -> land flight. The consumer positions the
   element at its DESTINATION (left/top, no own transform) and sets --fly-dx /
   --fly-dy (origin minus destination, px) + optional --fly-scale (launch
   scale). Generic: add-to-cart fly, reaction emoji -> pill, etc. */
@keyframes motionFlyTo {
    0%   { opacity: 1; transform: translate(var(--fly-dx, 0px), var(--fly-dy, 0px)) scale(var(--fly-scale, 1.5)); }
    100% { opacity: 1; transform: translate(0, 0) scale(1); }
}
.motion-fly-to { animation: motionFlyTo var(--motion-dur, var(--dur-transition)) var(--ease-out) both; }

/* expand-origin - shared-element FLIP: an element grows out of a source rect to
   fill the screen (lightbox open) and shrinks back into it on close, the way iOS
   expands a photo / video to fullscreen. Parameterized: the consumer measures the
   origin + final rects at runtime and sets --flip-dx / --flip-dy (origin centre
   minus final centre, px) + --flip-scale (cover-scale = max(originW/finalW,
   originH/finalH)). Reference driver: image-crop.js openLightbox/closeLightbox,
   which drives the SAME motion with an inline transition (it must read two live
   rects per direction + hand back to zoom/pan) rather than this class. */
@keyframes motionExpandOrigin {
    from { transform: translate(var(--flip-dx, 0px), var(--flip-dy, 0px)) scale(var(--flip-scale, 0.4)); }
    to   { transform: translate(0, 0) scale(1); }
}
.motion-expand-origin { animation: motionExpandOrigin var(--motion-dur, var(--dur-transition)) var(--ease-out) both; }

/* burst - celebratory particle. The consumer spawns N short-lived fixed nodes
   at a centre point and sets per-node --burst-dx / --burst-dy / --burst-rot.
   Like .msgr-particle (delete crumble) but radiating + celebratory. Restraint:
   gate at high frequency, keep subtle, never loop. */
@keyframes motionBurst {
    0%   { opacity: 1; transform: translate(-50%, -50%) scale(0.3); }
    100% { opacity: 0; transform: translate(calc(-50% + var(--burst-dx, 0px)), calc(-50% + var(--burst-dy, 0px))) scale(1) rotate(var(--burst-rot, 0deg)); }
}
.motion-burst {
    position: fixed;
    z-index: 9998;
    width: 7px;
    height: 7px;
    border-radius: 2px;
    pointer-events: none;
    will-change: transform, opacity;
    animation: motionBurst var(--motion-dur, var(--dur-celebrate)) var(--ease-out) both;
}
/* ── End Motion Library ── */

.msgr-section-heading {
    font-size: var(--font-size-body);
    font-weight: 600;
    color: var(--color-text);
    margin: 0 0 8px;
}
.msgr-vis-hint {
    font-size: var(--font-size-label);
    color: var(--color-text-light);
    margin-top: 6px;
    margin-bottom: 0;
}
.msgr-my-note-wrapper {
    margin-top: 12px;
}
.msgr-member-list { }
.msgr-member-item {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 8px 0;
    min-height: 52px;
    font-size: var(--font-size-body);
}
.msgr-member-avatar {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 12px;
    font-weight: 700;
    color: #fff;
    flex-shrink: 0;
    letter-spacing: 0.02em;
}
.msgr-member-name {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.msgr-role-chip {
    font-size: 11px;
    font-weight: 600;
    padding: 3px 9px;
    border-radius: 20px;
    white-space: nowrap;
    flex-shrink: 0;
}
.msgr-role-chip--admin  { background: rgba(107,76,138,0.12); color: var(--color-primary); }
.msgr-role-chip--staff  { background: rgba(0,0,0,0.07); color: var(--color-text-light); }
.msgr-role-chip--vendor { background: rgba(180,100,20,0.1); color: #a05a10; }
[data-theme="dark"] .msgr-role-chip--staff  { background: rgba(255,255,255,0.1); }
[data-theme="dark"] .msgr-role-chip--vendor { color: #e0924a; background: rgba(180,100,20,0.2); }
.msgr-role-chip--bot { background: rgba(0,130,120,0.1); color: #0a8078; }
[data-theme="dark"] .msgr-role-chip--bot { background: rgba(0,180,160,0.15); color: #2ecfc3; }
.msgr-danger-zone {
    border-top: 1px solid var(--color-border);
    padding-top: 16px;
    margin-top: 24px;
}
.msgr-modal-section-label--danger { color: var(--color-danger) !important; }
.msgr-remove-btn {
    background: none;
    border: none;
    padding: 0;
    width: 44px;
    height: auto;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    color: var(--color-text-light);
    border-radius: 6px;
    transition: color 0.15s, background 0.15s;
    flex-shrink: 0;
    font-size: 16px;
    align-self: stretch;
}
.msgr-remove-btn:hover {
    color: var(--color-danger);
    background: rgba(var(--color-error-rgb, 180, 50, 50), 0.08);
}

/* ── Mobile Responsive ── */

@media (hover: none) {
    .msgr-group-photo-wrap .msgr-group-photo-overlay { opacity: 1; }
}

@media (max-width: 600px) {
    .msgr-root { flex-direction: column; min-height: 0; max-height: none; }
    /* Hide the desktop column on mobile; show the inline strip instead */
    #msgrSourceNav { display: none !important; }
    .msgr-source-nav-strip {
        display: flex;
        flex-direction: row;
        width: 100%;
        min-width: 0;
        height: 48px;
        min-height: 48px;
        border-right: none;
        border-bottom: 1px solid var(--color-border);
        padding: 6px 10px;
        gap: 6px;
        overflow-x: auto;
        overflow-y: hidden;
        scrollbar-width: none;
        -ms-overflow-style: none;
        -webkit-overflow-scrolling: touch;
        align-items: center;
    }
    .msgr-source-nav-strip::-webkit-scrollbar { display: none; }
    .msgr-source-nav-strip .msgr-source-nav-item {
        flex-direction: row;
        flex-shrink: 0;
        width: auto;
        padding: 5px 12px;
        border-radius: 20px;
        gap: 5px;
        border: 1px solid var(--color-border);
    }
    .msgr-source-nav-strip .msgr-source-nav-item::before { display: none; }
    .msgr-source-nav-strip .msgr-source-nav-item.is-active {
        background: var(--item-color, var(--color-primary));
        color: #fff;
        border-color: transparent;
    }
    .msgr-source-nav-strip .msgr-source-nav-icon { font-size: 14px; }
    .msgr-source-nav-strip .msgr-source-nav-label { font-size: 13px; }
    .msgr-sidebar { width: 100%; min-width: 0; border-right: none; border-bottom: 1px solid var(--color-border); max-height: none; min-height: 200px; }
    .msgr-thread { display: none; width: 100%; min-height: 0; }
    .msgr-root.conv-open .msgr-sidebar { display: none; }
    .msgr-root.conv-open .msgr-thread { display: flex; position: fixed; top: 0; left: 0; right: 0; height: 100%; max-height: none; z-index: 960; background: var(--color-surface); overflow-x: hidden; transition: height 0.25s var(--ease-keyboard), top 0.25s var(--ease-keyboard); }
    /* The fixed thread above positions against the viewport, escaping the
       body's env(safe-area-inset-top) padding — so the header would sit under
       the status bar / notch. Inset the header itself below it. env() is 0 on
       non-notched devices (desktop/web), so this is a no-op there. View As is
       left untouched (the thread is already offset 44px for its banner). */
    body:not(.view-as-active) .msgr-root.conv-open .msgr-thread-header {
        padding-top: calc(env(safe-area-inset-top) + 10px);
    }
    /* contentInset:"always" builds: the OS already insets the webview below the
       notch, so env() would double it — keep the plain 10px there. */
    html.cap-inset-always body:not(.view-as-active) .msgr-root.conv-open .msgr-thread-header {
        padding-top: 10px;
    }
    .msgr-back-btn { display: inline-flex; }
    .msgr-msg { max-width: 85%; }
    body.view-as-active .msgr-root.conv-open .msgr-thread { top: 44px; height: calc(100% - 44px); }
    /* Lift the compose row clear of the home indicator AND the phone's curved bottom
       corners (which were clipping the attach/mic icons). The fixed thread escapes the
       body's env(safe-area-inset-bottom) padding, so pad the compose itself. env()
       collapses to 0 while the keyboard is up, so this only pads at rest. */
    /* Decoupled from .conv-open: the compose's rest padding stays CONSTANT whether or not
       a conversation is open, so toggling .conv-open on reopen never relayouts the compose.
       (On mobile the compose only renders inside the fixed thread, which is display:none
       until .conv-open, so applying this unconditionally is invisible while no conv is open.)
       When this padding was conv-open-scoped, reopening a thread grew the compose ~38px AFTER
       the open scroll, shrinking the flex message list from the bottom and clipping the newest
       message. The keyboard still animates via the inline 8px override (beats this selector). */
    .msgr-compose {
        padding-bottom: calc(env(safe-area-inset-bottom) + 12.6px);
    }
    /* contentInset:"always" builds already inset the webview above the home indicator,
       so env() would double it, keep just the generous extra. */
    html.cap-inset-always .msgr-compose {
        padding-bottom: 12.6px;
    }
}

/* ── Embedded messenger tab (body.msgr-active) ──────────────────────────────
   The messenger lives as #tab-messenger inside dashboard.html (was the standalone
   messages.html). applyTabSwitch() toggles body.msgr-active while the messenger tab
   is the active surface. These rules reproduce what messages.html's inline <style>
   did (full-bleed card, chrome hidden, page-scoped scroll lock), rescoped from
   body.messenger-page to body.msgr-active so they ONLY apply while the messenger tab
   is showing and never leak onto the dashboard's other tabs. The bottom-nav clearance
   already lives on body.has-bottom-nav (it pads the BODY), so it is not repeated. */
body.msgr-active { overscroll-behavior: none; }
body.msgr-active .app-header,
body.msgr-active .nav-bar { display: none; }
/* Fill the container: zero padding (the body keeps its has-bottom-nav bottom
   clearance) and make the active messenger tab a growing flex column. */
body.msgr-active .app-container { padding: 0; }
body.msgr-active #tab-messenger.active { display: flex; flex-direction: column; flex: 1; min-height: 0; }
body.msgr-active .msgr-root { flex: 1; min-height: 0; max-height: none; border: none; border-radius: 0; }
@media (min-width: 641px) {
    /* Desktop keeps the rounded card + side padding, like messages.html did. */
    body.msgr-active .app-container { padding: 16px; }
    body.msgr-active .msgr-root { border: 1px solid var(--color-border); border-radius: 20px; }
    /* Lock the embedded messenger to the viewport so the PAGE never scrolls, only the
       inner panels do (left conv-list via .msgr-scrollable-area, right thread via
       .msgr-messages). messages.html did this inline (html,body{height:100%;overflow:hidden}
       + .app-container{min-height:0}); the embed dropped it, so the flex height chain never
       bottomed out and the whole page (left panel included) scrolled. Desktop-only: mobile
       uses a position:fixed full-screen thread and already behaves. */
    body.msgr-active { height: 100vh; height: 100dvh; overflow: hidden; }
    body.msgr-active .app-container { min-height: 0; }
}
@media (max-width: 640px) {
    /* Full-bleed app on mobile. The notch / status-bar clearance is ALREADY provided
       by whichever layer owns it: the body's env(safe-area-inset-top) padding on web +
       cap-edge-to-edge, or the OS contentInset on cap-inset-always (where the body env
       is suppressed). So the header must add ONLY breathing room (flat 8px) and must NOT
       re-apply env() — doing so double-counts the inset and leaves a tall empty band over
       the header on real iOS (env>0). This mirrors the proven-correct messages.html rule.
       (The fixed conv-open thread header is handled by its own rule above.) */
    body.msgr-active .msgr-sidebar-header { padding-top: 8px; }
    /* The conv-list column must GROW to fill the full-bleed card (messages.html did
       this inline). Without it a short list leaves a --color-bg gap above the bottom
       nav and the retention notice floats mid-screen instead of sitting at the base. */
    body.msgr-active .msgr-sidebar { flex: 1; min-height: 0; }
}

/* ── Messenger Dark Mode ── */
[data-theme="dark"] .msgr-conv-item.active {
    background: rgba(255,255,255,0.06);
}
[data-theme="dark"] .msgr-compose-input {
    background: #253030;
    color: var(--color-text);
}
.msgr-compose-input:focus {
    outline: none;
}
[data-theme="dark"] .msgr-compose-input:focus {
    outline: none;
}
[data-theme="dark"] .msgr-search-input {
    background: #1a1e1e;
    border-color: #4a5555;
    color: var(--color-text);
}
[data-theme="dark"] .msgr-reply-preview {
    background: rgba(141, 205, 196, 0.08);
}
[data-theme="dark"] .msgr-edit-input {
    background: #1a1e1e;
    border-color: #4a5555;
    color: var(--color-text);
}

