Files
Haven/public/css/style-messages.css
T

1323 lines
45 KiB
CSS

/* Haven core styles, part 5 of 16: the message area, avatars and the avatar editor, sounds, the message box and the right sidebar. Loaded in order by style.css; the order matters. */
/* ── Message Area ──────────────────────────────────────── */
.message-area { flex: 1; display: flex; flex-direction: column; min-height: 0; position: relative; }
/* ── Jump to Bottom button ───────────────────────────── */
.jump-to-bottom {
position: absolute;
bottom: 120px;
right: 24px;
z-index: 50;
width: 2.25rem;
height: 2.25rem;
border-radius: 50%;
background: var(--bg-secondary);
border: 1px solid var(--border);
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
transition: background 0.15s ease, color 0.15s ease, opacity 0.2s ease, transform 0.2s ease;
opacity: 0;
transform: translateY(8px);
pointer-events: none;
}
.jump-to-bottom.visible {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.jump-to-bottom:hover {
background: var(--accent);
color: var(--accent-text);
border-color: var(--accent);
}
.messages {
flex: 1;
overflow-y: auto;
overflow-anchor: none;
padding: 1rem 1.25rem;
display: flex;
flex-direction: column;
gap: 0.125rem;
position: relative; /* for absolutely-positioned children like #msg-more-btn */
}
.message {
display: flex;
flex-direction: column;
border-radius: var(--radius-sm);
flex-shrink: 0;
}
.message:hover {
background: rgba(255,255,255,0.02);
transition: background var(--transition);
box-shadow: var(--msg-glow);
}
/* ── Message gutter ──────────────────────────────────────
A group leader indents its text by (row padding + avatar + gap). A compact
follow-up has no avatar, so it must pad by that exact same total or the
lines of one conversation don't share a left edge. Those numbers used to be
spelled out independently in five places (base, two breakpoints, two
density tiers), and the spacious tier never got its compact value at all,
so every follow-up sat 14px left of the message above it. One side is now
derived from the other: overrides set these variables and both move
together, so the two can't drift apart again. */
:root {
--msg-pad-x: 0.5rem; /* 8px: .message-row horizontal padding */
--msg-pad-y: 0.25rem; /* 4px: .message-row vertical padding */
--msg-avatar: 2.25rem; /* 36px: avatar column */
--msg-gap: 0.75rem; /* 12px: avatar → body gap */
--msg-gutter: calc(var(--msg-pad-x) + var(--msg-avatar) + var(--msg-gap));
}
.message-row {
display: flex;
gap: var(--msg-gap);
padding: var(--msg-pad-y) var(--msg-pad-x);
position: relative;
}
.message-avatar {
width: var(--msg-avatar);
height: var(--msg-avatar);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.875rem; /* 14px */
color: white;
flex-shrink: 0;
margin-top: 0.125rem;
}
.message-avatar-img {
width: var(--msg-avatar);
height: var(--msg-avatar);
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
margin-top: 0.125rem;
}
/* pfp border overlay: the saved fit rendered on top of a real avatar. The host
is a positioning context sized to the avatar; the marker (.pfp-border) is
folded into .bce-op wrappers by _foldPfpBorder. Reuses .bce-op. */
/* align-self stops the flex row from stretching the host to full row height
(tallest on emoji-only jumbo rows); block avatar drops the inline descender,
so the host is exactly the avatar box and the overlay stays centered on it. */
.pfp-host { position: relative; display: inline-block; flex-shrink: 0; align-self: flex-start; margin-top: 0.125rem; }
.pfp-host .message-avatar, .pfp-host .message-avatar-img { margin-top: 0; }
.pfp-host .message-avatar-img { display: block; }
/* No clip net: _computeMatrix3d refuses any distort whose perspective could fling
the border past the frame, so nothing here needs cropping. */
.pfp-border { position: absolute; inset: 0; pointer-events: none; }
.pfp-border-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
/* User list avatars */
/* Discord-style avatar wrapper with overlaid status dot */
.user-avatar-wrapper {
position: relative;
flex-shrink: 0;
width: 1.5rem;
height: 1.5rem;
}
.user-status-dot {
position: absolute;
bottom: -2px;
right: -2px;
width: 0.625rem;
height: 0.625rem;
border-radius: 50%;
background: var(--success);
border: 2px solid var(--bg-secondary);
box-sizing: border-box;
}
/* Away: half-circle (dome), visually distinct from full circle */
/* Official message (posted as a role): the role's colour down the side, a
light wash of it behind, and the role's name as a badge. */
.message.message-highlighted,
.message-compact.message-highlighted {
box-shadow: inset 3px 0 0 var(--hl-color, var(--accent));
background: color-mix(in srgb, var(--hl-color, var(--accent)) 9%, transparent);
}
.msg-highlight-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0 0.375rem;
border-radius: 0.25rem;
font-size: 0.6875rem;
font-weight: 700;
line-height: 1.4;
color: var(--hl-color, var(--accent));
border: 1px solid color-mix(in srgb, var(--hl-color, var(--accent)) 55%, transparent);
background: color-mix(in srgb, var(--hl-color, var(--accent)) 14%, transparent);
white-space: nowrap;
}
.btn-highlight.active { color: var(--accent); background: color-mix(in srgb, var(--accent) 16%, transparent); }
/* The reader's choice of look (app-message-highlights.js). Coloured text:
the text and a lighter wash in the role's colour, a thinner bar, no
badge, and a run of official messages from one person joined into one
block. The text colour is mixed with the theme's own, more of it on light
themes (--hl-text-mix), so a pale role colour stays readable. The text
colour is !important because many themes set message text that way. Off:
no mark at all. */
:root[data-official-style] .msg-highlight-badge { display: none; }
:root[data-official-style="off"] .message.message-highlighted,
:root[data-official-style="off"] .message-compact.message-highlighted {
box-shadow: none;
background: none;
}
:root[data-official-style="text"] .message.message-highlighted,
:root[data-official-style="text"] .message-compact.message-highlighted {
box-shadow: inset 2px 0 0 color-mix(in srgb, var(--hl-color, var(--accent)) 75%, transparent);
background: color-mix(in srgb, var(--hl-color, var(--accent)) 7%, transparent);
}
:root[data-official-style="text"] .message-highlighted .message-content {
color: color-mix(in srgb, var(--hl-color, var(--accent)) var(--hl-text-mix, 60%), var(--text-primary)) !important;
}
:root[data-official-style="text"] .message-highlighted:has(+ .message-compact.message-highlighted) {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
:root[data-official-style="text"] .message-highlighted + .message-compact.message-highlighted {
margin-top: -0.125rem;
padding-top: 0.1875rem;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.user-status-dot.away {
background: #faa61a;
border-radius: 0.125rem 0.125rem 50% 50%;
}
/* DND: square, immediately distinguishable */
.user-status-dot.dnd {
background: var(--danger);
border-radius: 0.125rem;
}
/* Invisible / Offline: hollow circle outline */
.user-status-dot.invisible {
background: transparent;
border: 2px solid var(--text-muted);
}
.user-item.offline .user-status-dot {
background: transparent;
border: 2px solid var(--text-muted);
}
.user-item-avatar {
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.625rem;
color: white;
flex-shrink: 0;
}
.user-item-avatar-img {
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
}
/* Avatar settings */
.avatar-settings { padding: 0.25rem 0; position: relative; z-index: 1; }
.settings-section-subtitle { font-size: 0.8125rem; font-weight: 600; color: var(--text-secondary); margin-bottom: 0.625rem; margin-top: 1rem; padding-bottom: 0.375rem; border-bottom: 1px solid var(--border); }
/* Avatar Upload Row (Anmire-style) */
.avatar-upload-row { display: flex; align-items: center; gap: 0.5rem; }
.avatar-upload-preview {
width: 3rem;
height: 3rem;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
border: 2px solid var(--border);
background: var(--bg-tertiary);
}
.avatar-upload-preview img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
}
/* Border edit modal: shaped avatar as the base layer, border image on top.
Both layers fill the same square so the shape and frame stay aligned. The
tool group (modes + stage) sits left, the edit history list sits right. */
/* Wider than a default modal so the stage and history list sit comfortably. */
#border-crop-modal .modal { width: min(92%, 36rem); }
.border-crop-body { display: flex; gap: 0.75rem; align-items: stretch; }
.border-crop-main { flex: 1; min-width: 0; }
.border-crop-stage { display: flex; align-items: center; justify-content: center; padding: 1.5rem 0; }
.border-crop-preview { position: relative; width: 14.4rem; height: 14.4rem; }
/* Purely visual: all interaction goes through .border-crop-box. Without this, a
scaled/moved op wrapper's hit area overflows the stage and swallows clicks on
the tool tabs above it. */
.border-crop-layers { position: absolute; inset: 0; pointer-events: none; }
.border-crop-avatar {
position: absolute; inset: 0; width: 100%; height: 100%;
object-fit: cover;
display: flex; align-items: center; justify-content: center;
font-weight: 700; font-size: 3rem; color: #fff;
}
.border-crop-border {
position: absolute; inset: 0; width: 100%; height: 100%;
object-fit: contain; pointer-events: none;
}
/* One folded op = one nested wrapper. The transform/clip is set inline in JS;
every wrapper fills its parent so ops compose cleanly from image outward. */
.bce-op { position: absolute; inset: 0; }
/* Avatar mode: the unedited image, faded under the crop so the cut area shows. */
.bce-ghost { opacity: 0.3; }
/* Edit history (the op log), newest at the bottom, each row undoes itself + later. */
.border-crop-side { width: 8.5rem; display: flex; flex-direction: column; }
.border-crop-side-title { margin: 0 0 0.4rem; font-size: 0.8125rem; color: var(--text-secondary); }
.border-crop-history { flex: 1; overflow-y: auto; max-height: 15rem; display: flex; flex-direction: column; gap: 0.25rem; }
.bce-hist-empty { color: var(--text-muted); font-size: 0.8125rem; padding: 0.35rem 0; }
.bce-hist-row {
display: flex; align-items: center; justify-content: space-between; gap: 0.4rem;
padding: 0.3rem 0.45rem; border: 1px solid var(--border); border-radius: 0.4rem;
background: var(--bg-tertiary, #2a2a2e); font-size: 0.8125rem;
}
.bce-hist-label { color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bce-hist-undo {
flex: none; border: none; background: none; cursor: pointer; padding: 0 0.15rem;
color: var(--text-muted); font-size: 1rem; line-height: 1;
}
.bce-hist-undo:hover { color: var(--accent, #7aa2ff); }
/* Segmented mode selector: pick one tool. */
.border-crop-modes { display: flex; border: 1px solid var(--border); border-radius: 0.5rem; overflow: hidden; margin-bottom: 0.75rem; }
.border-crop-mode-btn {
flex: 1; padding: 0.4rem 0.5rem; border: none; border-right: 1px solid var(--border);
background: var(--bg-tertiary, #2a2a2e); color: var(--text-secondary); cursor: pointer;
font-size: 0.8125rem; font-weight: 600;
}
.border-crop-mode-btn:last-child { border-right: none; }
.border-crop-mode-btn.active { background: var(--accent, #7aa2ff); color: var(--accent-text); }
/* Shown only for the Crop tool (toggled in _renderBorderEditor). Auto-positions
the crop handles around the border's opaque pixels; user edits/keeps from there. */
.border-crop-autocrop-btn {
display: none; width: 100%; padding: 0.4rem 0.5rem; margin-bottom: 0.75rem;
border: 1px solid var(--border); border-radius: 0.5rem;
background: var(--bg-tertiary, #2a2a2e); color: var(--text-secondary); cursor: pointer;
font-size: 0.8125rem; font-weight: 600;
}
.border-crop-autocrop-btn:hover { color: var(--text-primary); border-color: var(--accent, #7aa2ff); }
.border-crop-autocrop-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.border-crop-autocrop-btn:disabled:hover { color: var(--text-secondary); border-color: var(--border); }
/* Lock sides toggle (avatar editor only); lit while on. */
.border-crop-lock-btn.active { background: var(--accent, #7aa2ff); color: var(--accent-text); border-color: var(--accent, #7aa2ff); }
/* Shown only for the Rotate tool (toggled in _renderBorderEditor). Quarter-turn
nudges that wrap through the same 0..360 logic as the manual rotate handle. */
.border-crop-rotate-actions { display: none; gap: 0.5rem; margin-bottom: 0.75rem; }
.border-crop-rotate-actions .btn-sm { flex: 1; text-align: center; }
/* Shown only for the Opacity tool (toggled in _renderBorderEditor). */
.border-crop-resize-actions { display: none; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem; }
.border-crop-resize-actions #border-resize-anchor { flex: 1; }
.border-resize-anchor-label { font-size: 0.8125rem; color: var(--text-secondary); white-space: nowrap; }
.border-crop-opacity-actions { display: none; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem; }
.border-crop-opacity-actions #border-opacity-slider { flex: 1; }
.border-opacity-value { min-width: 3ch; text-align: right; font-size: 0.8125rem; font-weight: 600; color: var(--text-secondary); }
/* Drag handles overlaid on the border. All are positioned in JS (so they follow
the border) and hidden by default; only the active mode's handles show. */
.border-crop-box { position: absolute; inset: 0; touch-action: none; }
/* Move is the only tool that drags the image body, so it owns the move cursor. */
.border-crop-preview.mode-move .border-crop-box { cursor: move; }
.border-crop-handle {
position: absolute; width: 0.875rem; height: 0.875rem;
border-radius: 50%; background: var(--accent, #7aa2ff); border: 2px solid #fff;
box-sizing: border-box; touch-action: none; transform: translate(-50%, -50%);
display: none; align-items: center; justify-content: center;
}
.crop-edge { cursor: crosshair; }
.corner-handle { background: var(--bg-secondary, #26262b); cursor: grab; }
.corner-handle:active { cursor: grabbing; }
.scale-handle, .rotate-handle { width: 1.25rem; height: 1.25rem; font-size: 0.8rem; color: #fff; line-height: 1; }
.scale-handle { cursor: nwse-resize; }
.rotate-handle { background: var(--bg-secondary, #26262b); cursor: grab; }
.rotate-handle:active { cursor: grabbing; }
.border-crop-preview.mode-crop .crop-edge,
.border-crop-preview.mode-distort .corner-handle,
.border-crop-preview.mode-resize .scale-handle,
.border-crop-preview.mode-rotate .rotate-handle { display: flex; }
/* Avatar Shape Picker */
.avatar-shape-picker { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.avatar-shape-btn {
width: 2.375rem; height: 2.375rem;
display: flex; align-items: center; justify-content: center;
background: var(--bg-tertiary, #2a2a2e); border: 2px solid var(--border);
border-radius: 0.375rem; cursor: pointer;
transition: border-color 0.2s, box-shadow 0.2s;
}
.avatar-shape-btn:hover { border-color: var(--text-secondary); }
.avatar-shape-btn.active { border-color: var(--accent); box-shadow: 0 0 8px rgba(var(--accent-rgb, 88,101,242), 0.4); }
.shape-preview { width: 1.25rem; height: 1.25rem; background: var(--accent, #5865f2); }
.shape-circle { border-radius: 50%; }
.shape-rounded { border-radius: 0.25rem; }
.shape-squircle { border-radius: 28%; }
.shape-hex { clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%); }
.shape-diamond { clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%); }
/* Avatar Shape Classes: applied to .message-avatar and .message-avatar-img */
.avatar-circle { border-radius: 50% !important; }
.avatar-rounded { border-radius: 0.375rem !important; }
.avatar-squircle { border-radius: 28% !important; }
.avatar-hex { clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%); border-radius: 0 !important; }
.avatar-diamond { clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%); border-radius: 0 !important; }
/* Size slider */
#avatar-size-slider {
width: 100%;
-webkit-appearance: none;
appearance: none;
height: 0.375rem;
border-radius: 0.1875rem;
background: var(--bg-tertiary, #2a2a2e);
outline: none;
}
#avatar-size-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 1rem; height: 1rem;
border-radius: 50%;
background: var(--accent, #5865f2);
cursor: pointer;
border: 2px solid var(--bg-primary, #1e1e22);
}
#avatar-size-slider::-moz-range-thumb {
width: 1rem; height: 1rem;
border-radius: 50%;
background: var(--accent, #5865f2);
cursor: pointer;
border: 2px solid var(--bg-primary, #1e1e22);
}
/* Custom sounds admin */
.custom-sound-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0;
border-bottom: 1px solid var(--border);
}
.custom-sound-item:last-child { border-bottom: none; }
.custom-sound-name { flex: 1; font-size: 0.8125rem; color: var(--text-primary); }
.custom-sound-name-input {
flex: 1; font-size: 0.75rem; padding: 0.125rem 0.375rem; border-radius: 0.25rem;
border: 1px solid var(--accent); background: var(--bg-input);
color: var(--text-primary); outline: none;
}
.btn-xs {
padding: 0.125rem 0.5rem;
font-size: 0.6875rem;
border-radius: 0.25rem;
border: 1px solid var(--border);
background: var(--bg-secondary);
color: var(--text-primary);
cursor: pointer;
transition: background 0.15s;
}
.btn-xs:hover { background: var(--bg-tertiary); }
/* ── Sound Manager Popout ─────────────────────────────────── */
.sound-modal-tabs {
display: flex; gap: 0.25rem; margin-bottom: 0.75rem;
border-bottom: 1px solid var(--border); padding-bottom: 0.5rem;
}
.sound-tab {
padding: 0.3125rem 0.75rem; font-size: 0.75rem; border-radius: var(--radius-sm) var(--radius-sm) 0 0;
border: 1px solid var(--border); border-bottom: none; background: var(--bg-tertiary);
color: var(--text-secondary); cursor: pointer; transition: background 0.15s, color 0.15s;
}
.sound-tab:hover { background: var(--bg-hover); color: var(--text-primary); }
.sound-tab.active { background: var(--accent); color: var(--accent-text); border-color: var(--accent); }
.sound-tab-content { display: none; }
.sound-tab-content.active { display: block; }
/* Soundboard grid */
.soundboard-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 0.375rem; max-height: 17.5rem; overflow-y: auto; padding: 0.25rem 0;
}
.soundboard-btn {
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 0.25rem; padding: 0.625rem 0.375rem; border-radius: var(--radius);
border: 1px solid var(--border); background: var(--bg-secondary);
color: var(--text-primary); cursor: pointer; min-height: 3.75rem;
transition: background 0.15s, border-color 0.15s, transform 0.1s;
}
.soundboard-btn:hover { background: var(--bg-hover); border-color: var(--accent); }
.soundboard-btn:active { transform: scale(0.96); }
.soundboard-btn .sb-name { font-size: 0.75rem; font-weight: 600; text-align: center; word-break: break-word; }
.soundboard-btn .sb-hotkey {
font-size: 0.5625rem; color: var(--text-muted); background: var(--bg-tertiary);
padding: 0.0625rem 0.375rem; border-radius: 0.1875rem; font-family: var(--font-mono);
}
.sb-hotkey-row {
display: inline-flex; align-items: center; gap: 0.25rem;
}
.sb-hotkey-clear {
font-size: 0.8125rem; line-height: 1; color: var(--text-muted); cursor: pointer;
padding: 0 0.125rem; border-radius: 0.1875rem; transition: color 0.15s, background 0.15s;
}
.sb-hotkey-clear:hover { color: var(--danger); background: rgba(240,71,71,0.15); }
/* Row layouts (list mode, sidebar) get a finger-sized clear target. */
.soundboard-grid.list-mode .sb-hotkey-clear,
#sb-sidebar-grid .sb-hotkey-clear { font-size: 1rem; padding: 0 0.375rem; }
#sb-sidebar-grid .soundboard-btn { justify-content: space-between; }
#sb-sidebar-grid .sb-hotkey-row, #sb-sidebar-grid .sb-hotkey-set { flex-shrink: 0; margin-left: 0.5rem; }
.sb-hotkey-set {
font-size: 0.5625rem; color: var(--accent); cursor: pointer; opacity: 0.7;
transition: opacity 0.15s;
}
.sb-hotkey-set:hover { opacity: 1; text-decoration: underline; }
.sound-search-row { display: flex; gap: 0.5rem; margin-bottom: 0.5rem; }
.sound-assign-list { display: flex; flex-direction: column; gap: 0.25rem; }
.sound-assign-select { min-width: 8.75rem; }
/* Sidebar layout mode for soundboard (used by the sidebar panel grid only) */
.soundboard-grid.sidebar-mode {
display: flex !important;
flex-direction: column;
gap: 0.25rem;
max-height: 18.75rem;
grid-template-columns: auto;
}
.soundboard-grid.sidebar-mode .soundboard-btn {
min-height: 2rem;
justify-content: space-between;
flex-direction: row;
padding: 0.375rem 0.5rem;
}
.soundboard-grid.sidebar-mode .soundboard-btn .sb-name {
text-align: left;
flex: 1;
font-size: 0.8125rem;
}
/* List view mode for popup/pip soundboard (separate from sidebar-mode) */
.soundboard-grid.list-mode {
display: flex !important;
flex-direction: column;
gap: 0.25rem;
grid-template-columns: auto;
}
.soundboard-grid.list-mode .soundboard-btn {
min-height: 2rem;
justify-content: space-between;
flex-direction: row;
padding: 0.375rem 0.5rem;
}
.soundboard-grid.list-mode .soundboard-btn .sb-name {
text-align: left;
flex: 1;
font-size: 0.8125rem;
}
.soundboard-btn.hidden-sound { opacity: 0.4; }
/* Eye (hide/unhide) toggle on each tile. Always rendered with a reserved
width and at low opacity so it never shifts the tile contents on hover. */
.soundboard-btn .sb-hide-btn {
font-size: 0.6875rem;
color: var(--text-muted);
cursor: pointer;
padding: 0.125rem 0.25rem;
border-radius: 0.125rem;
width: 1.125rem;
height: 1.125rem;
line-height: 14px;
text-align: center;
opacity: 0.35;
transition: opacity 0.15s, color 0.15s;
margin-left: auto;
flex-shrink: 0;
box-sizing: border-box;
}
.soundboard-btn:hover .sb-hide-btn { opacity: 1; }
.soundboard-btn .sb-hide-btn:hover { color: var(--danger); opacity: 1; }
/* Hidden tile: the eye icon is the only way back in, so keep it fully visible. */
.soundboard-btn.hidden-sound .sb-hide-btn { opacity: 1; }
/* In tile layout the eye sits at the top-right corner without affecting flex flow */
.soundboard-grid:not(.list-mode):not(.sidebar-mode) .soundboard-btn { position: relative; }
.soundboard-grid:not(.list-mode):not(.sidebar-mode) .soundboard-btn .sb-hide-btn {
position: absolute; top: 4px; right: 4px; margin: 0;
}
/* Hotkey recording visual */
.hotkey-recording { animation: hotkey-pulse 0.6s ease infinite alternate; }
@keyframes hotkey-pulse { from { box-shadow: 0 0 0 2px var(--accent-glow); } to { box-shadow: 0 0 8px 4px var(--accent-glow); } }
/* Collapsible sections in the Manage tab sound list */
.sound-section {
margin-bottom: 0.25rem;
}
.sound-section-label {
cursor: pointer;
user-select: none;
font-size: 0.78em;
text-transform: uppercase;
letter-spacing: .06em;
color: var(--text-muted);
padding: 0.25rem 0 0.25rem 0;
list-style: none;
display: flex;
align-items: center;
gap: 0.25rem;
}
.sound-section-label::before {
content: '\25BE'; /* small down triangle */
font-size: 0.625rem;
transition: transform 0.15s;
}
details.sound-section:not([open]) .sound-section-label::before {
content: '\25B8'; /* small right triangle */
}
.message-body { flex: 1; min-width: 0; }
.message-header { display: flex; align-items: baseline; gap: 0.5rem; margin-bottom: 0.125rem; }
.message-author { font-weight: 600; font-size: 0.875rem; }
.message-time { font-size: 0.6875rem; color: var(--text-muted); }
/* A message in Persian, Arabic or Hebrew reads right to left on its own
line, so its punctuation lands on the right end; English is unaffected. */
.message-content { unicode-bidi: plaintext; }
/* With the interface in Persian every label does the same. The layout
itself stays left to right. */
html[lang="fa"] body * { unicode-bidi: plaintext; }
/* An empty box shows its Persian hint right to left; once something is
typed, the box follows the text (dir="auto" on the message boxes). */
html[lang="fa"] :is(input:not([type]), input[type="text"], input[type="search"], textarea):placeholder-shown { direction: rtl; }
.message-content {
font-size: 0.875rem;
color: var(--text-primary);
word-wrap: break-word;
overflow-wrap: break-word;
line-height: 1.45;
}
.message-content a { color: var(--text-link); }
/* A thin underline so a link reads as one on any palette, not only by its
colour; a coloured span around it cannot repaint it (#5661). */
.message-content a:not(.file-download-link) { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 0.15em; }
.message-compact {
display: flex;
position: relative;
/* Left pad matches the leader's avatar column (see --msg-gutter above). */
padding: 0.0625rem 0.5rem 0.0625rem var(--msg-gutter);
border-radius: var(--radius-sm);
transition: background var(--transition);
flex-shrink: 0;
}
.message-compact:hover { background: rgba(255,255,255,0.02); box-shadow: var(--msg-glow); }
/* Thin horizontal separator between different users' message groups */
.message-user-sep {
margin-top: 0.5rem;
border-top: 1px solid var(--border);
padding-top: 0.25rem;
}
.message-compact .compact-time {
position: absolute;
left: var(--msg-pad-x);
top: 2px;
font-size: 0.625rem;
color: var(--text-muted);
display: none;
}
/* Only show compact timestamp on real mouse hover */
@media (hover: hover) {
.message-compact:hover .compact-time { display: block; }
}
.system-message {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
color: var(--text-muted);
font-style: italic;
text-align: center;
}
.welcome-message {
padding: 0.625rem 1rem;
margin: 0.5rem auto;
max-width: 30rem;
font-size: 0.875rem;
font-weight: 600;
color: var(--accent, #5865f2);
text-align: center;
background: var(--bg-secondary, #2b2d31);
border: 1px solid var(--border-light, #3a3c42);
border-radius: 0.5rem;
}
/* A message marked NSFW (app-message-nsfw.js): text, pictures, videos and
link cards stay blurred under a label until clicked. Text blurs through a
shadow, so the label on top of it stays sharp. */
[data-nsfw="1"]:not([data-nsfw-shown]) :is(.message-content, .thread-msg-content) {
position: relative;
min-height: 2.25rem;
overflow: hidden;
border-radius: var(--radius);
cursor: pointer;
user-select: none;
color: transparent;
text-shadow: 0 0 12px var(--text-primary);
}
[data-nsfw="1"]:not([data-nsfw-shown]) :is(.message-content, .thread-msg-content) a { color: transparent; }
[data-nsfw="1"]:not([data-nsfw-shown]) :is(.message-content, .thread-msg-content) > * { filter: blur(18px); pointer-events: none; }
[data-nsfw="1"]:not([data-nsfw-shown]) :is(.message-content, .thread-msg-content)::after {
content: var(--nsfw-message-label, "NSFW");
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8rem;
font-weight: 600;
color: var(--text-primary);
text-shadow: none;
background: color-mix(in srgb, var(--bg-tertiary) 55%, transparent);
}
.typing-indicator {
padding: 0 1.25rem;
height: 1.25rem;
font-size: 0.75rem;
color: var(--text-muted);
display: flex;
align-items: center;
}
/* ── Message Input ─────────────────────────────────────── */
.message-input-area {
display: flex;
align-items: flex-end;
/* In a narrow message column (a half-screen browser with both sidebars
open) the toolbar was squeezing the text box down to a few characters.
The box keeps a usable width and the toolbar wraps onto its own row
above it instead. (#5626) */
flex-wrap: wrap;
gap: 0.5rem;
padding: 1rem 1.25rem 1rem;
position: relative;
/* Above #fx-layers (z:99990) so theme particles/embers don't float over
the textarea, send button, and toolbar. The composer is a critical
interaction surface and must stay visually unobstructed. */
z-index: 99995;
background: var(--bg-secondary);
}
/* Moderation history and warnings (#5759). */
.mod-cases-modal { max-width: 34rem; width: 100%; }
.mod-case-reason { margin: 0; padding: 0.5rem 0.75rem; border-left: 3px solid var(--warning, #f0a020); background: var(--bg-tertiary); border-radius: var(--radius-sm); white-space: pre-wrap; }
.mod-case-input { width: 100%; box-sizing: border-box; resize: vertical; }
.mod-case-severity { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.5rem; font-size: 0.8125rem; }
.mod-case-summary { margin-bottom: 0.5rem; font-size: 0.8125rem; }
.mod-case-list { max-height: 50vh; overflow-y: auto; display: flex; flex-direction: column; gap: 0.375rem; margin-bottom: 0.5rem; }
.mod-case-row { display: flex; gap: 0.5rem; align-items: flex-start; padding: 0.375rem 0.5rem; border-radius: var(--radius-sm); background: var(--bg-tertiary); }
.mod-case-row.is-revoked { opacity: 0.6; }
.mod-case-row.is-revoked .mod-case-text { text-decoration: line-through; }
.mod-case-body { flex: 1; min-width: 0; }
.mod-case-meta { font-size: 0.75rem; color: var(--text-muted); }
.mod-case-text { font-size: 0.875rem; white-space: pre-wrap; word-break: break-word; }
.mod-case-revoked { font-style: italic; }
.mod-case-note { display: flex; gap: 0.5rem; align-items: flex-end; }
.mod-case-note[hidden] { display: none; }
/* Above the message box in a read-only channel, for those who may post. */
.read-only-note {
padding: 0.25rem 1rem 0;
font-size: 0.75rem;
color: var(--text-muted);
}
.message-input-area textarea {
flex: 1;
min-width: 11rem;
padding: 0.625rem 0.875rem;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text-primary);
font-size: 0.875rem;
resize: none;
max-height: 7.5rem;
line-height: 1.4;
transition: border-color var(--transition);
}
.message-input-area textarea:focus { border-color: var(--accent); }
.message-input-area textarea::placeholder { color: var(--text-muted); }
.btn-send {
width: 2.625rem;
height: 2.625rem;
background: var(--accent);
color: var(--accent-text);
border-radius: var(--radius);
display: flex;
align-items: center;
justify-content: center;
transition: background var(--transition);
flex-shrink: 0;
}
.btn-send:hover { background: var(--accent-hover); }
/* ── Right Sidebar ─────────────────────────────────────── */
.right-sidebar {
width: var(--right-width);
min-width: 12.5rem;
max-width: 25rem;
background: var(--bg-secondary);
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
transition: width 0.25s ease, min-width 0.25s ease, border-width 0.25s ease, opacity 0.2s ease;
}
/* Collapsed state */
.right-sidebar.collapsed {
width: 0 !important;
min-width: 0 !important;
border-left-width: 0;
opacity: 0;
pointer-events: none;
}
/* Sidebar collapse arrow: fixed to the right edge of the viewport.
Always accessible regardless of sidebar state or scroll position. */
.sidebar-collapse-btn {
position: fixed;
right: 0;
top: 72px; /* near top, where the voice panel header sits */
transform: none;
width: 1.25rem;
height: 2.25rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.6875rem;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-right: none;
border-radius: 0.375rem 0 0 0.375rem;
color: var(--text-muted);
cursor: pointer;
padding: 0;
z-index: 100;
/* `right` is set imperatively in JS when sidebars open/close; animate it
so the button slides with the panel rather than popping into place. */
transition: right 0.25s ease, color 0.15s, background 0.15s;
}
.sidebar-collapse-btn:hover {
color: var(--text-primary);
background: var(--bg-tertiary);
}
/* Users section: fills remaining space and scrolls */
.right-sidebar-users {
flex: 1 1 0;
overflow-y: auto;
min-height: 0;
}
/* Resize handle (left edge of right sidebar) */
.right-sidebar-resize-handle {
position: absolute;
top: 0;
left: -3px;
width: 0.375rem;
height: 100%;
cursor: col-resize;
z-index: 10;
background: transparent;
transition: background 0.15s;
}
.right-sidebar-resize-handle:hover,
.right-sidebar-resize-handle.dragging {
background: var(--accent);
opacity: 0.5;
}
.panel { padding: 1rem; border-bottom: 1px solid var(--border); }
.panel-title {
font-size: 0.6875rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.8px;
color: var(--text-muted);
margin-bottom: 0.625rem;
}
.user-list { display: flex; flex-direction: column; gap: 0.25rem; }
.muted-text { font-size: 0.75rem; color: var(--text-muted); font-style: italic; }
.user-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0.375rem; /* 4px 6px */
border-radius: var(--radius-sm);
position: relative;
}
.user-item:hover {
background: var(--bg-hover);
}
.user-dot { width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--success); flex-shrink: 0; }
/* Text column beside the avatar: name on its own line, with an optional
smaller line under it for whatever the person is doing. Previously the name
sat in the same flex row as the custom status, the activity and the score
badge, all of which reserved width, so a user with both a status and a game
saw their username collapse to one character and an ellipsis. */
.user-item-text {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
gap: 0.0625rem;
}
/* The name line. Role dot/icon and badges sit with the name; nothing else
competes for this row. */
.user-item-line {
display: flex;
align-items: center;
gap: 0.375rem;
min-width: 0;
}
/* The "half line". Deliberately smaller and dimmer than the name so the list
still scans by username first. */
.user-item-sub {
display: flex;
align-items: center;
min-width: 0;
line-height: 1.15;
}
.user-item-name {
font-size: 0.8125rem;
color: var(--text-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
min-width: 0;
}
/* Inside the sub-line these fill the width instead of reserving a fixed slice
of the name's row, which is what made them crowd the username before. */
.user-item-sub .user-status-text,
.user-item-sub .user-activity {
max-width: 100%;
margin-left: 0;
flex: 1;
min-width: 0;
}
/* Role color dot (to the left of the username, inside avatar wrapper) */
.user-role-dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
flex-shrink: 0;
border: 1px solid rgba(0,0,0,0.3);
}
/* User tooltip (shown on hover over name) */
.user-item-tooltip {
display: none;
position: absolute;
left: 50%;
bottom: calc(100% + 6px);
transform: translateX(-50%);
background: var(--bg-secondary);
border: 1px solid var(--border-light);
border-radius: var(--radius);
padding: 0.5rem 0.75rem;
z-index: 1000;
white-space: nowrap;
pointer-events: none;
box-shadow: 0 6px 20px rgba(0,0,0,0.6);
font-size: 0.75rem;
color: var(--text-primary);
max-width: 13.75rem;
opacity: 1;
backdrop-filter: none;
}
.user-item-tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: var(--border-light);
}
.user-item:hover .user-item-tooltip {
display: block;
}
.tooltip-username {
font-weight: 700;
font-size: 0.8125rem;
margin-bottom: 0.125rem;
}
.tooltip-role {
font-size: 0.6875rem;
opacity: 0.8;
}
.tooltip-status {
font-size: 0.6875rem;
color: var(--text-muted);
font-style: italic;
margin-top: 0.125rem;
}
/* ═══════════════════════════════════════════════════════════
PROFILE POPUP (Discord-style mini profile card)
═══════════════════════════════════════════════════════════ */
.profile-popup {
position: fixed;
/* Appended to <body>, so it stacks against the page rather than inside the
member list that opened it. Below 900px the right sidebar becomes a fixed
overlay at 100000 with its backdrop at 99996, which left this popup at
10001 rendering behind both (#5465). Same layer the member-list gear menu
uses for exactly this reason (#5462). */
z-index: 100010;
width: 20rem;
/* Translucent, theme-aware glass. Colours come from CSS vars so it tracks
the active theme in both light and dark. */
background: color-mix(in srgb, var(--bg-secondary) 65%, transparent);
backdrop-filter: blur(20px) saturate(1.3);
-webkit-backdrop-filter: blur(20px) saturate(1.3);
border: 1px solid color-mix(in srgb, var(--border-light) 50%, transparent);
border-radius: var(--radius);
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
overflow: hidden;
animation: profile-popup-in 0.18s ease-out;
opacity: 1;
}
/* Hover-over variant: a non-interactive preview. The base card is already
translucent glass, so this only strips the interactive bits. */
.profile-popup-hover {
animation: profile-popup-hover-in 0.15s ease-out;
transition: opacity 0.2s ease, transform 0.2s ease;
pointer-events: none; /* it is a tooltip, not a card */
}
.profile-popup-hover .profile-popup-banner { opacity: 0.7; }
.profile-popup-hover .profile-popup-close { display: none; }
.profile-popup-hover .profile-popup-actions { display: none; }
/* Fade-out for hover popups */
.profile-popup-fading {
opacity: 0 !important;
transform: translateY(4px) !important;
pointer-events: none !important;
transition: opacity 0.2s ease, transform 0.2s ease !important;
}
@keyframes profile-popup-hover-in {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes profile-popup-in {
from { opacity: 0; transform: scale(0.95) translateY(6px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.profile-popup-banner {
height: 3.75rem;
position: relative;
background: var(--bg-tertiary);
}
.profile-popup-close {
position: absolute;
top: 6px; right: 8px;
background: none; border: none;
color: var(--text-muted);
font-size: 1.125rem;
cursor: pointer;
line-height: 1;
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
}
.profile-popup-close:hover { background: rgba(255,255,255,0.1); color: var(--text-primary); }
.profile-popup-avatar-wrapper {
position: relative;
width: 4.5rem; height: 4.5rem;
margin: -2.25rem 0 0 1rem;
}
.profile-popup-avatar {
width: 4.5rem; height: 4.5rem;
border-radius: 50%;
object-fit: cover;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.75rem;
font-weight: 700;
color: #fff;
}
.profile-popup-avatar.avatar-hexagon { border-radius: 0; clip-path: polygon(50% 0%,100% 25%,100% 75%,50% 100%,0% 75%,0% 25%); }
.profile-popup-avatar.avatar-squircle { border-radius: 22%; }
.profile-popup-avatar.avatar-diamond { border-radius: 0; clip-path: polygon(50% 0%,100% 50%,50% 100%,0% 50%); }
.profile-popup-avatar.avatar-shield { border-radius: 0; clip-path: polygon(50% 0%,100% 10%,100% 65%,50% 100%,0% 65%,0% 10%); }
.profile-popup-avatar-fallback {
background-color: var(--accent);
}
.profile-popup-status-dot {
position: absolute;
bottom: 2px; right: 2px;
width: 1rem; height: 1rem;
border-radius: 50%;
background: var(--success);
border: 3px solid var(--bg-primary);
}
.profile-popup-status-dot.away { background: var(--warning); }
.profile-popup-status-dot.dnd { background: var(--danger); }
.profile-popup-status-dot.invisible { background: var(--text-muted); }
.profile-popup-body {
padding: 0.625rem 1rem 1rem;
background: transparent;
}
.profile-popup-names {
display: flex;
flex-direction: column;
gap: 0.0625rem;
margin-bottom: 0.375rem;
}
.profile-popup-displayname {
font-size: 1.125rem;
font-weight: 700;
color: var(--text-primary);
line-height: 1.2;
}
.profile-popup-nickname {
font-size: 0.875rem;
font-weight: 600;
color: var(--accent);
line-height: 1.2;
margin-bottom: 0.125rem;
}
.profile-popup-username {
font-size: 0.8125rem;
color: var(--text-muted);
}
.profile-popup-status-text {
font-size: 0.8125rem;
color: var(--text-secondary);
font-style: italic;
padding: 0.25rem 0.5rem;
background: var(--bg-secondary);
border-radius: var(--radius-sm);
margin-bottom: 0.5rem;
}
.profile-popup-bio {
font-size: 0.8125rem;
color: var(--text-secondary);
line-height: 1.4;
margin-bottom: 0.25rem;
word-break: break-word;
}
.profile-bio-empty {
color: var(--text-muted);
font-style: italic;
font-size: 0.75rem;
}
.profile-bio-toggle {
background: none; border: none;
color: var(--accent);
cursor: pointer;
font-size: 0.75rem;
padding: 0.125rem 0;
}
.profile-bio-toggle:hover { text-decoration: underline; }
.profile-popup-divider {
height: 0.0625rem;
background: var(--border);
margin: 0.5rem 0;
}
.profile-popup-section-label {
font-size: 0.6875rem;
font-weight: 700;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 0.25rem;
letter-spacing: 0.03em;
}
.profile-popup-roles,
.user-profile-groups-list {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
margin-bottom: 0.5rem;
}
.profile-popup-role {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.5rem;
font-size: 0.75rem;
border: 1px solid;
border-radius: 0.75rem;
background: var(--bg-secondary);
}
.user-group-name {
display: flex;
align-items: center;
gap: 0.4rem;
}
.user-group-channel-info {
padding: 0.1rem 0.3rem;
border: 1px solid;
border-radius: 50%;
font-weight: 600;
color: var(--text-muted);
}
.profile-role-dot {
width: 0.5rem; height: 0.5rem;
border-radius: 50%;
flex-shrink: 0;
}
.profile-popup-join-date {
font-size: 0.75rem;
color: var(--text-secondary);
margin-bottom: 0.5rem;
}
/* Birthdays: the 🎉 in front of a name on the day, and the shower that falls
past the avatar when a profile card opens (_rainBirthdayConfetti). */
.profile-popup-displayname .birthday-mark { margin-right: 0.25em; }
.message-header .birthday-mark { margin-right: -0.25rem; }
.birthday-confetti {
position: fixed;
top: 0;
left: 0;
z-index: 100011; /* above the profile card */
line-height: 1;
pointer-events: none;
}
.profile-popup-actions {
margin-top: 0.5rem;
}
.profile-popup-action-btn {
width: 100%;
padding: 0.5rem 0.75rem;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.8125rem;
font-weight: 600;
transition: background var(--transition), transform 0.1s;
}
.profile-popup-action-btn:active { transform: scale(0.98); }
.profile-dm-btn {
background: var(--accent);
color: var(--accent-text);
}
.profile-dm-btn:hover { background: var(--accent-hover); }
.profile-edit-btn {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-light) !important;
}
.profile-edit-btn:hover { background: var(--bg-hover); }
.profile-gear-btn {
background: var(--bg-tertiary);
color: var(--text-secondary);
margin-top: 0.375rem;
border: 1px solid var(--border-light) !important;
}
.profile-gear-btn:hover { background: var(--bg-hover); color: var(--text-primary); }
/* Silent messages (app-silent.js): a muted bell after the text, and the
message box toggle lit like the Send button while it is on. */
.silent-tag {
display: inline-flex;
align-items: center;
vertical-align: -1px;
margin-left: 0.375rem;
color: var(--text-muted);
opacity: 0.75;
cursor: default;
user-select: none;
}
.silent-tag svg { display: block; }
.btn-silent.active,
.btn-silent.active:hover {
background: var(--accent);
color: var(--accent-text);
}