:root {
    --bg: #0a0e1f;
    --panel: #121730;
    --panel-alt: #1a2140;
    --border: #262c4a;
    --text: #e6e8f5;
    --muted: #8b93b8;
    --accent-from: #22d3ee;
    --accent-to: #a855f7;
    --accent: #8b5cf6;
    --accent-hover: #7c3aed;
    --accent-soft: rgba(139, 92, 246, 0.18);
    --gradient: linear-gradient(135deg, var(--accent-from), var(--accent-to));
    --bubble-me-text: #ffffff;
    --bubble-them: #1c2340;
    --danger: #f87171;
    --danger-soft: rgba(248, 113, 113, 0.14);
    --success: #4ade80;
    --success-soft: rgba(74, 222, 128, 0.14);
    --bg-glow-1: rgba(34, 211, 238, 0.13);
    --bg-glow-2: rgba(168, 85, 247, 0.16);
}

/* Switching theme (dark/light, or the accent color picker) in Settings
   used to just snap instantly - every element using these variables now
   eases into its new colors instead, so the whole app fades between
   themes rather than flashing. Scoped to color-ish properties only (never
   layout ones like width/padding) so this can't cause any visible jank
   elsewhere. Covered by the global prefers-reduced-motion rule further
   down like every other animation/transition in this file. */
* {
    transition: background-color .2s ease, border-color .2s ease, color .2s ease, fill .2s ease, stroke .2s ease;
}

/* A visible outline for keyboard navigation (Tab-ing through the app) -
   nothing here relied on the browser's own default outline (some of the
   custom buttons/inputs even suppress it), so keyboard users previously
   had no way to tell what was focused. Only shows for keyboard focus
   (:focus-visible), never on a plain mouse click/tap - matches how every
   modern OS/browser's own UI already behaves. */
:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* Light mode - opt-in via Settings (assets/theme.js sets this attribute). */
:root[data-theme="light"] {
    --bg: #f3f4fa;
    --panel: #ffffff;
    --panel-alt: #f1f2f9;
    --border: #e1e3ef;
    --text: #1b1d2a;
    --muted: #666d8a;
    --accent: #7c3aed;
    --accent-hover: #6d28d9;
    --accent-soft: rgba(124, 58, 237, 0.1);
    --bubble-me-text: #ffffff;
    --bubble-them: #eef0f7;
    --danger: #dc2626;
    --danger-soft: rgba(220, 38, 38, 0.08);
    --success: #16a34a;
    --success-soft: rgba(22, 163, 74, 0.08);
    --bg-glow-1: rgba(34, 211, 238, 0.08);
    --bg-glow-2: rgba(168, 85, 247, 0.09);
}

/* ---------- Color themes ----------
   A second, independent axis from light/dark above - pick any combination
   (e.g. Ocean + Light). No attribute / "nebula" is the original cyan-purple
   look and needs no override block, it's just the :root/light defaults
   above. Each theme only overrides --accent-from/--accent-to (the
   gradient) in its base (dark) block, same as the built-in look does -
   light mode always reuses that same gradient, just swaps the solid
   --accent used for borders/text/focus rings for one with better contrast
   on a white background. assets/theme.js applies these via [data-accent]. */

:root[data-accent="ocean"] {
    --accent-from: #38bdf8;
    --accent-to: #14b8a6;
    --accent: #0ea5e9;
    --accent-hover: #0284c7;
    --accent-soft: rgba(14, 165, 233, 0.18);
    --bg-glow-1: rgba(56, 189, 248, 0.13);
    --bg-glow-2: rgba(20, 184, 166, 0.16);
}
:root[data-theme="light"][data-accent="ocean"] {
    --accent: #0284c7;
    --accent-hover: #0369a1;
    --accent-soft: rgba(2, 132, 199, 0.1);
    --bg-glow-1: rgba(56, 189, 248, 0.08);
    --bg-glow-2: rgba(20, 184, 166, 0.09);
}

:root[data-accent="sunset"] {
    --accent-from: #fb923c;
    --accent-to: #ec4899;
    --accent: #f97316;
    --accent-hover: #ea580c;
    --accent-soft: rgba(249, 115, 22, 0.18);
    --bg-glow-1: rgba(251, 146, 60, 0.13);
    --bg-glow-2: rgba(236, 72, 153, 0.16);
}
:root[data-theme="light"][data-accent="sunset"] {
    --accent: #ea580c;
    --accent-hover: #c2410c;
    --accent-soft: rgba(234, 88, 12, 0.1);
    --bg-glow-1: rgba(251, 146, 60, 0.08);
    --bg-glow-2: rgba(236, 72, 153, 0.09);
}

:root[data-accent="forest"] {
    --accent-from: #34d399;
    --accent-to: #a3e635;
    --accent: #22c55e;
    --accent-hover: #16a34a;
    --accent-soft: rgba(34, 197, 94, 0.18);
    --bg-glow-1: rgba(52, 211, 153, 0.13);
    --bg-glow-2: rgba(163, 230, 53, 0.16);
}
:root[data-theme="light"][data-accent="forest"] {
    --accent: #16a34a;
    --accent-hover: #15803d;
    --accent-soft: rgba(22, 163, 74, 0.1);
    --bg-glow-1: rgba(52, 211, 153, 0.08);
    --bg-glow-2: rgba(163, 230, 53, 0.09);
}

:root[data-accent="rose"] {
    --accent-from: #fb7185;
    --accent-to: #e11d48;
    --accent: #f43f5e;
    --accent-hover: #e11d48;
    --accent-soft: rgba(244, 63, 94, 0.18);
    --bg-glow-1: rgba(251, 113, 133, 0.13);
    --bg-glow-2: rgba(225, 29, 72, 0.16);
}
:root[data-theme="light"][data-accent="rose"] {
    --accent: #e11d48;
    --accent-hover: #be123c;
    --accent-soft: rgba(225, 29, 72, 0.1);
    --bg-glow-1: rgba(251, 113, 133, 0.08);
    --bg-glow-2: rgba(225, 29, 72, 0.09);
}

:root[data-accent="slate"] {
    --accent-from: #94a3b8;
    --accent-to: #475569;
    --accent: #64748b;
    --accent-hover: #475569;
    --accent-soft: rgba(100, 116, 139, 0.18);
    --bg-glow-1: rgba(148, 163, 184, 0.13);
    --bg-glow-2: rgba(71, 85, 105, 0.16);
}
:root[data-theme="light"][data-accent="slate"] {
    --accent: #475569;
    --accent-hover: #334155;
    --accent-soft: rgba(71, 85, 105, 0.1);
    --bg-glow-1: rgba(148, 163, 184, 0.08);
    --bg-glow-2: rgba(71, 85, 105, 0.09);
}

/* ---------- Glass mode ----------
   An opt-in look (Settings) - the panel color becomes translucent and
   blurred instead of solid, everywhere a bar/menu/modal already uses
   --panel as a background. color-mix() lets this fade any theme's --panel
   toward transparent without needing a separate "translucent" variable
   defined per theme/light-dark combination. Specificity ([data-glass="1"]
   descendant selectors) beats the plain versions of these same rules
   elsewhere in this file regardless of source order, so this works no
   matter where in the cascade it sits. */
:root[data-glass="1"] .sidebar-brand,
:root[data-glass="1"] .sidebar-header,
:root[data-glass="1"] .chat-view-header {
    background: linear-gradient(var(--accent-soft), var(--accent-soft)), color-mix(in srgb, var(--panel) 60%, transparent);
    backdrop-filter: blur(18px) saturate(160%);
    -webkit-backdrop-filter: blur(18px) saturate(160%);
}

:root[data-glass="1"] .message-form,
:root[data-glass="1"] .modal-box,
:root[data-glass="1"] .bg-menu,
:root[data-glass="1"] .reaction-picker,
:root[data-glass="1"] .emoji-picker,
:root[data-glass="1"] .suggestions,
:root[data-glass="1"] .reply-bar {
    background: color-mix(in srgb, var(--panel) 70%, transparent);
    backdrop-filter: blur(18px) saturate(160%);
    -webkit-backdrop-filter: blur(18px) saturate(160%);
}

* { box-sizing: border-box; }

/* ---------- Motion ----------
   Two kinds: a blanket color transition (so switching theme/accent/glass
   in Settings fades instead of jumping - only paint properties, not
   layout ones, so this is cheap even applied broadly via *), and a set of
   small entrance animations for things that pop in (messages, modals,
   pickers, badges). Entrance animations use `animation`, not `transition` -
   unlike a transition, a CSS animation replays every time an element goes
   from display:none back to visible, which is exactly how .hidden already
   works everywhere in this app, with zero changes needed to that mechanism. */
*, *::before, *::after {
    transition: background-color .2s ease, border-color .2s ease, color .2s ease;
}

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.001ms !important;
    }
}

@keyframes fade-in {
    from { opacity: 0; }
    to { opacity: 1; }
}

@keyframes fade-scale-in {
    from { opacity: 0; transform: scale(0.95) translateY(4px); }
    to { opacity: 1; transform: scale(1) translateY(0); }
}

@keyframes msg-in {
    from { opacity: 0; transform: translateY(10px); }
    to { opacity: 1; transform: translateY(0); }
}

@keyframes pop-in {
    from { opacity: 0; transform: scale(0.5); }
    to { opacity: 1; transform: scale(1); }
}

.msg { animation: msg-in .22s ease; }
.reaction-pill { animation: pop-in .15s ease; }
.unread-badge { animation: pop-in .2s ease; }

.modal { animation: fade-in .15s ease; }
.modal-box { animation: fade-scale-in .2s ease; }

.reaction-picker, .emoji-picker, .bg-menu, .suggestions {
    animation: fade-scale-in .15s ease;
    transform-origin: top;
}

.call-overlay { animation: fade-in .2s ease; }
.chat-view, .empty-state { animation: fade-in .2s ease; }
.typing-indicator, .reply-bar, .edit-bar { animation: fade-in .15s ease; }

button, .theme-option, .accent-option, .chat-list li {
    transition: background-color .15s ease, border-color .15s ease, color .15s ease,
                filter .15s ease, transform .1s ease;
}
button:active { transform: scale(0.97); }

.messages { scroll-behavior: smooth; }

/* A little more life beyond the entrance animations above: hover/press
   feedback on interactive rows, a proper "typing..." dot bounce instead of
   static text, avatars/media fading in as they load instead of popping in
   at whatever instant the network happens to finish, and (on phones) the
   list <-> open-chat swap sliding like a real navigation instead of
   just cutting. */

.chat-list li:hover { transform: translateX(2px); }
.chat-list li:active { transform: scale(0.985); }

@keyframes badge-pulse {
    0%, 100% { box-shadow: 0 0 0 0 var(--accent-soft); }
    50% { box-shadow: 0 0 0 5px transparent; }
}
.chat-list li.has-unread .unread-badge { animation: pop-in .2s ease, badge-pulse 1.8s ease-in-out .2s infinite; }

/* Three dots bouncing in sequence, built by app.js's pollTyping() as
   <span class="typing-dots"><i></i><i></i><i></i></span> next to the text. */
.typing-dots { display: inline-flex; gap: 2px; margin-left: 4px; vertical-align: middle; }
.typing-dots i {
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background: currentColor;
    display: inline-block;
    animation: typing-bounce 1.1s ease-in-out infinite;
}
.typing-dots i:nth-child(2) { animation-delay: .15s; }
.typing-dots i:nth-child(3) { animation-delay: .3s; }
@keyframes typing-bounce {
    0%, 60%, 100% { transform: translateY(0); opacity: 0.5; }
    30% { transform: translateY(-3px); opacity: 1; }
}

@keyframes spin { to { transform: rotate(360deg); } }

/* A generic loading spinner - originally just the map's first-open status
   text, pulled out into its own reusable piece (see .spinner below) and
   dropped in everywhere something loads with nothing to show in the
   meantime: opening a chat, the sidebar's very first load, the message
   info panel, search, the GIF/sticker picker. Two sizes: the bare
   .spinner (drop into any status line via ::before, like the map already
   did) and .loading-pane (a bigger centered one for filling an otherwise-
   empty panel while its first bit of content is still in flight). */
.spinner, .map-status.loading::before {
    content: '';
    display: inline-block;
    width: 10px;
    height: 10px;
    margin-right: 6px;
    border-radius: 50%;
    border: 2px solid var(--border);
    border-top-color: var(--accent);
    vertical-align: -1px;
    animation: spin .7s linear infinite;
    flex-shrink: 0;
}

.loading-pane {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 1;
    padding: 40px 20px;
    color: var(--muted);
    font-size: 13px;
    animation: fade-in .15s ease;
}
.loading-pane::before {
    content: '';
    display: inline-block;
    width: 22px;
    height: 22px;
    margin-right: 10px;
    border-radius: 50%;
    border: 2.5px solid var(--border);
    border-top-color: var(--accent);
    animation: spin .7s linear infinite;
    flex-shrink: 0;
}

/* Skeleton placeholders - a shimmering approximation of the real layout
   (chat rows, message bubbles) instead of .loading-pane's spinner+text, for
   the two spots where that layout is obvious and worth previewing: the
   sidebar's very first load (app.php's static #chatList markup) and
   opening a chat (app.js's selectChat). .loading-pane/.spinner stay in use
   everywhere else (message info, search, GIF picker, ...) - not every
   loading spot has a "shape" worth shimmering. */
@keyframes skeleton-shimmer {
    0% { background-position: -200px 0; }
    100% { background-position: 200px 0; }
}
.skeleton {
    display: inline-block;
    background: linear-gradient(90deg, var(--panel-alt) 25%, var(--border) 50%, var(--panel-alt) 75%);
    background-size: 400px 100%;
    animation: skeleton-shimmer 1.4s ease-in-out infinite;
    border-radius: 6px;
}

.skeleton-row { display: flex; align-items: center; gap: 10px; padding: 10px; }
.skeleton-avatar { width: 36px; height: 36px; border-radius: 50%; flex-shrink: 0; }
.skeleton-lines { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.skeleton-line { height: 10px; border-radius: 5px; }
.skeleton-line.w60 { width: 60%; }
.skeleton-line.w40 { width: 40%; }

.messages-skeleton { display: flex; flex-direction: column; gap: 8px; padding: 4px 0; }
.skeleton-msg { display: flex; }
.skeleton-msg.them { justify-content: flex-start; }
.skeleton-msg.me { justify-content: flex-end; }
.skeleton-bubble { width: 160px; height: 36px; border-radius: 14px; }
.skeleton-bubble.short { width: 100px; }

.avatar img { animation: fade-in .25s ease; }

/* Optimistic send states (see app.js sendMessageText/reconcilePendingMessage/
   markMessageFailed) - a bubble appears the instant you hit send, looking
   like this until the server confirms it (or doesn't). A snappier, more
   noticeable pop than the plain fade-up every other message uses
   (@keyframes msg-in) - this one you want to actually *feel* happen the
   moment you hit send, not just quietly fade into place. */
@keyframes send-pop {
    0% { opacity: 0; transform: translateY(16px) scale(0.92); }
    65% { opacity: 1; transform: translateY(-3px) scale(1.015); }
    100% { opacity: 1; transform: translateY(0) scale(1); }
}
.msg.pending { animation: send-pop .3s cubic-bezier(.34, 1.56, .64, 1); opacity: 0.65; }

/* The send button itself pulses the instant you hit send/Enter (see app.js
   pulseSendButton()) - fires before anything else, so there's feedback tied
   to your tap even before the bubble has finished drawing. */
@keyframes send-pulse {
    0% { transform: scale(1); }
    35% { transform: scale(0.82) rotate(-6deg); }
    70% { transform: scale(1.08) rotate(2deg); }
    100% { transform: scale(1) rotate(0); }
}
.message-form button[type="submit"].sending { animation: send-pulse .35s ease; }
.msg-pending-icon {
    display: inline-block;
    font-size: 6px;
    vertical-align: middle;
    margin-left: 3px;
    animation: typing-bounce 1s ease-in-out infinite;
}
.msg.failed .bubble { border: 1px solid var(--danger); }
.msg-failed-label {
    display: block;
    background: none;
    border: none;
    padding: 2px 0 0;
    margin: 0;
    font: inherit;
    font-size: 11px;
    color: var(--danger);
    cursor: pointer;
    text-align: left;
}
.msg-failed-label:hover { text-decoration: underline; }

.msg-actions button { transition: background-color .15s ease, border-color .15s ease, color .15s ease, transform .1s ease; }
.msg-actions button:hover { transform: translateY(-1px); }

@media (max-width: 860px) {
    @keyframes slide-in-right {
        from { opacity: 0; transform: translateX(28px); }
        to { opacity: 1; transform: translateX(0); }
    }
    /* #mapView also carries the .chat-view class (see app.php), so this
       covers both the open-chat pane and the map pane. Swiping right to
       close (app.js) is a real live drag, not a canned animation - it
       tracks the finger directly via inline transform/opacity/transition
       styles, so there's nothing to add here for that gesture specifically. */
    .app.chat-open .chat-main .chat-view {
        animation: slide-in-right .22s ease;
    }
}

/* ---------- More motion, round two ---------- */

/* A real incoming/outgoing call "ringing" - concentric rings breathing out
   from the avatar, the same visual language every phone call screen uses.
   .ringing is toggled on #callOverlay by app.js's setCallUiState() for the
   'calling' and 'incoming' modes only - stops the instant the call
   connects (mode 'active'), so it never keeps pulsing through an actual
   conversation. */
@keyframes ring-pulse {
    0%   { box-shadow: 0 8px 30px rgba(139, 92, 246, 0.5), 0 0 0 0 var(--accent-soft); }
    70%  { box-shadow: 0 8px 30px rgba(139, 92, 246, 0.5), 0 0 0 24px transparent; }
    100% { box-shadow: 0 8px 30px rgba(139, 92, 246, 0.5), 0 0 0 0 transparent; }
}
.call-overlay.ringing .call-avatar { animation: ring-pulse 1.6s ease-out infinite; }

/* The reaction picker's whole popup already fades/scales in as one unit
   (see .reaction-picker's own rule above) - each emoji inside it *also*
   pops in with a slight cascade, so it reads as a little ripple across the
   row instead of six identical buttons appearing all at once. */
.reaction-picker button {
    animation: pop-in .18s cubic-bezier(.34, 1.56, .64, 1) backwards;
}
.reaction-picker button:nth-child(1) { animation-delay: 0ms; }
.reaction-picker button:nth-child(2) { animation-delay: 30ms; }
.reaction-picker button:nth-child(3) { animation-delay: 60ms; }
.reaction-picker button:nth-child(4) { animation-delay: 90ms; }
.reaction-picker button:nth-child(5) { animation-delay: 120ms; }
.reaction-picker button:nth-child(6) { animation-delay: 150ms; }

/* Picking a theme/accent in Settings gets a little "settle" bounce on the
   option that just became active, instead of its highlight just silently
   appearing - the one visible confirmation that your tap actually landed,
   beyond the color change itself. */
@keyframes settle-bounce {
    0%   { transform: scale(1); }
    40%  { transform: scale(1.06); }
    100% { transform: scale(1); }
}
.theme-option.active, .accent-option.active { animation: settle-bounce .25s ease; }

/* GIF/sticker search results ripple in as they load, instead of the whole
   grid just appearing at once - capped at the first 8 tiles (the only ones
   likely on screen without scrolling anyway) so a big result set doesn't
   make the tail end of the grid wait through a long queue of delays. */
.gif-grid img, .my-sticker-tile, .gif-add-sticker-tile {
    animation: pop-in .2s ease backwards;
}
.gif-grid img:nth-child(1), .my-sticker-tile:nth-child(1) { animation-delay: 0ms; }
.gif-grid img:nth-child(2), .my-sticker-tile:nth-child(2) { animation-delay: 25ms; }
.gif-grid img:nth-child(3), .my-sticker-tile:nth-child(3) { animation-delay: 50ms; }
.gif-grid img:nth-child(4), .my-sticker-tile:nth-child(4) { animation-delay: 75ms; }
.gif-grid img:nth-child(5), .my-sticker-tile:nth-child(5) { animation-delay: 100ms; }
.gif-grid img:nth-child(6), .my-sticker-tile:nth-child(6) { animation-delay: 125ms; }
.gif-grid img:nth-child(7), .my-sticker-tile:nth-child(7) { animation-delay: 150ms; }
.gif-grid img:nth-child(8), .my-sticker-tile:nth-child(8) { animation-delay: 175ms; }

/* Sending/receiving a reaction: the pill it lands in gives a quick
   "squash" instead of only the brand-new pill popping in (existing
   .reaction-pill rule) - makes a *count* changing on an already-visible
   pill (someone else adding the same emoji) register visually too. */
@keyframes reaction-bump {
    0%   { transform: scale(1); }
    45%  { transform: scale(1.18); }
    100% { transform: scale(1); }
}
.reaction-pill:active { animation: reaction-bump .2s ease; }

/* Group/chat avatars (initials or photo) give a small friendly bounce on
   hover in the chat list - a bit of life on the one element that's
   present in literally every row. */
.chat-list .avatar { transition: transform .15s ease; }
.chat-list li:hover .avatar { transform: scale(1.08) rotate(-3deg); }

/* The whole message bubble lifts slightly on hover (desktop only - on
   touch this would just look like a stuck hover state after tapping), on
   top of the existing per-button hover feedback in .msg-actions. */
@media (hover: hover) {
    .msg .bubble { transition: transform .15s ease, box-shadow .15s ease; }
    .msg:hover .bubble { transform: translateY(-1px); box-shadow: 0 4px 14px rgba(0, 0, 0, 0.15); }
}

/* Icon buttons across the header/sidebar get a little rotate-and-lift on
   hover instead of just a color change - settings gear, new-group, map,
   chat background, etc. (anything using the shared .icon-btn class). */
.icon-btn { transition: background-color .15s ease, border-color .15s ease, color .15s ease, transform .15s ease; }
.icon-btn:hover { transform: translateY(-1px) rotate(-4deg); }
.icon-btn:active { transform: scale(0.92) rotate(0); }

/* Safety net: nothing on this page should ever need to scroll sideways -
   if some element is a hair too wide (easy to happen on narrow phones,
   e.g. iPhones with the Dynamic Island's extra-tall safe area), this stops
   it from shifting the whole page instead of just clipping the overflow. */
html, body {
    max-width: 100%;
    overflow-x: hidden;
    background: var(--bg);
}

body {
    margin: 0;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    background-color: var(--bg);
    background-image:
        radial-gradient(circle at 15% 8%, var(--bg-glow-1), transparent 42%),
        radial-gradient(circle at 88% 92%, var(--bg-glow-2), transparent 45%);
    background-attachment: fixed;
    color: var(--text);
}

/* Thin, on-brand scrollbars everywhere. */
* { scrollbar-color: var(--border) transparent; scrollbar-width: thin; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: var(--accent); }

/* ---------- Brand ---------- */
.brand {
    display: flex;
    align-items: center;
    gap: 9px;
    font-weight: 800;
    font-size: 18px;
    letter-spacing: -0.02em;
}

.brand-text {
    background: var(--gradient);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}

.brand-mark {
    width: 26px;
    height: 26px;
    border-radius: 8px;
    flex-shrink: 0;
    box-shadow: 0 2px 10px rgba(139, 92, 246, 0.45);
}

/* ---------- Auth pages ---------- */
.auth-page {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
}

.auth-card {
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 16px;
    padding: 36px 32px;
    width: 100%;
    max-width: 360px;
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.02) inset;
}

.auth-card .brand { margin-bottom: 18px; }

.auth-card h1 {
    margin: 0 0 20px;
    font-size: 15px;
    font-weight: 500;
    color: var(--muted);
}

.auth-card form {
    display: flex;
    flex-direction: column;
    gap: 14px;
}

.auth-card label {
    font-size: 13px;
    color: var(--muted);
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.label-hint { font-weight: 400; opacity: 0.75; }

.auth-card input {
    font-size: 15px;
    padding: 10px 12px;
    border-radius: 8px;
    border: 1px solid var(--border);
    background: var(--panel-alt);
    color: var(--text);
}

.auth-card input::placeholder { color: var(--muted); }

.auth-card input:focus,
.new-chat-box input:focus,
.message-form input:focus,
.modal-box input:focus,
.chip-input input:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-soft);
}

.auth-card button {
    margin-top: 6px;
    padding: 10px 12px;
    border: none;
    border-radius: 8px;
    background: var(--gradient);
    color: #fff;
    font-size: 15px;
    font-weight: 600;
    cursor: pointer;
}

.auth-card button:hover { filter: brightness(1.08); }

.switch { margin-top: 16px; font-size: 13px; color: var(--muted); }
.switch a { color: var(--accent); text-decoration: none; }
.switch a:hover { text-decoration: underline; }

.error {
    background: var(--danger-soft);
    color: var(--danger);
    padding: 8px 10px;
    border-radius: 8px;
    font-size: 13px;
    margin: 0 0 14px;
}

.success {
    background: var(--success-soft);
    color: var(--success);
    padding: 8px 10px;
    border-radius: 8px;
    font-size: 13px;
    margin: 10px 0 0;
}

/* ---------- Profile photos ---------- */
.avatar {
    border-radius: 50%;
    overflow: hidden;
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--gradient);
    color: #fff;
    font-weight: 700;
    text-transform: uppercase;
}

.avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.avatar svg { width: 62%; height: 62%; display: block; } /* the "Gemini" AI contact's sparkle icon - see ICON_GEMINI in app.js */

.avatar-sm { width: 28px; height: 28px; font-size: 12px; }
.avatar-md { width: 36px; height: 36px; font-size: 14px; }
.avatar-lg { width: 84px; height: 84px; font-size: 30px; }

.avatar-section {
    display: flex;
    align-items: center;
    gap: 14px;
    margin-bottom: 18px;
}

.avatar-actions {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.group-details-photo-row {
    display: flex;
    align-items: center;
    gap: 14px;
    margin-bottom: 16px;
}

.group-bg-actions {
    display: flex;
    gap: 8px;
    margin-bottom: 4px;
}

/* ---------- Chat app ---------- */
/* Plain 100vh - two attempts at a keyboard-aware version of this (a JS
   visualViewport calculation, then 100dvh) each broke something different
   on mobile (a huge black area, then the header disappearing entirely) in
   ways that couldn't be reproduced/debugged without a real device to test
   on. Reverted back to this known-good baseline rather than guess a third
   time - the header staying on screen reliably matters more than the
   keyboard-push issue this was trying to fix. Revisit with a screenshot/
   video of the actual problem (and which phone/browser) rather than blind. */
.chat-page { height: 100vh; overflow: hidden; }

.app {
    display: flex;
    height: 100vh;
}

.sidebar {
    width: 300px;
    flex-shrink: 0;
    background: var(--panel);
    border-right: 1px solid var(--border);
    display: flex;
    flex-direction: column;
}

.sidebar-brand {
    padding: calc(16px + env(safe-area-inset-top)) 16px 0;
}

.sidebar-brand, .sidebar-header, .chat-view-header {
    /* Layered: the accent tint on top, a solid panel color underneath so
       text stays readable regardless of how transparent --accent-soft is -
       this is what makes these bars visibly shift with the chosen color
       theme (see Settings) instead of staying neutral gray always. */
    background: linear-gradient(var(--accent-soft), var(--accent-soft)), var(--panel);
}

.sidebar-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 14px 16px;
    border-bottom: 1px solid var(--border);
}

.me-wrap { display: flex; align-items: center; gap: 8px; min-width: 0; }
.my-username { font-weight: 600; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.logout { font-size: 12px; color: var(--muted); text-decoration: none; }
.logout:hover { color: var(--danger); }

/* All the action icons that used to live in .sidebar-header, now down here
   in one slim bar instead (icon-only, no labels - that's what keeps it
   thin) - a fixed-height row, not scrolling away with .chat-list above it. */
.sidebar-bottom-nav {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: space-around;
    padding: 8px 4px;
    border-top: 1px solid var(--border);
}
.sidebar-bottom-nav .icon-btn {
    padding: 6px;
    font-size: 20px;
}
.sidebar-bottom-nav .icon { width: 20px; height: 20px; margin: 0; vertical-align: middle; }

.icon-btn {
    background: transparent;
    border: none;
    color: var(--muted);
    font-size: 15px;
    cursor: pointer;
    padding: 2px;
    line-height: 1;
}

.icon-btn:hover { color: var(--text); }

/* Chatties' own icon set (includes/icons.php) - inline SVGs that inherit
   whatever color the surrounding button/text already has. Buttons that mix
   an icon with a text label (e.g. "Map", theme options) get a small gap via
   vertical-align + margin instead of flex, so they drop into existing
   button markup without needing to restructure every one into a flex row. */
.icon {
    width: 17px;
    height: 17px;
    vertical-align: -3px;
    margin-right: 2px;
    flex-shrink: 0;
}

.msg-react-btn .icon, .msg-reply-btn .icon, .msg-forward-btn .icon, .msg-edit-btn .icon {
    width: 13px;
    height: 13px;
    vertical-align: -2px;
    margin-right: 0;
}

.theme-option .icon { width: 15px; height: 15px; }

.settings-hint {
    font-size: 12px;
    color: var(--muted);
    margin: -6px 0 14px;
}

.theme-picker {
    display: flex;
    gap: 6px;
    margin: 6px 0 16px;
}

.theme-option {
    flex: 1;
    padding: 9px 6px;
    border-radius: 8px;
    border: 1px solid var(--border);
    background: var(--panel-alt);
    color: var(--text);
    font-size: 12px;
    cursor: pointer;
    text-align: center;
}

.theme-option:hover { border-color: var(--accent); }

.theme-option.active {
    border-color: var(--accent);
    background: var(--accent-soft);
    font-weight: 600;
}

.accent-picker {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 6px 0 16px;
}

.accent-option {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 5px;
    width: 62px;
    padding: 8px 4px;
    border-radius: 10px;
    border: 1px solid var(--border);
    background: var(--panel-alt);
    color: var(--text);
    font-size: 11px;
    cursor: pointer;
}

.accent-option:hover { border-color: var(--accent); }

.accent-option.active {
    border-color: var(--accent);
    background: var(--accent-soft);
    font-weight: 600;
}

/* The little colored circle preview - gradient comes from a per-button
   inline style (--swatch), set once in app.php, one per palette. */
.accent-swatch {
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: var(--swatch);
}

.new-chat-box {
    display: flex;
    gap: 6px;
    padding: 12px 16px 0;
}

.new-chat-box input {
    flex: 1;
    min-width: 0;
    padding: 8px 10px;
    border-radius: 8px;
    border: 1px solid var(--border);
    background: var(--panel-alt);
    color: var(--text);
    font-size: 14px;
}

.new-chat-box input::placeholder { color: var(--muted); }

.new-chat-box button {
    padding: 8px 12px;
    border-radius: 8px;
    border: none;
    background: var(--gradient);
    color: #fff;
    cursor: pointer;
    font-size: 13px;
    font-weight: 600;
}

.new-chat-box button:hover { filter: brightness(1.08); }

.suggestions {
    margin: 4px 16px 0;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: 8px;
    overflow: hidden;
}

.suggestions .suggestion {
    padding: 8px 10px;
    font-size: 14px;
    cursor: pointer;
}

.suggestions .suggestion:hover { background: var(--accent-soft); }

/* @mention autocomplete - same look as .suggestions above, but floats
   above the input row instead of taking up space in its flex layout. */
.message-input-row { position: relative; }
#mentionSuggestions {
    position: absolute;
    z-index: 25;
    bottom: calc(100% + 6px);
    /* Roughly aligned with the pill input itself, not the whole row - the
       row now also has the leading "+" button and trailing photo/mic/send
       buttons on either side of it. */
    left: 48px;
    right: 90px;
    margin: 0;
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
}


.chat-list {
    list-style: none;
    margin: 8px 0 0;
    padding: 0 8px;
    overflow-y: auto;
    flex: 1;
}

/* Stories bar (see renderStoriesBar in app.js) - a horizontal scroller of
   avatar rings above the chat list, same idea as Instagram/WhatsApp
   Status. */
.stories-bar {
    display: flex;
    gap: 14px;
    padding: 10px 12px;
    overflow-x: auto;
    flex-shrink: 0;
    border-bottom: 1px solid var(--border);
}

/* "On this day" banner (see checkMemories in app.js). */
.memory-banner {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 8px 12px 0;
    padding: 10px 12px;
    border-radius: 10px;
    background: var(--accent-soft);
    flex-shrink: 0;
    cursor: pointer;
}
.memory-banner-icon { font-size: 22px; flex-shrink: 0; }
.memory-banner-text { flex: 1; min-width: 0; }
.memory-banner-label { font-size: 12px; font-weight: 600; color: var(--accent-to); }
.memory-banner-preview {
    font-size: 13px;
    color: var(--text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.memory-banner button {
    background: none;
    border: none;
    color: var(--muted);
    font-size: 18px;
    cursor: pointer;
    flex-shrink: 0;
}
.story-item {
    flex: none;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    width: 60px;
    background: none;
    border: none;
    cursor: pointer;
    color: var(--text);
    font: inherit;
}
.story-item-ring {
    width: 54px;
    height: 54px;
    border-radius: 50%;
    padding: 2px;
    display: flex;
    align-items: center;
    justify-content: center;
    /* Unseen (at least one un-viewed item) gets the gradient ring; a fully
       seen story just gets a flat muted one - same visual language every
       Stories-style feature uses to mean "already looked at this". */
    background: var(--gradient);
}
.story-item.seen .story-item-ring { background: var(--border); }
.story-item-ring .avatar { width: 100%; height: 100%; border: 2px solid var(--panel); }
.story-item-name {
    font-size: 11px;
    max-width: 60px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* "My Story" with nothing posted yet - a plain "+" instead of a ring, tap
   to post one (see #storyFileInput). */
.story-item-add .story-item-ring {
    background: var(--panel-alt);
    border: 2px dashed var(--border);
}
.story-item-add-icon {
    font-size: 22px;
    color: var(--accent);
    line-height: 1;
}

/* The full-screen story viewer - reuses .photo-viewer's fixed/centered/
   dark-backdrop base (see that block above), same as .snap-viewer. */
.story-viewer { flex-direction: column; padding: 0; }
.story-viewer-progress-row {
    position: absolute;
    top: max(10px, env(safe-area-inset-top));
    left: 12px;
    right: 12px;
    display: flex;
    gap: 4px;
    z-index: 2;
}
.story-viewer-progress-seg {
    flex: 1;
    height: 3px;
    border-radius: 2px;
    background: rgba(255, 255, 255, 0.3);
    overflow: hidden;
}
.story-viewer-progress-seg-fill {
    height: 100%;
    width: 0%;
    background: #fff;
}
.story-viewer-header {
    position: absolute;
    top: calc(22px + env(safe-area-inset-top));
    left: 12px;
    right: 12px;
    z-index: 2;
    display: flex;
    align-items: center;
    gap: 8px;
    color: #fff;
}
.story-viewer-username { font-weight: 600; font-size: 14px; }
.story-viewer-time { font-size: 12px; opacity: .75; }
.story-viewer-delete {
    margin-left: auto;
    background: none;
    border: none;
    color: #fff;
    font-size: 18px;
    cursor: pointer;
    padding: 6px;
}
.story-viewer img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    margin: auto;
}
.story-viewer-nav {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 35%;
    background: none;
    border: none;
    cursor: pointer;
}
.story-viewer-nav-prev { left: 0; }
.story-viewer-nav-next { right: 0; }

.sidebar-footer {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 5px;
    padding: 8px 16px calc(8px + env(safe-area-inset-bottom));
    font-size: 11px;
    color: var(--muted);
}
.sidebar-footer .icon { width: 12px; height: 12px; margin-right: 0; }

.chat-list li {
    padding: 10px 10px;
    border-radius: 10px;
    cursor: pointer;
    margin-bottom: 2px;
    border: 1px solid transparent;
}

.chat-list li:hover { background: var(--panel-alt); }
.chat-list li.active {
    background: var(--accent-soft);
    border-color: rgba(139, 92, 246, 0.4);
}

.chat-row { display: flex; align-items: center; gap: 10px; min-width: 0; }
.chat-row-text { min-width: 0; flex: 1; }

.chat-list .title {
    font-weight: 600;
    font-size: 14px;
    display: flex;
    align-items: center;
    gap: 6px;
}

.chat-list .badge {
    font-size: 10px;
    background: var(--gradient);
    color: #fff;
    border-radius: 5px;
    padding: 1px 5px;
}

.chat-list .preview {
    font-size: 12px;
    color: var(--muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.chat-list li.has-unread .title { font-weight: 700; color: var(--text); }
.chat-list li.has-unread .preview { color: var(--text); }

.unread-badge {
    flex-shrink: 0;
    min-width: 20px;
    height: 20px;
    padding: 0 6px;
    border-radius: 10px;
    background: var(--gradient);
    color: #fff;
    font-size: 11px;
    font-weight: 700;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* Pin/unpin a chat to the top of the list (see toggleChatPinned in app.js -
   a local, per-browser preference, not shared with anyone else in the
   chat). Hidden until hover on desktop, faintly visible always on touch -
   same convention as .msg-delete on a message bubble. */
.chat-pin-btn {
    flex-shrink: 0;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    border: none;
    background: transparent;
    color: var(--muted);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    padding: 0;
    opacity: 0;
    transition: opacity .15s ease, background-color .15s ease, color .15s ease;
}
.chat-pin-btn .icon { width: 15px; height: 15px; }
.chat-list li:hover .chat-pin-btn { opacity: 1; }
.chat-pin-btn.pinned { opacity: 1; color: var(--accent); }
.chat-pin-btn:hover { background: var(--accent-soft); color: var(--accent); }
@media (hover: none) {
    .chat-pin-btn { opacity: 0.55; }
    .chat-pin-btn.pinned { opacity: 1; }
}

.chat-main {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

/* Caps how wide an open chat/map/Shorts/profile pane ever gets and centers
   it, instead of stretching edge-to-edge on a wide desktop monitor - message
   bubbles especially read badly at full width on anything wider than a
   laptop screen. A plain max-width (not a media query) - on anything
   narrower than this, .chat-view is already narrower than the cap anyway,
   so this is a no-op on mobile/tablet and only ever matters on wide screens.
   margin: 0 auto centers it within .chat-main's own flex row (auto margins
   absorb the leftover space either side, same trick as centering anything
   else in a flex container). */
.chat-view {
    max-width: 900px;
    margin: 0 auto;
    width: 100%;
}

.empty-state {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 10px;
    color: var(--muted);
    text-align: center;
    padding: 0 20px;
}
.empty-state-icon {
    font-size: 46px;
    opacity: .6;
}
.empty-state p { margin: 0; max-width: 280px; }

.chat-view { flex: 1; display: flex; flex-direction: column; min-height: 0; min-width: 0; }
.hidden { display: none !important; }

/* Smooth closing for overlay-style menus/popups - modals, dropdown menus,
   the reaction/emoji pickers, the message context menu, the photo/snap/
   story viewers. Each of these already had its own open animation (see the
   fade-in/fade-scale-in rules near the top of this file) but closing them
   just meant an instant display:none - opening looked smooth, closing
   looked like it snapped away. These selectors override .hidden with
   opacity+visibility instead, which CAN be transitioned (display can't) -
   safe to do here specifically because every one of them is
   position:fixed/absolute, so keeping them in the render tree (just
   invisible) while "closed" doesn't shift any surrounding layout.
   NOTE: .suggestions is deliberately NOT included here - unlike the rest,
   it's a plain in-flow block (no position:absolute/fixed), so doing the
   same trick to it would leave an invisible-but-space-occupying gap behind
   whenever it's "closed", pushing the chat list/message row down - it
   keeps the plain instant display:none instead. */
.modal.hidden,
.bg-menu.hidden,
.reaction-picker.hidden,
.emoji-picker.hidden,
.message-context-menu.hidden,
.photo-viewer.hidden {
    display: flex !important;
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity .15s ease, visibility .15s;
}
.emoji-picker.hidden { display: block !important; }
.modal, .bg-menu, .reaction-picker, .emoji-picker, .message-context-menu, .photo-viewer {
    opacity: 1;
    visibility: visible;
    transition: opacity .15s ease, visibility .15s;
}

.chat-view-header {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: calc(14px + env(safe-area-inset-top)) 16px 14px;
    border-bottom: 1px solid var(--border);
    /* background comes from the shared .sidebar-brand/.sidebar-header/
       .chat-view-header rule above - keeps the accent tint. */
    font-weight: 600;
}

/* Name + (when there is one) the typing indicator stacked underneath it,
   like a native chat app's name/"last seen"/"typing..." header - the
   typing indicator is the exact same element/id it always was, just
   relocated here from its own separate bar below the header (see app.php) -
   app.js's show/hide logic didn't need to change at all for that. */
.chat-view-title-block {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 1px;
}
#chatTitle {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* Streak (see includes/streaks.php) - used both in the chat header
   (#chatStreakBadge) and inline in a chat-list row's title. */
.streak-badge {
    display: inline-flex;
    font-size: 12px;
    font-weight: 600;
    color: #f59e0b;
}
.chat-view-title-block .typing-indicator {
    margin: 0;
    padding: 0;
    font-size: 12px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.back-btn {
    display: none;
    flex-shrink: 0;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border: none;
    border-radius: 50%;
    background: var(--panel-alt);
    cursor: pointer;
    color: var(--text);
    padding: 0;
}
.back-btn:hover { background: var(--border); }
.back-btn .icon { width: 19px; height: 19px; }

.chat-view-actions {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-shrink: 0;
}

.chat-view-header button {
    font-size: 12px;
    padding: 6px 10px;
    border-radius: 7px;
    border: 1px solid var(--border);
    background: var(--panel-alt);
    color: var(--text);
    cursor: pointer;
}

/* Call buttons share one pill-shaped background instead of each getting
   their own circle - a native-feeling call-button group that also takes
   less header width than two fully separate round buttons. At least one
   of the two is always visible for any chat calling is enabled for
   (video is additionally private-chat-only), so this pill not clearing
   itself when both happen to be hidden isn't a real-world case worth
   chasing down with fragile CSS. */
.chat-header-call-actions {
    display: flex;
    align-items: center;
    background: var(--panel-alt);
    border-radius: 20px;
    padding: 2px;
}
.chat-view-header .chat-header-call-actions button {
    width: 36px;
    height: 36px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: none;
    background: transparent;
    border-radius: 50%;
    color: var(--text);
    cursor: pointer;
    padding: 0;
}
.chat-view-header .chat-header-call-actions button:hover { background: var(--border); }
.chat-header-call-actions .icon { width: 18px; height: 18px; }

/* The rest of the header's action buttons (background/group details/
   delete) - plain round icon buttons, ID-targeted so this reliably beats
   the ".chat-view-header button" rule above regardless of source order. */
.chat-view-header #chatBgBtn,
.chat-view-header #groupDetailsBtn,
.chat-view-header #deleteChatBtn {
    width: 36px;
    height: 36px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: none;
    border-radius: 50%;
    background: var(--panel-alt);
    color: var(--text);
    cursor: pointer;
    padding: 0;
    font-size: 15px;
}
.chat-view-header #chatBgBtn:hover,
.chat-view-header #groupDetailsBtn:hover,
.chat-view-header #deleteChatBtn:hover { background: var(--border); }
.chat-view-header #chatBgBtn .icon,
.chat-view-header #deleteChatBtn .icon { width: 17px; height: 17px; }

.chat-view-header button:hover { border-color: var(--accent); }

/* Not scoped to .chat-view-header - originally only the header's own
   Delete chat button needed this, but groupLeaveOrDeleteBtn/clearChatBtn
   (inside the group details modal) want the same red "this is destructive"
   look and were falling back to a plain, un-red secondary button style
   without it. */
button.danger {
    border-color: rgba(248, 113, 113, 0.4);
    color: var(--danger);
}

button.danger:hover {
    background: var(--danger-soft);
    border-color: var(--danger);
}

/* The positioning context for #scrollToBottomBtn (see app.php's comment on
   .messages-wrap for why the button can't just live inside #messages). */
.messages-wrap {
    flex: 1;
    min-width: 0;
    min-height: 0;
    display: flex;
    position: relative;
}

.messages {
    flex: 1;
    min-width: 0;
    min-height: 0;
    overflow-y: auto;
    padding: 16px 20px;
    display: flex;
    flex-direction: column;
    gap: 8px;
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
}

/* Floats over the bottom-right corner of the message list once you've
   scrolled away from the latest message (see the scroll listener in
   app.js) - jumps back down on tap, with a badge for how many messages
   arrived while you weren't looking. */
.scroll-to-bottom {
    position: absolute;
    right: 20px;
    bottom: 16px;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    border: 1px solid var(--border);
    background: var(--panel);
    color: var(--text);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
    animation: pop-in .15s ease;
}
.scroll-to-bottom:hover { background: var(--panel-alt); }
.scroll-to-bottom .icon { width: 18px; height: 18px; }

.scroll-to-bottom-badge {
    position: absolute;
    top: -4px;
    right: -4px;
    min-width: 18px;
    height: 18px;
    padding: 0 4px;
    border-radius: 999px;
    background: var(--accent);
    color: #fff;
    font-size: 10px;
    font-weight: 700;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.bg-menu-wrap { position: relative; }

.bg-menu {
    position: absolute;
    top: calc(100% + 6px);
    right: 0;
    display: flex;
    flex-direction: column;
    min-width: 180px;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 10px;
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
    overflow: hidden;
    z-index: 20;
}

/* The "+" attach menu (see #attachMenuWrap in app.php) - it lives in the
   bottom toolbar, so it opens *upward* instead of the default downward
   .bg-menu direction (opening down there would push it half off-screen). */
.attach-menu {
    top: auto;
    bottom: calc(100% + 6px);
    left: 0;
    right: auto;
}

/* The chat header's "..." overflow menu (see #chatOverflowMenu) - same
   idea, holds what used to be a whole row of separate icons. The
   background/sound pickers still nested inside it (#chatBgWrap/
   #chatSoundWrap) keep their own further-nested .bg-menu popovers
   completely unchanged - only this outer level is new. */
.header-overflow-menu {
    min-width: 220px;
    padding: 4px;
}
.header-overflow-menu > button,
.header-overflow-menu > .bg-menu-wrap > button {
    width: 100%;
    text-align: left;
    border-radius: 8px;
    padding: 10px 12px;
}
.header-overflow-menu .bg-menu-wrap { position: relative; }
/* A nested picker (background/sound) opens to the *left* of the overflow
   panel rather than stacking underneath it - keeps it from running off
   the right edge of the screen, since the overflow panel itself is already
   right-aligned under the "..." button. */
.header-overflow-menu .bg-menu-wrap .bg-menu {
    top: 0;
    right: calc(100% + 6px);
    left: auto;
}

.bg-menu button.active { color: var(--accent); font-weight: 600; }
.bg-menu button {
    padding: 10px 14px;
    border: none;
    background: transparent;
    color: var(--text);
    font-size: 13px;
    text-align: left;
    cursor: pointer;
}

.bg-menu button:hover { background: var(--panel-alt); }
.bg-menu button:not(:last-child) { border-bottom: 1px solid var(--border); }

.msg { max-width: 60%; position: relative; }
.msg .sender { font-size: 11px; color: var(--muted); margin-bottom: 2px; }
.msg .bubble {
    position: relative; /* anchors the little speech-bubble tail (see .msg.them/.msg.me .bubble::before) to the bubble itself, not the whole .msg row */
    padding: 8px 12px;
    border-radius: 14px;
    font-size: 14px;
    line-height: 1.4;
    word-wrap: break-word;
    white-space: pre-wrap;
    /* Without this, holding a bubble down long enough to trigger the
       custom long-press menu (see app.js's startLongPress) also triggers
       the browser's own text-selection/callout at the same time - the
       message ends up highlighted blue, or iOS pops up its own "Copy/Look
       Up/..." menu right underneath ours. The menu already has its own
       "Copy text" action, so there's no real selection need on the bubble
       itself. */
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
}
.msg .time { font-size: 10px; color: var(--muted); margin-top: 2px; }

.edited-label { font-size: 10px; color: var(--muted); font-style: italic; margin-right: 4px; }

/* Sent (single check) / delivered (double, muted) / read (double, accent) -
   see app.js's ticksHtml/refreshAllTicks. Replaces the old single "Seen at
   ..." line under just the last message with a per-message indicator,
   WhatsApp-style. */
.msg-ticks {
    font-size: 10px;
    letter-spacing: -2px;
    margin-left: 3px;
    color: var(--muted);
    display: inline-block;
}
.msg-ticks.sent { letter-spacing: 0; }
.msg-ticks.read {
    color: var(--accent-from);
    animation: pop-in .2s ease;
}

.mention {
    color: var(--accent-to);
    font-weight: 600;
    background: var(--accent-soft);
    border-radius: 4px;
    padding: 0 2px;
}

/* A call's outcome (missed/declined/ended) - a centered system-style line,
   not a real chat bubble (see log_call_outcome.php). */
.msg.call-log {
    align-self: center;
    max-width: 80%;
    text-align: center;
    color: var(--muted);
    font-size: 12px;
    margin: 4px 0;
}
.call-log-text {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: var(--panel-alt);
    border-radius: 20px;
    padding: 5px 12px;
}

/* ---------- Polls (see api/create_poll.php / api/vote_poll.php) ---------- */

.poll-bubble { min-width: 220px; }
.poll-question { font-weight: 600; margin-bottom: 8px; }
.poll-options { display: flex; flex-direction: column; gap: 6px; }
.poll-option {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 8px 10px;
    border-radius: 8px;
    border: 1px solid var(--border);
    background: var(--panel-alt);
    color: var(--text);
    text-align: left;
    cursor: pointer;
    overflow: hidden;
}
.poll-option.voted { border-color: var(--accent); }
.poll-option-fill {
    position: absolute;
    inset: 0;
    background: var(--accent-soft);
    z-index: 0;
    transition: width .3s ease;
}
.poll-option-label, .poll-option-pct { position: relative; z-index: 1; font-size: 13px; }
.poll-option-pct { flex-shrink: 0; color: var(--muted); font-size: 12px; }
.poll-total { margin-top: 6px; font-size: 11px; color: var(--muted); }

.poll-options-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 8px; }
.poll-option-row { display: flex; gap: 6px; }
.poll-option-row input { flex: 1; }
.poll-option-remove {
    background: none;
    border: none;
    color: var(--muted);
    font-size: 18px;
    cursor: pointer;
    flex-shrink: 0;
}
.call-log .time { display: block; margin-top: 2px; }

.msg.them { align-self: flex-start; }
.msg.them .bubble { background: var(--bubble-them); color: var(--text); border-bottom-left-radius: 4px; }
/* The little speech-bubble "tail" - a small triangle flush against the
   corner .border-bottom-left-radius above already flattened, same look
   most chat apps use. Call-log/system lines never get one (they don't
   use .bubble at all, see .call-log-text), and reply previews/reactions
   sit outside .bubble so this doesn't interfere with them. Skipped for
   stickers (no bubble background at all - see .bubble-sticker, a tail
   would just be a stray colored triangle next to a transparent image)
   and GIFs (always the same fixed color regardless of who sent it - see
   .bubble-gif - so a tail colored for "them"/"me" wouldn't match). */
.msg.them .bubble:not(.bubble-sticker):not(.bubble-gif)::before {
    content: '';
    position: absolute;
    left: -6px;
    bottom: 0;
    width: 10px;
    height: 12px;
    background: var(--bubble-them);
    clip-path: polygon(100% 0, 100% 100%, 0% 100%);
}

.msg.me { align-self: flex-end; text-align: right; }

.msg-delete {
    position: absolute;
    top: -6px;
    left: -6px;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    border: 1px solid var(--border);
    background: var(--panel-alt);
    color: var(--muted);
    font-size: 13px;
    line-height: 18px;
    text-align: center;
    padding: 0;
    cursor: pointer;
    opacity: 0;
    transition: opacity 0.15s ease;
}

.msg.me:hover .msg-delete { opacity: 1; }
.msg-delete:hover { background: var(--danger-soft); border-color: var(--danger); color: var(--danger); }

/* No hover on touch devices - just show it faintly so it's still discoverable. */
@media (hover: none) {
    .msg-delete { opacity: 0.55; }
}
.msg.me .bubble { background: var(--gradient); color: var(--bubble-me-text); border-bottom-right-radius: 4px; }
.msg.me .bubble:not(.bubble-sticker):not(.bubble-gif)::before {
    content: '';
    position: absolute;
    right: -6px;
    bottom: 0;
    width: 10px;
    height: 12px;
    background: var(--gradient);
    clip-path: polygon(0 0, 100% 100%, 0% 100%);
}

/* Consecutive messages from the same sender, close together in time (see
   appendMessages()'s `grouped` check) - sit closer together, drop the tail,
   and round the touching top corner flat, so a run of them reads as one
   continuous thought instead of a stack of identical bubbles. */
.msg.grouped { margin-top: -4px; }
.msg.them.grouped .bubble { border-top-left-radius: 4px; }
.msg.me.grouped .bubble { border-top-right-radius: 4px; }
.msg.them.grouped .bubble::before,
.msg.me.grouped .bubble::before {
    display: none;
}

/* A thin "Today"/"Monday"/"14 March" divider between days (see
   maybeInsertDateDivider in app.js) - centered, pill-shaped, quiet enough
   not to compete with the messages themselves. */
.date-divider {
    align-self: center;
    padding: 4px 12px;
    margin: 4px 0;
    border-radius: 999px;
    background: var(--panel-alt);
    color: var(--muted);
    font-size: 11px;
    font-weight: 600;
    animation: fade-in .2s ease;
}

/* Same divider, accent-tinted - marks where you left off (see
   insertUnreadDivider in app.js), so it stands out from the plain date
   dividers around it instead of blending in as just another one. */
.unread-divider {
    background: var(--accent-soft);
    color: var(--accent);
}

/* The "Gemini" AI chat contact (see #chatView.ai-chat, toggled in
   selectChat()) - its own messages get an accent-tinted bubble instead of
   the plain flat chat-bubble color, and its header avatar gets a soft
   matching glow, so the whole conversation reads as visually distinct from
   a normal person-to-person chat, echoing its sparkle avatar. */
#chatView.ai-chat .msg.them .bubble { background: var(--accent-soft); color: var(--text); }
#chatView.ai-chat .msg.them .bubble:not(.bubble-sticker):not(.bubble-gif)::before { background: var(--accent-soft); }
#chatView.ai-chat #chatHeaderAvatar { box-shadow: 0 0 0 3px var(--accent-soft); }

/* Same idea for the second AI chat contact, "Groq" (#chatView.groq-chat) -
   a fixed amber tint rather than var(--accent-soft), since that variable
   already follows whichever accent color/theme the user picked in Settings
   (see the AI block above) and would otherwise make Groq's messages look
   identical to Gemini's whenever an orange theme happens to be active. */
#chatView.groq-chat .msg.them .bubble { background: rgba(249, 115, 22, 0.18); color: var(--text); }
#chatView.groq-chat .msg.them .bubble:not(.bubble-sticker):not(.bubble-gif)::before { background: rgba(249, 115, 22, 0.18); }
#chatView.groq-chat #chatHeaderAvatar { box-shadow: 0 0 0 3px rgba(249, 115, 22, 0.18); }
:root[data-theme="light"] #chatView.groq-chat .msg.them .bubble { background: rgba(234, 88, 12, 0.1); }
:root[data-theme="light"] #chatView.groq-chat .msg.them .bubble:not(.bubble-sticker):not(.bubble-gif)::before { background: rgba(234, 88, 12, 0.1); }
:root[data-theme="light"] #chatView.groq-chat #chatHeaderAvatar { box-shadow: 0 0 0 3px rgba(234, 88, 12, 0.1); }

/* The "someone's typing" bubble (see showTypingBubble in app.js) - a real
   .msg.them row, so it automatically gets the exact same bubble color/tail/
   grouping as a genuine received message; only the dots inside are custom. */
.typing-msg .typing-bubble {
    display: flex;
    align-items: center;
    gap: 3px;
    padding: 11px 14px;
}
.typing-bubble span {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: currentColor;
    opacity: 0.6;
    animation: typing-bounce 1.1s ease-in-out infinite;
}
.typing-bubble span:nth-child(2) { animation-delay: .15s; }
.typing-bubble span:nth-child(3) { animation-delay: .3s; }

/* React/reply icons - shown on hover (desktop) or faintly always (touch),
   same convention as .msg-delete above. */
.msg-actions {
    display: flex;
    gap: 4px;
    margin-top: 3px;
    opacity: 0;
    transition: opacity 0.15s ease;
}
.msg.them .msg-actions { justify-content: flex-start; }
.msg.me .msg-actions { justify-content: flex-end; }
.msg:hover .msg-actions { opacity: 1; }
@media (hover: none) {
    .msg-actions { opacity: 0.55; }
}

.msg-react-btn, .msg-reply-btn, .msg-forward-btn, .msg-edit-btn {
    border: 1px solid var(--border);
    background: var(--panel-alt);
    color: var(--muted);
    border-radius: 20px;
    font-size: 11px;
    line-height: 1;
    padding: 3px 6px;
    cursor: pointer;
}
.msg-react-btn:hover, .msg-reply-btn:hover, .msg-forward-btn:hover, .msg-edit-btn:hover { border-color: var(--accent); color: var(--text); }

.reactions {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    margin-top: 4px;
}
.msg.me .reactions { justify-content: flex-end; }

.reaction-pill {
    border: 1px solid var(--border);
    background: var(--panel-alt);
    color: var(--text);
    border-radius: 20px;
    font-size: 12px;
    padding: 2px 8px;
    cursor: pointer;
}
.reaction-pill.mine { border-color: var(--accent); background: var(--accent-soft); }
.reaction-pill:hover { border-color: var(--accent); }

/* Quoted preview of the message being replied to - sits above the bubble. */
.reply-preview {
    border-left: 3px solid var(--accent);
    background: var(--panel-alt);
    border-radius: 6px;
    padding: 4px 8px;
    margin-bottom: 4px;
    text-align: left;
    max-width: 100%;
    cursor: pointer;
    transition: background-color .15s ease;
}
/* Tap/click jumps to the original message (see the .reply-preview click
   handler in app.js) - same idea as tapping a search result or the pinned
   banner. */
.reply-preview:hover { background: var(--border); }
.reply-preview-sender { font-size: 11px; font-weight: 600; color: var(--accent-to); }
.reply-preview-text {
    font-size: 12px;
    color: var(--muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* "@8ball <vraag>" (see commandBubbleHtml in app.js) - the original
   question, shown smaller/muted above its answer so both stay visible. */
.command-question {
    font-size: 12px;
    opacity: .75;
    margin-bottom: 2px;
}

/* "@shake" (see triggerScreenShake in app.js) - a brief side-to-side shake
   of the whole app, not just the message list, so it reads as "the chat
   itself got shaken" rather than a small in-place bubble animation.
   Covered by the global prefers-reduced-motion rule near the top of this
   file like every other animation here - no separate opt-out needed. */
@keyframes screen-shake {
    10%, 90% { transform: translateX(-1px); }
    20%, 80% { transform: translateX(2px); }
    30%, 50%, 70% { transform: translateX(-4px); }
    40%, 60% { transform: translateX(4px); }
}
.screen-shake {
    animation: screen-shake .5s cubic-bezier(.36, .07, .19, .97) both;
}

/* Floating emoji palette opened by .msg-react-btn - positioned in JS via
   getBoundingClientRect() of whichever button was tapped. */
.reaction-picker {
    position: absolute;
    z-index: 30;
    display: flex;
    gap: 2px;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 20px;
    padding: 4px;
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
}
.reaction-picker button {
    border: none;
    background: transparent;
    font-size: 20px;
    line-height: 1;
    padding: 4px 6px;
    border-radius: 50%;
    cursor: pointer;
}

/* Long-press (or right-click) message context menu - see app.js's
   openMessageContextMenu. Positioned the same way .reaction-picker is
   (absolute + scroll-adjusted top/left set from JS). */
.message-context-menu {
    position: absolute;
    z-index: 40;
    display: flex;
    flex-direction: column;
    min-width: 170px;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 6px;
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
    animation: fade-scale-in .12s ease;
    transform-origin: top left;
}
.message-context-menu button {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 9px 10px;
    border: none;
    background: transparent;
    color: var(--text);
    font-size: 14px;
    text-align: left;
    border-radius: 8px;
    cursor: pointer;
}
.message-context-menu button:hover { background: var(--panel-alt); }
.message-context-menu button.danger:hover { background: var(--danger-soft); }
.message-context-menu .icon { width: 16px; height: 16px; flex-shrink: 0; }

/* A small star badge on a starred message's bubble - see app.js's
   toggleStarMessage. Sits in the corner rather than inline with the text
   so it doesn't reflow/shift the message itself when toggled. */
.msg.starred .bubble { position: relative; }
.msg.starred .bubble::after {
    content: '★';
    position: absolute;
    top: -6px;
    right: -6px;
    font-size: 12px;
    color: #f5b301;
    background: var(--panel);
    border-radius: 50%;
    width: 16px;
    height: 16px;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
    animation: pop-in .2s cubic-bezier(.34, 1.56, .64, 1);
}

/* Pinned-message banner, shown above the message list (see #pinnedBanner
   in app.php / updatePinnedBanner in app.js) whenever the open chat has one. */
.pinned-banner {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 8px 20px;
    border: none;
    border-bottom: 1px solid var(--border);
    background: var(--panel-alt);
    color: var(--text);
    cursor: pointer;
    text-align: left;
    animation: fade-in .15s ease;
}
.pinned-banner-icon { font-size: 15px; flex-shrink: 0; }
.pinned-banner-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.pinned-banner-label { font-size: 11px; font-weight: 700; color: var(--accent-to); }
.pinned-banner-preview {
    font-size: 13px;
    color: var(--muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.pinned-banner-unpin {
    flex-shrink: 0;
    width: 22px;
    height: 22px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    color: var(--muted);
    font-size: 16px;
}
.pinned-banner-unpin:hover { background: var(--danger-soft); color: var(--danger); }

/* "Info" modal (who's read this message) - reuses .members-list's row
   look, just with a read/not-read status instead of admin controls. */
.message-info-text { color: var(--muted); font-size: 13px; margin: 0 0 12px; }
.message-info-status { font-size: 12px; color: var(--muted); }
.message-info-status.read { color: var(--accent-from); font-weight: 600; }

/* Emoji picker for the message input - a bigger scrollable panel, floating
   just above the input row rather than pinned to one button like the
   reaction picker above (there's no single message it belongs to). */
.emoji-picker {
    position: fixed;
    z-index: 30;
    left: 12px;
    right: 12px;
    /* The input got its own row above the tools row (see .message-form),
       so this needs to clear both rows now, not just one. */
    bottom: calc(126px + env(safe-area-inset-bottom));
    max-width: 420px;
    margin: 0 auto;
    max-height: 280px;
    overflow-y: auto;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 14px;
    padding: 10px;
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
}

.emoji-group-label {
    font-size: 11px;
    font-weight: 600;
    color: var(--muted);
    text-transform: uppercase;
    letter-spacing: 0.03em;
    margin: 10px 2px 4px;
}
.emoji-group-label:first-child { margin-top: 2px; }

.emoji-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(32px, 1fr));
}

.emoji-grid button {
    border: none;
    background: transparent;
    font-size: 20px;
    line-height: 1;
    padding: 5px;
    border-radius: 8px;
    cursor: pointer;
}
.emoji-grid button:hover { background: var(--panel-alt); }

/* ---------- Link previews ---------- */
.bubble-link { padding: 0 !important; overflow: hidden; }

.link-preview-fallback {
    display: block;
    padding: 8px 12px;
    color: inherit;
    text-decoration: underline;
    word-break: break-all;
}

.link-preview-card {
    display: block;
    color: inherit;
    text-decoration: none;
    width: 260px;
    max-width: 60vw;
}

.link-preview-card img {
    display: block;
    width: 100%;
    height: 130px;
    object-fit: cover;
}

.link-preview-text { padding: 8px 10px; text-align: left; }
.link-preview-title { font-size: 13px; font-weight: 600; line-height: 1.3; }
.link-preview-desc {
    font-size: 12px;
    color: var(--muted);
    margin-top: 2px;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.link-preview-url {
    font-size: 11px;
    color: var(--muted);
    margin-top: 4px;
    text-transform: uppercase;
    letter-spacing: 0.02em;
}
.reaction-picker button:hover { background: var(--panel-alt); }

/* GIF/image messages: no text padding, just the picture in a rounded frame. */
.bubble-gif {
    padding: 4px !important;
    background: var(--bubble-them) !important;
    line-height: 0;
}

.bubble-gif img {
    display: block;
    max-width: min(240px, 60vw);
    max-height: 240px;
    width: auto;
    height: auto;
    border-radius: 10px;
}

/* A shared short (see api/share_short.php) - same idea as .bubble-gif
   above, just a playable <video> instead of a static image. */
.bubble-shared-video {
    padding: 4px !important;
    background: var(--bubble-them) !important;
    line-height: 0;
}
.bubble-shared-video video {
    display: block;
    max-width: min(240px, 60vw);
    max-height: 320px;
    border-radius: 10px;
}

/* Stickers (transparent) - no bubble at all, just the sticker floating with
   a soft shadow, exactly like WhatsApp/Telegram stickers. */
.bubble-sticker {
    padding: 0 !important;
    background: transparent !important;
    position: relative;
}

.bubble-sticker img {
    display: block;
    width: min(140px, 40vw);
    height: min(140px, 40vw);
    object-fit: contain;
    filter: drop-shadow(0 3px 6px rgba(0, 0, 0, 0.3));
}

.sticker-favorite {
    position: absolute;
    top: -4px;
    right: -4px;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    border: 1px solid var(--border);
    background: var(--panel);
    color: var(--muted);
    font-size: 14px;
    line-height: 24px;
    text-align: center;
    padding: 0;
    cursor: pointer;
    opacity: 0;
    transition: opacity 0.15s ease;
}

.msg.them:hover .sticker-favorite { opacity: 1; }

@media (hover: none) {
    .sticker-favorite { opacity: 0.85; }
}

.sticker-favorite:hover { color: #f5b301; border-color: #f5b301; }
.sticker-favorite.favorited { color: #f5b301; border-color: #f5b301; opacity: 1; cursor: default; }

.typing-indicator {
    margin: 0;
    padding: 2px 20px;
    font-size: 12px;
    font-style: italic;
    color: var(--muted);
}

.reply-bar {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 20px;
    border-top: 1px solid var(--border);
    background: var(--panel-alt);
}

.reply-bar-text { flex: 1; min-width: 0; border-left: 3px solid var(--accent); padding-left: 8px; }
.reply-bar-sender { font-size: 12px; font-weight: 600; color: var(--accent-to); }

/* Same bar, different accent color - just enough visual distinction to
   tell "replying to" and "editing" apart at a glance. */
.edit-bar .reply-bar-text { border-left-color: #f5b301; }
.edit-bar .reply-bar-sender { color: #f5b301; }
.reply-bar-preview {
    font-size: 13px;
    color: var(--muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

#replyBarCancelBtn {
    border: none;
    background: transparent;
    color: var(--muted);
    font-size: 20px;
    line-height: 1;
    cursor: pointer;
    padding: 2px 6px;
}
#replyBarCancelBtn:hover { color: var(--danger); }

/* Smart Reply chips - a horizontally-scrollable row of tap-to-send
   suggestions sitting right above the input row (see #smartReplyBar in
   app.php / api/smart_reply.php). Pill-shaped and accent-tinted so they
   read as *suggestions*, not as something already sent. */
.smart-reply-bar {
    display: flex;
    gap: 8px;
    padding: 8px 20px 0;
    overflow-x: auto;
    scrollbar-width: none;
}
.smart-reply-bar::-webkit-scrollbar { display: none; }

/* Replaces the whole message form when either side has blocked the other
   (see updateBlockedNotice in app.js). */
.blocked-notice {
    margin: 8px 12px;
    padding: 10px 14px;
    border-radius: 10px;
    background: var(--danger-soft);
    color: var(--danger);
    font-size: 13px;
    text-align: center;
}
.blocked-notice button {
    margin-left: 6px;
    border: none;
    background: none;
    color: var(--danger);
    text-decoration: underline;
    cursor: pointer;
    font: inherit;
    padding: 0;
}
#blockChatBtn.blocked { color: var(--danger); }

.smart-reply-chip {
    flex: 0 0 auto;
    padding: 7px 14px;
    border-radius: 999px;
    border: 1px solid var(--accent-soft);
    background: var(--accent-soft);
    color: var(--accent);
    font-size: 13px;
    font-weight: 600;
    white-space: nowrap;
    cursor: pointer;
    animation: fade-in .15s ease;
}
.smart-reply-chip:hover { background: var(--accent); color: #fff; }
.smart-reply-chip:active { transform: scale(.96); }

/* One row - attach (+), the pill-shaped text field (emoji tucked inside
   it), photo, then mic/send sharing one spot (app.js's updateSendControls
   toggles which is visible depending on whether there's text typed) -
   WhatsApp-style, replacing the old "big text field with a Send button on
   top, everything else in a row underneath" layout. */
.message-form {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
    border-top: 1px solid var(--border);
    background: var(--panel);
}

.message-input-row {
    display: flex;
    align-items: center;
    gap: 6px;
}

.message-tools-row {
    display: flex;
    align-items: center;
    gap: 8px;
    /* Both children (.voice-timer/.voice-cancel) are .hidden outside of an
       active recording - collapses this row's height to just its own
       padding/gap rather than a visible empty strip most of the time. */
    min-height: 0;
}

.message-input-pill {
    flex: 1;
    min-width: 0;
    min-height: 44px;
    display: flex;
    align-items: center;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: 22px;
    padding: 4px 6px 4px 18px;
}

.message-form input {
    flex: 1;
    min-width: 0;
    border: none;
    background: transparent;
    color: var(--text);
    font-size: 16px;
    padding: 11px 4px;
}

.message-form input:focus { outline: none; }
.message-form input::placeholder { color: var(--muted); }

/* The emoji button living *inside* the input pill - smaller/plainer than
   the round buttons outside it, closer to how a native app draws an
   inline accessory icon. */
.input-inline-btn {
    flex-shrink: 0;
    width: 34px;
    height: 34px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: none;
    background: transparent;
    color: var(--muted);
    border-radius: 50%;
    cursor: pointer;
    padding: 0;
}
.input-inline-btn:hover { color: var(--text); background: var(--border); }
.input-inline-btn .icon { width: 19px; height: 19px; }

/* The attach (+)/photo/mic/send buttons around the pill. Two selectors on
   everything here (.round-icon-btn *and* .message-form .round-icon-btn) -
   the plain ".message-form button" element rule below would otherwise win
   on specificity (class+element beats a lone class) and force these back
   to a solid gradient/white-text look. */
.round-icon-btn, .message-form .round-icon-btn {
    flex-shrink: 0;
    width: 42px;
    height: 42px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: none;
    border-radius: 50%;
    background: var(--panel-alt);
    color: var(--text);
    cursor: pointer;
    padding: 0;
}
.round-icon-btn:hover, .message-form .round-icon-btn:hover { background: var(--border); }
.round-icon-btn .icon, .message-form .round-icon-btn .icon { width: 20px; height: 20px; }

/* The send button (and any other "primary" round icon button) - the one
   spot that keeps the app's signature gradient. */
.round-icon-btn.accent, .message-form .round-icon-btn.accent {
    background: var(--gradient);
    color: #fff;
}
.round-icon-btn.accent:hover, .message-form .round-icon-btn.accent:hover {
    filter: brightness(1.08);
    background: var(--gradient);
}

/* ---------- Voice messages ---------- */
#voicePickerBtn.recording {
    background: var(--danger) !important;
    border-color: var(--danger);
    color: #fff;
    animation: voice-pulse 1.4s ease-in-out infinite;
}

@keyframes voice-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.6; }
}

.voice-timer {
    align-self: center;
    font-size: 12px;
    font-variant-numeric: tabular-nums;
    color: var(--muted);
    min-width: 34px;
}

.voice-cancel {
    /* Self-sufficient now (padding/border-radius/cursor included here
       directly) - these used to come for free from a generic
       ".message-form button" rule that got removed when the rest of the
       input row switched to round icon buttons instead. */
    padding: 6px 12px;
    border-radius: 10px;
    cursor: pointer;
    background: transparent !important;
    border: 1px solid var(--border);
    color: var(--muted) !important;
    font-size: 12px;
}

.voice-cancel:hover { border-color: var(--danger); color: var(--danger); }

/* Custom voice-message player (see setupVoicePlayer in app.js) - replaces
   the native <audio controls> bar, which looks wildly different across
   browsers/OSes and never matches the app's own look. The <audio> element
   itself stays in the DOM (for playback/seeking) but visually hidden. */
.bubble-voice {
    padding: 8px 10px !important;
    display: flex;
    align-items: center;
    gap: 8px;
    width: 220px;
    max-width: 60vw;
}
.bubble-voice audio { display: none; }

.voice-play-btn {
    flex-shrink: 0;
    width: 30px;
    height: 30px;
    border-radius: 50%;
    border: none;
    background: rgba(255, 255, 255, 0.18);
    color: inherit;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    padding: 0;
}
.msg.them .voice-play-btn { background: var(--accent-soft); color: var(--accent); }
.voice-play-btn svg { width: 15px; height: 15px; }
.voice-play-btn:hover { filter: brightness(1.1); }

.voice-progress {
    flex: 1;
    min-width: 0;
    height: 4px;
    border-radius: 2px;
    background: rgba(255, 255, 255, 0.25);
    cursor: pointer;
}
.msg.them .voice-progress { background: var(--border); }

.voice-progress-fill {
    height: 100%;
    width: 0%;
    border-radius: 2px;
    background: currentColor;
}

.voice-time {
    flex-shrink: 0;
    font-size: 11px;
    font-variant-numeric: tabular-nums;
    opacity: 0.85;
}

/* ---------- GIF picker ---------- */
/* ---------- GIF/sticker picker: a bottom sheet, not a centered dialog ----------
   Same #gifModal/.modal-box elements as every other modal (so it inherits the
   dark backdrop, escape/backdrop-click plumbing, etc.), but .gif-modal/
   .gif-modal-box below override the centering/box-dialog look with a
   from-the-bottom sheet - anchored to the bottom edge, rounded top corners
   only, sliding up rather than fading+scaling in. */
/* #gifModal (id), not .gif-modal/.gif-modal-box (plain classes), for every
   property that also has a same-specificity ".modal"/".modal-box" base rule
   (or mobile-media-query override) elsewhere in this file - those would
   otherwise win on source order alone (several of them are declared further
   down), silently discarding this whole box-shape override on some or all
   screen sizes. The id beats that outright, regardless of where either rule
   sits in the file. */
#gifModal { align-items: flex-end; padding: 0; }

#gifModal .gif-modal-box {
    max-width: 460px;
    max-height: min(72vh, 560px);
    border-radius: 20px 20px 0 0;
    padding: 8px 16px calc(14px + env(safe-area-inset-bottom));
    display: flex;
    flex-direction: column;
    animation: gif-sheet-up .22s cubic-bezier(.32, .72, 0, 1);
    /* .modal-box's own base rule sets overflow-y: auto - override it here so
       the box itself never scrolls as a whole (that would drag the header
       off-screen); only .gif-grid, the flexible child below, scrolls. */
    overflow-y: hidden;
}

@keyframes gif-sheet-up {
    from { transform: translateY(100%); }
    to { transform: translateY(0); }
}

.gif-sheet-handle {
    width: 36px;
    height: 4px;
    border-radius: 2px;
    background: var(--border);
    margin: 2px auto 10px;
    flex-shrink: 0;
}

.gif-sheet-header {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-shrink: 0;
}

.gif-sheet-icon-btn {
    flex-shrink: 0;
    width: 34px;
    height: 34px;
    border-radius: 50%;
    border: none;
    background: var(--panel-alt);
    color: var(--text);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    padding: 0;
    transition: background-color .15s ease;
}
.gif-sheet-icon-btn:hover { background: var(--border); }
.gif-sheet-icon-btn .icon { width: 17px; height: 17px; }

/* The GIF/Sticker/Mine switcher, styled as one connected pill (iOS
   segmented-control look) instead of three separate bordered boxes. */
.gif-type-pill {
    flex: 1;
    display: flex;
    background: var(--panel-alt);
    border-radius: 999px;
    padding: 3px;
    gap: 2px;
    min-width: 0;
}

.gif-tab {
    flex: 1;
    min-width: 0;
    padding: 7px 4px;
    border-radius: 999px;
    border: none;
    background: transparent;
    color: var(--muted);
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    transition: background-color .15s ease, color .15s ease;
}

.gif-tab:hover { color: var(--text); }

.gif-tab.active {
    background: var(--panel);
    color: var(--text);
    box-shadow: 0 1px 4px rgba(0, 0, 0, .25);
}

#gifSearchInput {
    width: 100%;
    padding: 9px 10px;
    border-radius: 8px;
    border: 1px solid var(--border);
    background: var(--panel-alt);
    color: var(--text);
    font-size: 14px;
    margin: 10px 0 0;
    flex-shrink: 0;
}

#gifSearchInput::placeholder { color: var(--muted); }

.gif-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    align-content: start;
    gap: 6px;
    margin-top: 12px;
    flex: 1;
    min-height: 120px;
    overflow-y: auto;
}

/* Brief "yep, that sent" bounce on the tile you just tapped - the picker no
   longer closes after sending (see app.js), so without this a rapid-fire
   tap gave zero feedback that anything happened. Reuses the same pulse the
   send button itself gets on a normal typed message. */
.gif-tile-sent { animation: send-pulse .35s ease; }

.gif-grid img {
    width: 100%;
    height: 96px;
    object-fit: cover;
    border-radius: 8px;
    cursor: pointer;
    display: block;
    background: var(--panel-alt);
    transition: transform 0.1s ease, outline-color 0.1s ease;
    outline: 2px solid transparent;
    outline-offset: -2px;
}

.gif-grid img:hover {
    transform: scale(1.04);
    outline-color: var(--accent);
}

/* Stickers are transparent - "cover" would crop them oddly, "contain" keeps
   the whole sticker visible within its thumbnail square. */
.gif-grid-stickers img {
    object-fit: contain;
    padding: 6px;
}

.gif-add-sticker-tile {
    width: 100%;
    height: 96px;
    border-radius: 8px;
    border: 1px dashed var(--accent);
    background: transparent;
    color: var(--accent);
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
}

.gif-add-sticker-tile:hover { background: var(--accent-soft); }

.my-sticker-tile {
    position: relative;
}

.my-sticker-tile img {
    width: 100%;
    height: 96px;
    object-fit: contain;
    padding: 6px;
    border-radius: 8px;
    cursor: pointer;
    background: var(--panel-alt);
}

.my-sticker-tile img:hover { outline: 2px solid var(--accent); outline-offset: -2px; }

.my-sticker-remove {
    position: absolute;
    top: 2px;
    right: 2px;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    border: none;
    background: var(--panel);
    color: var(--muted);
    font-size: 13px;
    line-height: 18px;
    text-align: center;
    padding: 0;
    cursor: pointer;
}

.my-sticker-remove:hover { background: var(--danger-soft); color: var(--danger); }

.gif-status {
    color: var(--muted);
    font-size: 13px;
    text-align: center;
    padding: 20px 0;
    margin: 0;
}

/* ---------- Map ---------- */
.map-hint {
    margin: 0;
    padding: 8px 20px;
    font-size: 12px;
    color: var(--muted);
    background: var(--panel-alt);
    border-bottom: 1px solid var(--border);
}

.leaflet-map {
    flex: 1;
    min-height: 0;
    background: var(--panel-alt);
}

#shareLocationBtn.sharing {
    background: var(--danger-soft) !important;
    border-color: rgba(248, 113, 113, 0.4) !important;
    color: var(--danger) !important;
}

/* ---------- Shorts (see app.js's refreshShorts/renderShortsFeed) ---------- */

.shorts-view {
    padding: 0;
    background: #000;
}
.shorts-header {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    z-index: 3;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: max(10px, env(safe-area-inset-top)) 12px 10px;
    color: #fff;
    background: linear-gradient(to bottom, rgba(0, 0, 0, 0.6), transparent);
}
.shorts-header .back-btn { color: #fff; }
.shorts-header span { flex: 1; font-weight: 600; }
#shortsUploadBtn {
    width: 34px;
    height: 34px;
    border-radius: 50%;
    border: none;
    background: rgba(255, 255, 255, 0.15);
    color: #fff;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
}

.shorts-feed {
    height: 100%;
    overflow-y: auto;
    scroll-snap-type: y mandatory;
    scrollbar-width: none;
}
.shorts-feed::-webkit-scrollbar { display: none; }

/* Pull-to-refresh (see app.js) - sits just above the feed, off-screen
   until dragged into view; JS moves it via inline transform as you pull,
   this just defines its resting look/position. */
.shorts-pull-indicator {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 50px;
    margin-top: -50px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    font-size: 20px;
    z-index: 1;
    transition: transform .2s ease;
}
.shorts-pull-indicator.spinning { animation: spin .8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* The big heart that bursts and fades on a double-tap like (see
   wireShortsItem's dblclick handler). */
.shorts-heart-burst {
    position: absolute;
    top: 50%;
    left: 50%;
    z-index: 2;
    font-size: 90px;
    transform: translate(-50%, -50%) scale(0);
    pointer-events: none;
}
.shorts-heart-burst-anim { animation: shorts-heart-burst .7s ease; }
@keyframes shorts-heart-burst {
    0% { transform: translate(-50%, -50%) scale(0); opacity: 1; }
    30% { transform: translate(-50%, -50%) scale(1.15); opacity: 1; }
    45% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
    100% { transform: translate(-50%, -50%) scale(1); opacity: 0; }
}

.shorts-item-views {
    margin-top: 4px;
    font-size: 12px;
    opacity: .85;
}

.shorts-item {
    position: relative;
    height: 100%;
    scroll-snap-align: start;
    scroll-snap-stop: always;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #000;
}
.shorts-item video {
    max-width: 100%;
    max-height: 100%;
    width: 100%;
    height: 100%;
    object-fit: contain;
}
.shorts-item-overlay {
    position: absolute;
    left: 12px;
    right: 70px;
    bottom: calc(20px + env(safe-area-inset-bottom));
    z-index: 2;
    color: #fff;
    text-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
}
.shorts-item-user {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 6px;
    font-weight: 600;
}
.shorts-follow-btn {
    padding: 4px 12px;
    border-radius: 999px;
    border: 1px solid #fff;
    background: rgba(0, 0, 0, 0.3);
    color: #fff;
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
}
.shorts-follow-btn.following { background: rgba(255, 255, 255, 0.9); color: #000; }
.shorts-item-caption { font-size: 14px; }

.shorts-side-actions {
    position: absolute;
    right: 10px;
    bottom: calc(30px + env(safe-area-inset-bottom));
    z-index: 2;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 18px;
}
.shorts-action-btn {
    background: none;
    border: none;
    color: #fff;
    text-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    font-size: 11px;
    cursor: pointer;
}
.shorts-action-btn .shorts-action-icon { font-size: 26px; line-height: 1; }
.shorts-action-btn.liked .shorts-action-icon { filter: none; }

/* Comments panel (see #shortCommentsModal in app.php / openShortComments
   in app.js) - reuses the GIF picker's own bottom-sheet look (.gif-modal/
   .gif-modal-box), just capped a bit shorter since it's a list + one input
   row, not a whole grid. */
.shorts-comments-box { max-height: 60vh; display: flex; flex-direction: column; }
.shorts-comments-list {
    list-style: none;
    margin: 0;
    padding: 0;
    overflow-y: auto;
    flex: 1;
}
.shorts-comment {
    display: flex;
    align-items: baseline;
    gap: 8px;
    padding: 8px 4px;
    border-bottom: 1px solid var(--border);
}
.shorts-comment-user { font-weight: 600; font-size: 13px; flex-shrink: 0; }
.shorts-comment-text { font-size: 13px; flex: 1; word-break: break-word; }
.shorts-comment-delete {
    background: none;
    border: none;
    color: var(--muted);
    font-size: 18px;
    cursor: pointer;
    padding: 0 4px;
    flex-shrink: 0;
}
.shorts-empty-comments { text-align: center; color: var(--muted); padding: 20px; }
.shorts-comment-form {
    display: flex;
    gap: 8px;
    padding-top: 10px;
    border-top: 1px solid var(--border);
}
.shorts-comment-form input { flex: 1; }

.shorts-empty {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--muted);
    text-align: center;
    padding: 0 20px;
}

/* ---------- Activity tab ---------- */

.icon-btn-wrap { position: relative; display: inline-flex; }
.activity-dot {
    position: absolute;
    top: 2px;
    right: 2px;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--danger);
}

.activity-list {
    list-style: none;
    margin: 0;
    padding: 8px 12px;
    overflow-y: auto;
    flex: 1;
}
.activity-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 4px;
    border-bottom: 1px solid var(--border);
    cursor: pointer;
}
.activity-item-text { flex: 1; min-width: 0; font-size: 13px; }
.activity-item-text strong { font-weight: 600; }
.activity-item-time { font-size: 11px; color: var(--muted); }
.activity-item-thumb {
    width: 40px;
    height: 40px;
    border-radius: 6px;
    object-fit: cover;
    flex-shrink: 0;
    background: #000;
}

/* ---------- Profile view (see app.js's openProfileView) ---------- */

.profile-body {
    flex: 1;
    overflow-y: auto;
    padding: 20px;
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 10px;
}
.profile-avatar-section {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
}
.profile-stats {
    display: flex;
    gap: 20px;
    color: var(--muted);
    font-size: 13px;
}
.profile-stats strong { color: var(--text); }
#profileFollowBtn {
    padding: 6px 20px;
    border-radius: 999px;
}
#profileFollowBtn.following { background: var(--panel-alt); color: var(--text); border: 1px solid var(--border); }
.profile-bio {
    max-width: 340px;
    font-size: 14px;
    color: var(--text);
    white-space: pre-wrap;
}
.profile-bio-edit {
    width: 100%;
    max-width: 340px;
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.profile-bio-edit textarea { resize: vertical; }
/* 16px+ stops iOS from auto-zooming in on focus - these two live outside
   .modal-box (the profile view is its own full page, not a modal), so the
   existing mobile media query's own allowlist further down never covered
   them. */
.profile-bio-edit input, .profile-bio-edit textarea { font-size: 16px; }

.profile-shorts-grid {
    width: 100%;
    margin-top: 10px;
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 3px;
}
.profile-shorts-grid video, .profile-shorts-grid img {
    width: 100%;
    aspect-ratio: 9 / 16;
    object-fit: cover;
    background: #000;
    cursor: pointer;
}

/* Shorts search (see openShortsSearch/runShortsSearch in app.js). */
.shorts-search-bar {
    position: absolute;
    top: 46px;
    left: 0;
    right: 0;
    z-index: 3;
    padding: 8px 12px;
    background: rgba(0, 0, 0, 0.85);
}
.shorts-search-bar input {
    width: 100%;
    padding: 8px 12px;
    border-radius: 999px;
    border: 1px solid var(--border);
    background: var(--panel);
    color: var(--text);
    font-size: 16px; /* 16px+ stops iOS from auto-zooming in on focus */
}
.shorts-search-results {
    position: absolute;
    top: 94px;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 2;
    background: var(--bg);
    overflow-y: auto;
    padding: 8px;
    margin-top: 0 !important;
}
/* A hashtag inside a Shorts caption (see highlightShortsHashtags in
   app.js) - a real button, styled to look like plain accent-colored text. */
.shorts-hashtag {
    background: none;
    border: none;
    padding: 0;
    color: #7dd3fc;
    font: inherit;
    font-weight: 600;
    cursor: pointer;
}

.map-status {
    color: var(--muted);
    font-size: 12px;
    text-align: center;
    margin: 0;
    padding: 8px 20px;
    background: var(--panel);
    border-top: 1px solid var(--border);
}

/* Custom avatar markers (see app.js buildAvatarIcon). */
.avatar-marker {
    width: 34px;
    height: 34px;
    border-radius: 50%;
    background: var(--gradient);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 14px;
    border: 2px solid #fff;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}

.avatar-marker.me { border-color: #22d3ee; }

.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
    background: var(--panel);
    color: var(--text);
}

.leaflet-popup-content { font-size: 13px; margin: 10px 12px; }
.leaflet-container a.leaflet-popup-close-button { color: var(--muted); }

/* ---------- Calls ---------- */
#callBtn {
    font-size: 16px !important;
    padding: 6px 12px !important;
}

.call-overlay {
    position: fixed;
    inset: 0;
    z-index: 200;
    background: rgba(5, 7, 20, 0.85);
    backdrop-filter: blur(6px);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
}

/* Full-screen photo/GIF viewer (see openPhotoViewer in app.js) - same
   overlay treatment as a call, just for an image instead of a call box. */
.photo-viewer {
    position: fixed;
    inset: 0;
    z-index: 210;
    background: rgba(0, 0, 0, 0.92);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
    animation: fade-in .15s ease;
}

.photo-viewer img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    border-radius: 4px;
}

.photo-viewer-close {
    position: absolute;
    top: calc(16px + env(safe-area-inset-top));
    right: calc(16px + env(safe-area-inset-right));
    width: 40px;
    height: 40px;
    border-radius: 50%;
    border: none;
    background: rgba(255, 255, 255, 0.12);
    color: #fff;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
}
.photo-viewer-close:hover { background: rgba(255, 255, 255, 0.22); }

/* A snap's chat-bubble placeholder (see isSnapUrl/openSnapViewer in app.js)
   - never shows the actual photo inline, just an inviting "tap to view"
   pill, distinct from a normal bubble so it reads as "this one's different". */
.bubble-snap {
    display: flex;
    align-items: center;
    gap: 6px;
    cursor: pointer;
    background: linear-gradient(135deg, #fbbf24, #f59e0b) !important;
    color: #1b1d2a !important;
    font-weight: 600;
}
.bubble-snap-viewed {
    cursor: default;
    background: var(--bubble-them) !important;
    color: var(--muted) !important;
    font-weight: 400;
}
.msg.me .bubble-snap-viewed { background: var(--bubble-them) !important; }
.snap-icon { font-size: 16px; }

/* The full-screen snap viewer itself - reuses .photo-viewer's fixed/
   centered/dark-backdrop layout (see the block above), .snap-viewer only
   adds the draining progress bar across the top. */
.snap-viewer-progress {
    position: absolute;
    top: calc(10px + env(safe-area-inset-top));
    left: 12px;
    right: 12px;
    height: 4px;
    border-radius: 2px;
    background: rgba(255, 255, 255, 0.25);
    overflow: hidden;
}
.snap-viewer-progress-fill {
    height: 100%;
    width: 0%;
    background: #fff;
}

/* "@666" easter egg (see triggerJumpscare in app.js) - pops in instantly
   (no fade-in, that would defeat the point) covering literally everything
   else, including any open modal, then app.js removes it again after ~1s. */
.jumpscare-overlay {
    position: fixed;
    inset: 0;
    z-index: 500;
    background: #000;
    display: flex;
    align-items: center;
    justify-content: center;
}
.jumpscare-overlay img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.photo-viewer-close .icon { width: 20px; height: 20px; }

/* Toast notifications (see showToast in app.js) - stack bottom-center,
   above everything else including open modals. */
.toast-container {
    position: fixed;
    left: 50%;
    bottom: calc(24px + env(safe-area-inset-bottom));
    transform: translateX(-50%);
    z-index: 400;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    pointer-events: none;
}

.toast {
    pointer-events: auto;
    max-width: min(80vw, 360px);
    padding: 10px 18px;
    border-radius: 999px;
    background: var(--panel);
    border: 1px solid var(--border);
    color: var(--text);
    font-size: 13px;
    font-weight: 600;
    text-align: center;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
    cursor: pointer;
    animation: toast-in .2s cubic-bezier(.34, 1.56, .64, 1);
}
.toast.toast-hide { animation: toast-out .2s ease forwards; }
.toast-error { border-color: var(--danger); color: var(--danger); }

@keyframes toast-in {
    from { opacity: 0; transform: translateY(10px) scale(.95); }
    to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes toast-out {
    from { opacity: 1; transform: translateY(0); }
    to { opacity: 0; transform: translateY(6px); }
}

.call-box {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    padding: 32px;
}

.call-avatar {
    width: 96px;
    height: 96px;
    border-radius: 50%;
    background: var(--gradient);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 36px;
    font-weight: 700;
    margin-bottom: 8px;
    box-shadow: 0 8px 30px rgba(139, 92, 246, 0.5);
}

.call-name {
    font-size: 20px;
    font-weight: 700;
    color: var(--text);
}

.call-status {
    font-size: 14px;
    color: var(--muted);
    margin-bottom: 24px;
}

.call-actions {
    display: flex;
    gap: 16px;
}

.call-btn {
    border: none;
    border-radius: 50%;
    width: 56px;
    height: 56px;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    color: #fff;
}

.call-btn-accept { background: #22c55e; }
.call-btn-decline, .call-btn-hangup { background: var(--danger); }
.call-btn-mute { background: var(--panel-alt); border: 1px solid var(--border); color: var(--text); }
.call-btn-mute.muted { background: var(--accent-soft); border-color: var(--accent); }
.call-btn-camera { background: var(--panel-alt); border: 1px solid var(--border); color: var(--text); }
.call-btn-camera.camera-off { background: var(--accent-soft); border-color: var(--accent); }

/* ---------- Video calls ---------- */
#remoteVideo {
    display: none;
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    background: #000;
}

#localVideo {
    display: none;
    position: absolute;
    top: calc(20px + env(safe-area-inset-top));
    right: 20px;
    width: 100px;
    height: 140px;
    object-fit: cover;
    border-radius: 12px;
    border: 2px solid rgba(255, 255, 255, 0.35);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
    transform: scaleX(-1); /* mirror, like a selfie camera */
    z-index: 3;
}

.call-overlay.video-call {
    padding: 0;
    align-items: stretch;
}

.call-overlay.video-call #remoteVideo,
.call-overlay.video-call #localVideo {
    display: block;
}

.call-overlay.video-call .call-avatar { display: none; }

.call-overlay.video-call .call-box {
    position: relative;
    z-index: 2;
    width: 100%;
    justify-content: flex-end;
    padding: 24px 24px calc(40px + env(safe-area-inset-bottom));
    background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent 45%);
    box-sizing: border-box;
}

/* ---------- Group calls ----------
   Entirely separate markup/ids from the 1-to-1 call overlay above
   (#groupCallOverlay vs #callOverlay) - reuses the same .call-box/
   .call-avatar/.call-actions/.call-btn look for visual consistency, just
   with a tile grid instead of one single remote avatar. */
.group-call-box { max-width: 480px; width: 100%; }

.group-call-tiles {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 18px;
    margin: 20px 0;
    max-height: 50vh;
    overflow-y: auto;
}

.group-call-tile {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    width: 84px;
    animation: pop-in .25s cubic-bezier(.34, 1.56, .64, 1);
}

.group-call-avatar {
    width: 64px;
    height: 64px;
    font-size: 24px;
    margin-bottom: 0;
    /* Same breathing pulse a ringing 1-to-1 call avatar gets - here it
       doubles as a lightweight "still connected" cue for each tile. */
    animation: ring-pulse 2.2s ease-out infinite;
}

.group-call-tile-name {
    font-size: 12px;
    color: var(--text);
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ---------- Modals ---------- */
.modal {
    position: fixed;
    inset: 0;
    background: rgba(5, 7, 20, 0.65);
    backdrop-filter: blur(3px);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 50;
    padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
}

.modal-box {
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 24px;
    width: 100%;
    max-width: 380px;
    max-height: calc(100vh - 48px);
    overflow-y: auto;
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);
    transform: scale(1);
    transition: transform .15s ease;
}
/* Mirrors fade-scale-in's own open scale (0.95) - shrinks back down as the
   backdrop (see .modal.hidden above) fades out, instead of just popping
   away at full size the instant .hidden gets added. */
.modal.hidden .modal-box { transform: scale(0.95); }

.modal-box h2 { margin: 0 0 16px; font-size: 18px; }

.modal-box label {
    display: flex;
    flex-direction: column;
    gap: 6px;
    font-size: 13px;
    color: var(--muted);
    margin-bottom: 12px;
}

.modal-box label.toggle-label {
    flex-direction: row;
    align-items: center;
    gap: 8px;
    font-size: 14px;
    color: var(--text);
    cursor: pointer;
}
.toggle-label input[type="checkbox"] {
    width: 17px;
    height: 17px;
    accent-color: var(--accent);
    cursor: pointer;
}

.modal-box input,
.modal-box textarea {
    padding: 9px 10px;
    border-radius: 8px;
    border: 1px solid var(--border);
    background: var(--panel-alt);
    color: var(--text);
    font-size: 14px;
}

.modal-box textarea {
    width: 100%;
    font-family: inherit;
    resize: vertical;
    margin-bottom: 8px;
}

.modal-box input::placeholder,
.modal-box textarea::placeholder { color: var(--muted); }

.chip-input { display: flex; gap: 6px; }
.chip-input input { flex: 1; min-width: 0; }
.chip-input button {
    padding: 8px 10px;
    border-radius: 8px;
    border: 1px solid var(--border);
    background: var(--panel-alt);
    color: var(--text);
    cursor: pointer;
}

.chip-input button:hover { border-color: var(--accent); }

.chips {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-bottom: 14px;
}

.chip {
    background: var(--bubble-them);
    border: 1px solid var(--border);
    border-radius: 20px;
    padding: 4px 10px;
    font-size: 13px;
    display: flex;
    align-items: center;
    gap: 6px;
}

.chip .remove { cursor: pointer; color: var(--muted); font-weight: 700; }
.chip .remove:hover { color: var(--danger); }

.members-list {
    list-style: none;
    margin: 0 0 14px;
    padding: 0;
    max-height: 220px;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.members-list li {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 8px 10px;
    border-radius: 8px;
    background: var(--panel-alt);
    font-size: 14px;
}

.admin-badge {
    font-size: 10px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--accent-to);
    border: 1px solid var(--border);
    border-radius: 20px;
    padding: 1px 7px;
    margin-left: 6px;
}

.member-actions { display: flex; gap: 6px; flex-shrink: 0; }

/* ---------- Message search ---------- */
.search-modal-box { max-width: 480px; }

.search-modal-box #searchInput {
    width: 100%;
    padding: 10px 12px;
    border-radius: 8px;
    border: 1px solid var(--border);
    background: var(--panel-alt);
    color: var(--text);
    font-size: 15px;
    margin-bottom: 10px;
}

.search-scope-toggle { margin-bottom: 10px; }

.search-status {
    color: var(--muted);
    font-size: 13px;
    text-align: center;
    padding: 16px 0;
    margin: 0;
}

.search-results {
    list-style: none;
    margin: 0 0 14px;
    padding: 0;
    max-height: 50vh;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.search-result {
    display: block;
    width: 100%;
    text-align: left;
    padding: 10px 12px;
    border-radius: 8px;
    border: 1px solid var(--border);
    background: var(--panel-alt);
    cursor: pointer;
    /* Each result pops in as it's drawn - same cascading feel as the
       reaction picker/GIF grid (see the "More motion, round two" block
       above), capped the same way for the same reason. */
    animation: pop-in .18s ease backwards;
}
.search-result:hover { border-color: var(--accent); background: var(--accent-soft); }
.search-results li:nth-child(1) .search-result { animation-delay: 0ms; }
.search-results li:nth-child(2) .search-result { animation-delay: 25ms; }
.search-results li:nth-child(3) .search-result { animation-delay: 50ms; }
.search-results li:nth-child(4) .search-result { animation-delay: 75ms; }
.search-results li:nth-child(5) .search-result { animation-delay: 100ms; }
.search-results li:nth-child(6) .search-result { animation-delay: 125ms; }

.search-result-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 4px;
}

.search-result-chat { font-size: 12px; font-weight: 700; color: var(--accent-to); }
.search-result-time { font-size: 11px; color: var(--muted); flex-shrink: 0; }

.search-result-text {
    font-size: 13px;
    color: var(--text);
    overflow: hidden;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}

.search-result-text mark {
    background: var(--accent-soft);
    color: var(--accent-to);
    border-radius: 3px;
    padding: 0 2px;
}

/* Briefly flashes the target bubble after jumping to it from a search
   result, so it's obvious *which* message among many you were taken to. */
@keyframes search-flash {
    0%, 100% { box-shadow: 0 0 0 0 transparent; }
    30% { box-shadow: 0 0 0 3px var(--accent); }
}
.msg.search-highlight .bubble { animation: search-flash 1.8s ease; }

.member-remove, .member-promote, .member-demote {
    padding: 4px 10px;
    border-radius: 7px;
    border: 1px solid var(--border);
    background: transparent;
    color: var(--muted);
    font-size: 12px;
    cursor: pointer;
    white-space: nowrap;
}

.member-remove:hover { border-color: var(--danger); color: var(--danger); }
.member-promote:hover, .member-demote:hover { border-color: var(--accent); color: var(--text); }

.forward-target {
    width: 100%;
    text-align: left;
    border: none;
    background: transparent;
    color: var(--text);
    font-size: 14px;
    cursor: pointer;
    padding: 0;
}
.forward-target:hover { color: var(--accent-to); }

.modal-actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    margin-top: 6px;
}

.modal-actions button {
    padding: 9px 14px;
    border-radius: 8px;
    border: none;
    background: var(--gradient);
    color: #fff;
    font-weight: 600;
    cursor: pointer;
}

.modal-actions button:hover { filter: brightness(1.08); }

.modal-actions button.secondary {
    background: transparent;
    border: 1px solid var(--border);
    color: var(--text);
}

.modal-actions button.secondary:hover { filter: none; border-color: var(--accent); }

/* A "secondary" (outline-style) button outside the Cancel/Save row - the
   avatar and password actions in Settings use this on their own. */
button.secondary {
    padding: 9px 14px;
    border-radius: 8px;
    border: 1px solid var(--border);
    background: transparent;
    color: var(--text);
    font-weight: 600;
    font-size: 13px;
    cursor: pointer;
}

button.secondary:hover { border-color: var(--accent); }

.full-width { width: 100%; }

.modal-divider {
    border: none;
    border-top: 1px solid var(--border);
    margin: 18px 0;
}

.password-fields {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-bottom: 10px;
}

.scheduled-messages-list {
    list-style: none;
    margin: 0;
    padding: 0;
    max-height: 140px;
    overflow-y: auto;
}
.scheduled-messages-list li {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 6px 0;
    font-size: 12px;
    border-bottom: 1px solid var(--border);
}
.scheduled-message-cancel {
    background: none;
    border: none;
    color: var(--danger);
    font-size: 16px;
    cursor: pointer;
    flex-shrink: 0;
}

/* ---------- Responsive (phones + tablets in portrait) ---------- */
@media (max-width: 860px) {
    /* One pane at a time: the chat list, or the open chat, never both.
       .app gets "chat-open" toggled by app.js whenever a chat is selected. */
    .app {
        position: relative;
        overflow: hidden;
    }

    .sidebar {
        width: 100%;
    }

    .chat-main {
        display: none;
    }

    .app.chat-open .sidebar {
        display: none;
    }

    .app.chat-open .chat-main {
        display: flex;
    }

    .back-btn {
        display: inline-flex;
    }

    /* Chat bubbles can use more of the narrower screen width, but stay
       comfortably clear of the edge - not almost as wide as the screen. */
    .msg {
        max-width: 78%;
    }

    /* "+ Add member" / "Delete chat" text is what used to force the whole
       chat pane wider than the viewport (flex-shrink:0 buttons + no
       min-width:0 upstream). Shrink them so the row actually fits. */
    .chat-view-header {
        gap: 8px;
        padding-left: 12px;
        padding-right: 12px;
    }

    .chat-view-header button {
        font-size: 11px;
        padding: 6px 8px;
    }

    /* 16px+ inputs stop iOS/iPadOS from auto-zooming in on focus.
       #gifSearchInput needs naming explicitly - an id selector's own base
       font-size would otherwise outrank ".modal-box input" here. */
    .auth-card input,
    .new-chat-box input,
    .message-form input,
    .modal-box input,
    .modal-box textarea,
    #gifSearchInput {
        font-size: 16px;
    }

    /* Comfortable tap targets. */
    .chat-list li {
        padding: 12px 10px;
    }

    .new-chat-box button,
    .message-form button,
    .chat-view-header button {
        min-height: 40px;
    }

    .theme-option {
        min-height: 40px;
    }

    .modal {
        padding: calc(16px + env(safe-area-inset-top)) calc(16px + env(safe-area-inset-right))
                 calc(16px + env(safe-area-inset-bottom)) calc(16px + env(safe-area-inset-left));
    }

    .modal-box {
        max-height: calc(100vh - 32px);
        overflow-y: auto;
    }
}

@media (max-width: 480px) {
    .auth-card {
        padding: 24px 20px;
    }

    .sidebar-header {
        padding: 12px;
    }

    .new-chat-box {
        padding: 10px 12px 0;
    }

    .messages {
        padding: 14px 12px;
    }

    .message-form {
        padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
    }

    .modal-box {
        padding: 20px 16px;
    }

    .theme-picker {
        gap: 4px;
    }

    .theme-option {
        font-size: 11px;
        padding: 8px 2px;
    }
}

/* ---------- "Add to Home Screen" banner (iOS/iPadOS only, see install-banner.js) ---------- */
.install-banner {
    position: fixed;
    left: 12px;
    right: 12px;
    bottom: calc(12px + env(safe-area-inset-bottom));
    z-index: 100;
    display: flex;
    align-items: center;
    gap: 10px;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 12px 14px;
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);
}

.install-banner-text {
    flex: 1;
    font-size: 13px;
    line-height: 1.4;
    color: var(--text);
}

.install-banner-text strong {
    background: var(--gradient);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}

.install-banner .share-icon {
    display: inline-block;
    width: 16px;
    height: 16px;
    line-height: 16px;
    text-align: center;
    border: 1.5px solid var(--text);
    border-radius: 4px;
    font-size: 11px;
}

.install-banner-close {
    flex-shrink: 0;
    background: transparent;
    border: none;
    color: var(--muted);
    font-size: 20px;
    line-height: 1;
    cursor: pointer;
    padding: 4px;
}

.install-banner-close:hover { color: var(--text); }

.install-banner-install {
    flex-shrink: 0;
    border: none;
    border-radius: 8px;
    padding: 8px 14px;
    background: var(--gradient);
    color: #fff;
    font-weight: 600;
    font-size: 13px;
    cursor: pointer;
}

.install-banner-install:hover { filter: brightness(1.08); }

.apk-instructions-overlay {
    position: fixed;
    inset: 0;
    z-index: 300;
    background: rgba(5, 7, 20, 0.75);
    backdrop-filter: blur(3px);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: env(safe-area-inset-top) 16px env(safe-area-inset-bottom);
}

.apk-instructions-box {
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 24px;
    max-width: 380px;
    width: 100%;
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);
}

.apk-instructions-box h2 { margin: 0 0 14px; font-size: 18px; color: var(--text); }

.apk-instructions-box ol {
    margin: 0 0 18px;
    padding-left: 20px;
    color: var(--text);
    font-size: 14px;
    line-height: 1.6;
}

.apk-instructions-box li { margin-bottom: 8px; }

.apk-instructions-box .install-banner-install {
    width: 100%;
    padding: 10px;
    font-size: 14px;
}
