@devchitchat/chat 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +313 -0
  2. package/index.js +148 -0
  3. package/migrate/001-drop-channel-invites.js +3 -0
  4. package/migrate/002-invite-initial-roles.js +5 -0
  5. package/migrate/003-dm-channels.js +35 -0
  6. package/migrate/004-notifications.js +15 -0
  7. package/migrate/005-uploads.js +21 -0
  8. package/migrate/006-mention-priority.js +3 -0
  9. package/migrate/007-push-subscriptions.js +14 -0
  10. package/migrate/008-messages-channel-seq-index.js +3 -0
  11. package/migrate/009-message-reactions.js +15 -0
  12. package/migrate/010-edit-messages.js +7 -0
  13. package/package.json +51 -0
  14. package/pages/_error.html +12 -0
  15. package/pages/_layout.html +31 -0
  16. package/pages/_layout.js +13 -0
  17. package/pages/admin/_layout.html +52 -0
  18. package/pages/admin/_layout.js +8 -0
  19. package/pages/admin/bots/[userId].js +88 -0
  20. package/pages/admin/bots/[userId].phtml +89 -0
  21. package/pages/admin/bots/index.js +41 -0
  22. package/pages/admin/bots/index.phtml +58 -0
  23. package/pages/admin/index.js +8 -0
  24. package/pages/admin/invites/index.js +72 -0
  25. package/pages/admin/invites/index.phtml +88 -0
  26. package/pages/admin/users/[userId].js +60 -0
  27. package/pages/admin/users/[userId].phtml +57 -0
  28. package/pages/admin/users/index.js +20 -0
  29. package/pages/admin/users/index.phtml +37 -0
  30. package/pages/api/uploads/index.js +66 -0
  31. package/pages/api/user/settings.js +26 -0
  32. package/pages/auth/signout.js +14 -0
  33. package/pages/channels/[channelId].js +99 -0
  34. package/pages/channels/[channelId].phtml +173 -0
  35. package/pages/index.js +33 -0
  36. package/pages/invite/[token].js +10 -0
  37. package/pages/login/index.js +57 -0
  38. package/pages/login/index.phtml +29 -0
  39. package/pages/public/client/action-sheet.js +77 -0
  40. package/pages/public/client/app.js +38 -0
  41. package/pages/public/client/auth-tabs.js +13 -0
  42. package/pages/public/client/emoji-data.js +197 -0
  43. package/pages/public/client/islands/call.js +1770 -0
  44. package/pages/public/client/islands/sidebar.js +1197 -0
  45. package/pages/public/client/long-press.js +59 -0
  46. package/pages/public/client/modal.js +50 -0
  47. package/pages/public/client/router.js +87 -0
  48. package/pages/public/client/rtc-peer-manager.js +344 -0
  49. package/pages/public/client/settings-sync.js +76 -0
  50. package/pages/public/client/shared/messages.js +147 -0
  51. package/pages/public/client/swipe-nav.js +98 -0
  52. package/pages/public/client/theme.js +27 -0
  53. package/pages/public/client/ws.js +71 -0
  54. package/pages/public/favicon.ico +0 -0
  55. package/pages/public/favicon.png +0 -0
  56. package/pages/public/icon.png +0 -0
  57. package/pages/public/manifest.json +11 -0
  58. package/pages/public/sw.js +38 -0
  59. package/pages/public/themes/base.css +1786 -0
  60. package/pages/public/themes/dark.css +22 -0
  61. package/pages/public/themes/forest.css +22 -0
  62. package/pages/public/themes/light.css +23 -0
  63. package/pages/public/themes/ocean.css +22 -0
  64. package/pages/public/themes/rose.css +22 -0
  65. package/pages/registration/index.js +35 -0
  66. package/pages/registration/index.phtml +38 -0
  67. package/pages/uploads/[uploadId]/[filename].js +45 -0
  68. package/src/adapters/InMemoryAuthRepository.js +74 -0
  69. package/src/adapters/InMemoryChannelRepository.js +138 -0
  70. package/src/adapters/InMemoryDeliveryRepository.js +52 -0
  71. package/src/adapters/InMemoryFileStore.js +53 -0
  72. package/src/adapters/InMemoryHubRepository.js +85 -0
  73. package/src/adapters/InMemoryMessageRepository.js +35 -0
  74. package/src/adapters/InMemoryReactionRepository.js +45 -0
  75. package/src/adapters/InMemorySearchRepository.js +37 -0
  76. package/src/adapters/InMemorySignalingRepository.js +35 -0
  77. package/src/adapters/InMemoryUploadRepository.js +36 -0
  78. package/src/adapters/InMemoryUserSettingsRepository.js +15 -0
  79. package/src/adapters/LocalFileStore.js +40 -0
  80. package/src/adapters/SqliteAuthRepository.js +184 -0
  81. package/src/adapters/SqliteChannelRepository.js +149 -0
  82. package/src/adapters/SqliteDeliveryRepository.js +53 -0
  83. package/src/adapters/SqliteHubRepository.js +99 -0
  84. package/src/adapters/SqliteMessageRepository.js +90 -0
  85. package/src/adapters/SqlitePushRepository.js +39 -0
  86. package/src/adapters/SqliteReactionRepository.js +50 -0
  87. package/src/adapters/SqliteSearchRepository.js +42 -0
  88. package/src/adapters/SqliteSignalingRepository.js +50 -0
  89. package/src/adapters/SqliteUploadRepository.js +34 -0
  90. package/src/adapters/SqliteUserSettingsRepository.js +23 -0
  91. package/src/adminAuth.js +25 -0
  92. package/src/config.js +11 -0
  93. package/src/context.js +77 -0
  94. package/src/core/dm.js +10 -0
  95. package/src/core/mentions.js +27 -0
  96. package/src/core/messages.js +21 -0
  97. package/src/core/reactions.js +6 -0
  98. package/src/core/roles.js +5 -0
  99. package/src/core/uploads.js +107 -0
  100. package/src/db/initDb.js +225 -0
  101. package/src/db/openDb.js +18 -0
  102. package/src/db/runMigrations.js +45 -0
  103. package/src/db/transaction.js +11 -0
  104. package/src/ports/IFileStore.js +34 -0
  105. package/src/services/AuthService.js +208 -0
  106. package/src/services/BotService.js +148 -0
  107. package/src/services/ChannelService.js +176 -0
  108. package/src/services/DeliveryService.js +28 -0
  109. package/src/services/HubService.js +133 -0
  110. package/src/services/MessageService.js +122 -0
  111. package/src/services/NotificationService.js +45 -0
  112. package/src/services/PresenceService.js +55 -0
  113. package/src/services/ReactionService.js +57 -0
  114. package/src/services/SearchService.js +21 -0
  115. package/src/services/SignalingService.js +177 -0
  116. package/src/services/UploadService.js +111 -0
  117. package/src/services/UserSettingsService.js +30 -0
  118. package/src/services/WebPushService.js +217 -0
  119. package/src/util/crypto.js +21 -0
  120. package/src/util/errors.js +14 -0
  121. package/src/util/ids.js +3 -0
  122. package/src/util/logger.js +21 -0
  123. package/src/ws/ChatServer.js +478 -0
  124. package/src/ws/handlers/authHandlers.js +152 -0
  125. package/src/ws/handlers/channelHandlers.js +166 -0
  126. package/src/ws/handlers/hubHandlers.js +82 -0
  127. package/src/ws/handlers/messageHandlers.js +88 -0
  128. package/src/ws/handlers/pushHandlers.js +25 -0
  129. package/src/ws/handlers/reactionHandlers.js +27 -0
  130. package/src/ws/handlers/rtcHandlers.js +126 -0
  131. package/styles.css +22 -0
@@ -0,0 +1,1786 @@
1
+ /* base.css — layout, components, and structural styles.
2
+ All colours come from CSS custom properties so themes only need to
3
+ redefine the variables; no structural rules are repeated per-theme. */
4
+
5
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
6
+
7
+ /* Form elements don't inherit font by default; without this iOS Safari
8
+ zooms in on focus when font-size < 16px. */
9
+ input, textarea, select, button { font: inherit; }
10
+
11
+ :root {
12
+ --font-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
13
+ --font-mono: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', monospace;
14
+ --font-size: 17px;
15
+ --radius-sm: 4px;
16
+ --radius-md: 8px;
17
+ --radius-lg: 16px;
18
+
19
+ --sidebar-width: 240px;
20
+ --composer-height: 80px;
21
+ --call-bar-height: 0px; /* expands when a call is active */
22
+
23
+ --transition: 150ms ease;
24
+ }
25
+
26
+ html, body { height: 100%; font-family: var(--font-sans); }
27
+ body { background: var(--bg-base); color: var(--text-primary); }
28
+
29
+ /* Reset font size */
30
+ html, body, h1, h2, h3, h4, h5, h6, p, a, button, input, select, time {
31
+ font-size: var(--font-size);
32
+ }
33
+
34
+ details summary {
35
+ cursor: pointer;
36
+ }
37
+
38
+ /* ── App shell ───────────────────────────────────────────────────────────── */
39
+ .app-shell {
40
+ display: flex;
41
+ height: 100%;
42
+ }
43
+
44
+ /* ── Sidebar ─────────────────────────────────────────────────────────────── */
45
+ .sidebar {
46
+ width: var(--sidebar-width);
47
+ flex-shrink: 0;
48
+ display: flex;
49
+ flex-direction: column;
50
+ background: var(--bg-sidebar);
51
+ border-right: 1px solid var(--border);
52
+ overflow-y: auto;
53
+ height: 100vh;
54
+ position: sticky;
55
+ top: 0;
56
+ z-index: 1;
57
+ padding-top: env(safe-area-inset-top);
58
+ }
59
+
60
+ .hub-group { padding: 8px 0; }
61
+ .hub-header {
62
+ padding: 6px 16px;
63
+ font-weight: 700;
64
+ text-transform: uppercase;
65
+ letter-spacing: 0.8px;
66
+ color: var(--text-muted);
67
+ }
68
+ .channel-list {
69
+ list-style: none;
70
+ text-transform: none;
71
+ }
72
+
73
+ .hub-name {
74
+ display: flex;
75
+ align-items: center;
76
+ gap: 2px;
77
+ user-select: none;
78
+ -webkit-user-select: none;
79
+ -webkit-touch-callout: none;
80
+ }
81
+ .hub-name > span { flex: 1; }
82
+
83
+ .channel-item {
84
+ display: flex;
85
+ align-items: center;
86
+ border-bottom: solid 2px transparent;
87
+ border-top: solid 2px transparent;
88
+ }
89
+
90
+ .channel-item a {
91
+ color: var(--text-secondary);
92
+ text-decoration: none;
93
+ flex: 1;
94
+ height: 2.3rem;
95
+ display: flex;
96
+ align-items: center;
97
+ padding-left: .7rem;
98
+ user-select: none;
99
+ -webkit-user-select: none;
100
+ -webkit-touch-callout: none;
101
+ }
102
+
103
+ .channel-item a:hover {
104
+ color: var(--text-primary);
105
+ }
106
+
107
+ .channel-item.active a {
108
+ color: var(--text-primary);
109
+ }
110
+
111
+ /* ── Channel + hub drag-and-drop reordering (desktop) ───────────────────── */
112
+ @media (min-width: 1025px) {
113
+ .channel-item[draggable="true"] { cursor: grab; }
114
+ .channel-item.dragging { opacity: 0.4; }
115
+ .channel-item.drop-before { border-top: 2px solid var(--accent, #5865f2); }
116
+ .channel-item.drop-after { border-bottom: 2px solid var(--accent, #5865f2); }
117
+
118
+ .hub-header[draggable="true"] { cursor: grab; }
119
+ .hub-header.dragging { opacity: 0.4; }
120
+ .hub-header.drop-before { border-top: 2px solid var(--accent, #5865f2); }
121
+ .hub-header.drop-after { border-bottom: 2px solid var(--accent, #5865f2); }
122
+ }
123
+
124
+ /* ── Sidebar gear buttons ────────────────────────────────────────────────── */
125
+ .btn-hub-gear,
126
+ .btn-channel-gear {
127
+ display: none;
128
+ flex-shrink: 0;
129
+ }
130
+
131
+ /* Desktop (≥ 1025px): reveal gear on row hover */
132
+ @media (min-width: 1025px) {
133
+ .hub-name:hover .btn-hub-gear,
134
+ .channel-item:hover .btn-channel-gear {
135
+ display: flex;
136
+ }
137
+ }
138
+
139
+ /* ── Direct Messages section ─────────────────────────────────────────────── */
140
+ .dm-section {
141
+ margin-top: 4px;
142
+ padding: 8px 0 4px;
143
+ border-top: 1px solid var(--border);
144
+ }
145
+
146
+ .dm-section-title {
147
+ padding: 4px 16px 6px;
148
+ font-size: 11px;
149
+ font-weight: 700;
150
+ text-transform: uppercase;
151
+ letter-spacing: 1px;
152
+ color: var(--text-muted);
153
+ display: flex;
154
+ align-items: center;
155
+ gap: 6px;
156
+ }
157
+
158
+ /* Avatar-style bullet before "Direct Messages" label */
159
+ .dm-section-title::before {
160
+ content: '';
161
+ display: inline-block;
162
+ width: 6px;
163
+ height: 6px;
164
+ border-radius: 50%;
165
+ background: var(--accent);
166
+ opacity: 0.6;
167
+ flex-shrink: 0;
168
+ }
169
+
170
+ .dm-list {
171
+ list-style: none;
172
+ }
173
+
174
+ .dm-item {
175
+ display: flex;
176
+ align-items: center;
177
+ }
178
+
179
+ .dm-link {
180
+ display: flex;
181
+ align-items: center;
182
+ gap: 8px;
183
+ flex: 1;
184
+ height: 2rem;
185
+ padding: 0 16px;
186
+ color: var(--text-secondary);
187
+ text-decoration: none;
188
+ font-size: 0.9em;
189
+ user-select: none;
190
+ -webkit-user-select: none;
191
+ -webkit-touch-callout: none;
192
+ transition: color var(--transition), background var(--transition);
193
+ }
194
+
195
+ .dm-link:hover {
196
+ color: var(--text-primary);
197
+ background: var(--bg-hover);
198
+ }
199
+
200
+ .dm-item.dm-selected .dm-link {
201
+ color: var(--text-primary);
202
+ background: var(--bg-active);
203
+ font-weight: 600;
204
+ }
205
+
206
+ .dm-name {
207
+ flex: 1;
208
+ overflow: hidden;
209
+ text-overflow: ellipsis;
210
+ white-space: nowrap;
211
+ }
212
+
213
+ /* Unread dot on DM/channel items — blue for normal mentions, red for urgent.
214
+ Uses data attributes instead of classes so rdbljs className re-renders
215
+ (which only touch el.className) never wipe the dot state. */
216
+ .dm-item[data-mention] .dm-link::after,
217
+ .channel-item[data-mention] .channel-link::after {
218
+ content: '';
219
+ display: inline-block;
220
+ width: 8px;
221
+ height: 8px;
222
+ border-radius: 50%;
223
+ background: var(--accent);
224
+ margin-left: auto;
225
+ flex-shrink: 0;
226
+ }
227
+ .dm-item[data-urgent] .dm-link::after,
228
+ .channel-item[data-urgent] .channel-link::after {
229
+ content: '';
230
+ display: inline-block;
231
+ width: 8px;
232
+ height: 8px;
233
+ border-radius: 50%;
234
+ background: var(--color-danger);
235
+ margin-left: auto;
236
+ flex-shrink: 0;
237
+ }
238
+
239
+ /* ── Action sheet (mobile + iPad) ────────────────────────────────────────── */
240
+ .action-sheet-backdrop {
241
+ position: fixed;
242
+ inset: 0;
243
+ z-index: 200;
244
+ background: rgba(0, 0, 0, 0.45);
245
+ opacity: 0;
246
+ pointer-events: none;
247
+ transition: opacity var(--transition);
248
+ }
249
+ .action-sheet-backdrop.open {
250
+ opacity: 1;
251
+ pointer-events: auto;
252
+ }
253
+
254
+ .action-sheet {
255
+ position: fixed;
256
+ left: 0; right: 0; bottom: 0;
257
+ z-index: 201;
258
+ background: var(--bg-sidebar);
259
+ border-top: 1px solid var(--border);
260
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
261
+ padding: 12px 0;
262
+ padding-bottom: env(safe-area-inset-bottom, 16px);
263
+ transform: translateY(100%);
264
+ transition: transform var(--transition);
265
+ max-width: 640px;
266
+ margin: 0 auto;
267
+ }
268
+ .action-sheet.open { transform: translateY(0); }
269
+
270
+ .action-sheet-handle {
271
+ width: 36px; height: 4px;
272
+ background: var(--border);
273
+ border-radius: 2px;
274
+ margin: 0 auto 12px;
275
+ }
276
+
277
+ .action-sheet-label {
278
+ padding: 4px 20px 8px;
279
+ font-size: 12px;
280
+ font-weight: 700;
281
+ text-transform: uppercase;
282
+ letter-spacing: 0.8px;
283
+ color: var(--text-muted);
284
+ }
285
+
286
+ .action-sheet-item {
287
+ display: flex;
288
+ align-items: center;
289
+ width: 100%;
290
+ padding: 14px 20px;
291
+ background: none;
292
+ border: none;
293
+ color: var(--text-primary);
294
+ font-size: 1rem;
295
+ text-align: left;
296
+ cursor: pointer;
297
+ }
298
+ .action-sheet-item:hover,
299
+ .action-sheet-item:active { background: var(--bg-hover); }
300
+ .action-sheet-item.danger { color: var(--color-danger); }
301
+ .action-sheet-item:disabled { color: var(--text-muted); cursor: default; opacity: 0.45; pointer-events: none; }
302
+
303
+ /* ── Modal (desktop) ─────────────────────────────────────────────────────── */
304
+ .modal-backdrop {
305
+ position: fixed;
306
+ inset: 0;
307
+ z-index: 200;
308
+ background: rgba(0, 0, 0, 0.55);
309
+ display: flex;
310
+ align-items: center;
311
+ justify-content: center;
312
+ opacity: 0;
313
+ pointer-events: none;
314
+ transition: opacity var(--transition);
315
+ }
316
+ .modal-backdrop.open {
317
+ opacity: 1;
318
+ pointer-events: auto;
319
+ }
320
+
321
+ .modal {
322
+ background: var(--bg-sidebar);
323
+ border: 1px solid var(--border);
324
+ border-radius: var(--radius-lg);
325
+ width: 100%;
326
+ max-width: 480px;
327
+ padding: 28px 32px;
328
+ display: flex;
329
+ flex-direction: column;
330
+ gap: 20px;
331
+ transform: scale(0.97);
332
+ transition: transform var(--transition);
333
+ }
334
+ .modal-backdrop.open .modal { transform: scale(1); }
335
+
336
+ .modal-header {
337
+ display: flex;
338
+ align-items: center;
339
+ justify-content: space-between;
340
+ }
341
+ .modal-title { font-size: 1.1rem; font-weight: 700; }
342
+ .modal-close {
343
+ background: none; border: none; cursor: pointer;
344
+ color: var(--text-muted); font-size: 1.2rem; padding: 4px;
345
+ }
346
+ .modal-close:hover { color: var(--text-primary); }
347
+
348
+ .modal-body { display: flex; flex-direction: column; gap: 16px; }
349
+
350
+ .modal-footer {
351
+ display: flex;
352
+ justify-content: flex-end;
353
+ gap: 10px;
354
+ border-top: 1px solid var(--border);
355
+ padding-top: 20px;
356
+ margin-top: 4px;
357
+ }
358
+
359
+ .modal-danger-zone {
360
+ border-top: 1px solid var(--border);
361
+ padding-top: 16px;
362
+ display: flex;
363
+ flex-direction: column;
364
+ gap: 10px;
365
+ }
366
+ .modal-danger-zone p { font-size: 13px; color: var(--text-muted); }
367
+ .btn-danger {
368
+ align-self: flex-start;
369
+ padding: 8px 16px;
370
+ border: 1px solid var(--color-danger);
371
+ border-radius: var(--radius-sm);
372
+ background: none;
373
+ color: var(--color-danger);
374
+ font-weight: 600;
375
+ cursor: pointer;
376
+ }
377
+ .btn-danger:hover { background: color-mix(in srgb, var(--color-danger) 12%, transparent); }
378
+
379
+ .sidebar-footer {
380
+ margin-top: auto;
381
+ padding: 12px 16px;
382
+ padding-bottom: calc(12px + env(safe-area-inset-bottom));
383
+ border-top: 1px solid var(--border);
384
+ display: flex;
385
+ flex-direction: column;
386
+ gap: 8px;
387
+ }
388
+
389
+ .sidebar-username {
390
+ font-size: 12px;
391
+ color: var(--text-muted);
392
+ padding: 0 2px;
393
+ }
394
+
395
+ .sidebar-footer-controls {
396
+ display: flex;
397
+ align-items: center;
398
+ gap: 6px;
399
+ }
400
+ .sidebar-footer-controls select {
401
+ flex: 1;
402
+ min-width: 0;
403
+ font-size: 12px;
404
+ padding: 4px 6px;
405
+ border-radius: var(--radius-sm);
406
+ border: 1px solid var(--border);
407
+ background: var(--bg-input);
408
+ color: var(--text-secondary);
409
+ cursor: pointer;
410
+ }
411
+ .btn-sm {
412
+ font-size: 12px;
413
+ padding: 4px 8px;
414
+ white-space: nowrap;
415
+ }
416
+
417
+ .btn-new-hub {
418
+ width: 100%;
419
+ padding: 7px 10px;
420
+ border-radius: 6px;
421
+ border: 1px dashed var(--border);
422
+ background: transparent;
423
+ color: var(--text-secondary);
424
+ font-size: 13px;
425
+ cursor: pointer;
426
+ text-align: left;
427
+ }
428
+ .btn-new-hub:hover {
429
+ background: var(--bg-input);
430
+ color: var(--text-primary);
431
+ border-color: var(--accent);
432
+ }
433
+
434
+ .btn-enable-push {
435
+ width: 100%;
436
+ padding: 7px 10px;
437
+ border-radius: 6px;
438
+ border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
439
+ background: color-mix(in srgb, var(--accent) 8%, transparent);
440
+ color: var(--text-primary);
441
+ font-size: 13px;
442
+ cursor: pointer;
443
+ text-align: left;
444
+ margin-bottom: 6px;
445
+ }
446
+ .btn-enable-push:hover {
447
+ background: color-mix(in srgb, var(--accent) 15%, transparent);
448
+ color: var(--text-primary);
449
+ border-color: var(--accent);
450
+ }
451
+ .btn-enable-push[data-blocked] {
452
+ opacity: 0.55;
453
+ cursor: default;
454
+ border-style: dashed;
455
+ }
456
+ .btn-enable-push[data-blocked]:hover {
457
+ background: color-mix(in srgb, var(--accent) 8%, transparent);
458
+ color: var(--text-secondary);
459
+ border-color: color-mix(in srgb, var(--accent) 40%, transparent);
460
+ }
461
+
462
+ .user-avatar {
463
+ width: 30px; height: 30px; border-radius: 50%; background: var(--accent); display: inline-flex; align-items: center; justify-content: center;
464
+ font-weight: 700; color: white; flex-shrink: 0;
465
+ }
466
+ .user-name {
467
+ font-weight: 600;
468
+ color: var(--text-primary);
469
+ }
470
+
471
+ /* ── Main content ────────────────────────────────────────────────────────── */
472
+ .main-content {
473
+ flex: 1;
474
+ display: flex;
475
+ flex-direction: column;
476
+ min-width: 0;
477
+ height: 100vh;
478
+ z-index: 2;
479
+ }
480
+
481
+ /* ── Call bar ────────────────────────────────────────────────────────────── */
482
+ .call-bar {
483
+ background: var(--bg-call);
484
+ border-bottom: 1px solid var(--border);
485
+ padding: 8px 16px;
486
+ display: flex;
487
+ flex-direction: column;
488
+ gap: 8px;
489
+ }
490
+ .call-controls { display: flex; align-items: center; gap: 8px; }
491
+ .streams { display: flex; flex-wrap: wrap; gap: 8px; }
492
+ .stream-tile { position: relative; border-radius: var(--radius-md); overflow: hidden; background: var(--bg-sidebar); }
493
+ .stream-tile video { width: 160px; height: 90px; object-fit: cover; display: block; }
494
+
495
+ /* ── Chat panel ──────────────────────────────────────────────────────────── */
496
+ .chat-panel { flex: 1; display: flex; flex-direction: column; min-height: 0; }
497
+ .chat-header { padding: 14px 20px; padding-top: calc(14px + env(safe-area-inset-top)); border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 12px; }
498
+ .chat-title {
499
+ font-size: 1rem;
500
+ margin-bottom: 7px;
501
+ }
502
+ .chat-topic { color: var(--text-secondary); }
503
+ .chat-header-actions { margin-left: auto; display: flex; gap: 6px; }
504
+
505
+ .search-bar { padding: 8px 20px; background: var(--bg-topbar); border-bottom: 1px solid var(--border); }
506
+ .search-bar input {
507
+ width: 100%; padding: 8px 12px; border-radius: var(--radius-md); border: 1px solid var(--border); background: var(--bg-input); color: var(--text-primary);
508
+ }
509
+ .search-results { list-style: none; margin-top: 8px; display: flex; flex-direction: column; gap: 4px; }
510
+ .search-result {
511
+ padding: 8px 12px; border-radius: var(--radius-sm); background: var(--bg-hover);
512
+ }
513
+ .result-handle { font-weight: 600; margin-right: 8px; }
514
+
515
+ .messages {
516
+ overflow-y: auto;
517
+ overflow-x: clip;
518
+ touch-action: pan-y;
519
+ overscroll-behavior: contain;
520
+ padding: 16px 20px;
521
+ display: flex;
522
+ flex-direction: column;
523
+ gap: 4px;
524
+ scroll-behavior: smooth;
525
+ }
526
+
527
+ .message {
528
+ padding: 4px 0;
529
+ }
530
+ .message pre {
531
+ padding: 1rem;
532
+ font-family: 'Courier New', Courier, monospace;
533
+ font-size: .8rem;
534
+ background: var(--code-background);
535
+ }
536
+ .message-handle { font-weight: 600; color: var(--accent); grid-column: 1; grid-row: 1; }
537
+ .message-handle.dm-trigger { cursor: pointer; }
538
+ .message-handle.dm-trigger:hover { text-decoration: underline; }
539
+ .message-time { font-size: 11px; color: var(--text-muted); grid-column: 2; grid-row: 1; align-self: end; }
540
+ .message-text { grid-column: 1 / -1; grid-row: 2; line-height: 1.5; color: var(--text-primary); white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere; }
541
+ .message-text a { word-break: break-all; }
542
+
543
+ /* ── Reaction bar ────────────────────────────────────────────────────────── */
544
+ .reaction-bar {
545
+ display: flex;
546
+ flex-wrap: wrap;
547
+ gap: 4px;
548
+ margin-top: 4px;
549
+ }
550
+ .reaction-pill {
551
+ display: inline-flex;
552
+ align-items: center;
553
+ gap: 3px;
554
+ background: none;
555
+ border: 1px solid var(--border);
556
+ border-radius: 12px;
557
+ padding: 2px 7px;
558
+ font-size: 13px;
559
+ cursor: pointer;
560
+ color: var(--text-secondary);
561
+ transition: border-color var(--transition), background var(--transition);
562
+ line-height: 1.4;
563
+ }
564
+ .reaction-pill:hover {
565
+ border-color: var(--accent);
566
+ background: color-mix(in srgb, var(--accent) 8%, transparent);
567
+ }
568
+ .reaction-pill.reacted {
569
+ border-color: var(--accent);
570
+ background: color-mix(in srgb, var(--accent) 15%, transparent);
571
+ color: var(--accent);
572
+ font-weight: 600;
573
+ }
574
+ .reaction-count { font-size: 12px; }
575
+ .reaction-add {
576
+ display: inline-flex;
577
+ align-items: center;
578
+ justify-content: center;
579
+ background: none;
580
+ border: 1px dashed var(--border);
581
+ border-radius: 12px;
582
+ padding: 2px 7px;
583
+ font-size: 13px;
584
+ cursor: pointer;
585
+ color: var(--text-muted);
586
+ transition: border-color var(--transition), color var(--transition);
587
+ line-height: 1.4;
588
+ }
589
+ .reaction-add:hover { border-color: var(--accent); color: var(--accent); }
590
+
591
+ /* ── Message context menu (right-click) ─────────────────────────────────── */
592
+ .msg-context-menu {
593
+ position: absolute;
594
+ background: var(--bg-sidebar);
595
+ border: 1px solid var(--border);
596
+ border-radius: var(--radius-md);
597
+ box-shadow: 0 4px 20px rgba(0,0,0,.35);
598
+ z-index: 300;
599
+ min-width: 140px;
600
+ overflow: hidden;
601
+ }
602
+ .msg-context-menu-item {
603
+ display: block;
604
+ width: 100%;
605
+ padding: 0 14px;
606
+ height: 32px;
607
+ background: none;
608
+ border: none;
609
+ cursor: pointer;
610
+ text-align: left;
611
+ color: var(--text-primary);
612
+ font-size: 14px;
613
+ }
614
+ .msg-context-menu-item:hover { background: color-mix(in srgb, var(--accent) 12%, transparent); }
615
+ .msg-context-menu-item--danger { color: var(--color-danger); }
616
+ .msg-context-menu-item--danger:hover { background: color-mix(in srgb, var(--color-danger) 10%, transparent); }
617
+
618
+ /* ── Emoji picker ────────────────────────────────────────────────────────── */
619
+ .emoji-picker {
620
+ max-width: 320px;
621
+ width: 320px;
622
+ background: var(--bg-sidebar);
623
+ border: 1px solid var(--border);
624
+ border-radius: var(--radius-md);
625
+ box-shadow: 0 8px 32px rgba(0,0,0,.4);
626
+ overflow: hidden;
627
+ display: flex;
628
+ flex-direction: column;
629
+ }
630
+ .emoji-picker-search {
631
+ width: 100%;
632
+ padding: 8px 12px;
633
+ border: none;
634
+ border-bottom: 1px solid var(--border);
635
+ background: var(--bg-input);
636
+ color: var(--text-primary);
637
+ font-size: 14px;
638
+ outline: none;
639
+ }
640
+ .emoji-picker-tabs {
641
+ display: flex;
642
+ overflow-x: auto;
643
+ scrollbar-width: none;
644
+ border-bottom: 1px solid var(--border);
645
+ background: var(--bg-sidebar);
646
+ flex-shrink: 0;
647
+ }
648
+ .emoji-picker-tabs::-webkit-scrollbar { display: none; }
649
+ .emoji-picker-tab {
650
+ flex-shrink: 0;
651
+ background: none;
652
+ border: none;
653
+ padding: 6px 8px;
654
+ font-size: 16px;
655
+ cursor: pointer;
656
+ opacity: 0.5;
657
+ border-bottom: 2px solid transparent;
658
+ transition: opacity var(--transition), border-color var(--transition);
659
+ }
660
+ .emoji-picker-tab:hover { opacity: 0.8; }
661
+ .emoji-picker-tab.active { opacity: 1; border-bottom-color: var(--accent); }
662
+ .emoji-picker-grid {
663
+ display: grid;
664
+ grid-template-columns: repeat(8, 1fr);
665
+ gap: 2px;
666
+ padding: 8px;
667
+ overflow-y: auto;
668
+ max-height: 240px;
669
+ }
670
+ .emoji-picker-grid button {
671
+ font-size: 22px;
672
+ background: none;
673
+ border: none;
674
+ cursor: pointer;
675
+ border-radius: var(--radius-sm);
676
+ width: 32px;
677
+ height: 32px;
678
+ display: flex;
679
+ align-items: center;
680
+ justify-content: center;
681
+ padding: 0;
682
+ transition: background var(--transition);
683
+ }
684
+ .emoji-picker-grid button:hover { background: color-mix(in srgb, var(--accent) 15%, transparent); }
685
+
686
+ /* Action sheet emoji picker wrapper */
687
+ .action-sheet-emoji-picker-wrap .emoji-picker {
688
+ max-width: 100%;
689
+ width: 100%;
690
+ box-shadow: none;
691
+ border: none;
692
+ border-radius: 0;
693
+ }
694
+
695
+ .composer {
696
+ margin-top: auto;
697
+ display: flex;
698
+ flex-wrap: wrap;
699
+ align-items: flex-end;
700
+ gap: 8px;
701
+ padding: 20px 20px;
702
+ padding-bottom: calc(12px + env(safe-area-inset-bottom));
703
+ border-top: 1px solid var(--border);
704
+ background: var(--bg-base);
705
+ position: relative;
706
+ }
707
+ .composer textarea {
708
+ flex: 1;
709
+ padding: 10px 14px;
710
+ border-radius: var(--radius-md);
711
+ border: 1px solid var(--border);
712
+ background: var(--bg-input);
713
+ color: var(--text-primary);
714
+ font-family: var(--font-sans);
715
+ resize: none;
716
+ line-height: 1.5;
717
+ height: calc(1.5em + 20px); /* lock to 1 row; iOS Safari expands textarea when placeholder wraps */
718
+ max-height: 160px;
719
+ overflow-y: auto;
720
+ }
721
+ .composer textarea:focus { outline: none; border-color: var(--accent); }
722
+ .composer textarea::placeholder {
723
+ white-space: nowrap;
724
+ overflow: hidden;
725
+ text-overflow: ellipsis;
726
+ }
727
+ /* Urgent toggle button */
728
+ .btn-urgent-toggle {
729
+ background: none;
730
+ border: none;
731
+ cursor: pointer;
732
+ padding: 6px;
733
+ border-radius: var(--radius-sm);
734
+ opacity: 0.4;
735
+ transition: opacity var(--transition), filter var(--transition);
736
+ flex-shrink: 0;
737
+ align-self: center;
738
+ }
739
+ .btn-urgent-toggle:hover { opacity: 0.7; }
740
+ .btn-urgent-toggle.is-urgent { opacity: 1; filter: sepia(1) saturate(4) hue-rotate(10deg); }
741
+
742
+ /* Composer in urgent mode — textarea border turns red */
743
+ .composer.composer-urgent textarea,
744
+ .composer.composer-urgent textarea:focus { border-color: var(--color-danger); }
745
+
746
+ /* ── Load-more sentinel ──────────────────────────────────────────────────── */
747
+ .load-more-sentinel {
748
+ height: 1px;
749
+ margin: 0;
750
+ padding: 0;
751
+ }
752
+
753
+ /* ── Date separator ──────────────────────────────────────────────────────── */
754
+ .date-separator {
755
+ display: flex;
756
+ align-items: center;
757
+ gap: 8px;
758
+ margin: 16px 0 8px;
759
+ color: var(--text-muted);
760
+ font-size: 12px;
761
+ }
762
+ .date-separator::before,
763
+ .date-separator::after {
764
+ content: '';
765
+ flex: 1;
766
+ height: 1px;
767
+ background: var(--border);
768
+ }
769
+ .date-separator-label {
770
+ white-space: nowrap;
771
+ padding: 0 6px;
772
+ font-weight: 500;
773
+ }
774
+
775
+ /* ── @mention inline highlight ───────────────────────────────────────────── */
776
+ .mention { background: color-mix(in srgb, var(--accent) 15%, transparent); color: var(--accent); border-radius: 3px; padding: 0 2px; font-weight: 500; }
777
+ .mention-self { background: color-mix(in srgb, var(--accent) 30%, transparent); font-weight: 600; }
778
+
779
+ /* ── @mention picker popup ───────────────────────────────────────────────── */
780
+ .mention-picker {
781
+ position: absolute;
782
+ bottom: calc(100% + 4px);
783
+ left: 0;
784
+ right: 0;
785
+ background: var(--bg-sidebar);
786
+ border: 1px solid var(--border);
787
+ border-radius: 6px;
788
+ box-shadow: 0 4px 24px rgba(0,0,0,.4);
789
+ z-index: 200;
790
+ max-height: 240px;
791
+ overflow-y: auto;
792
+ }
793
+ .mention-option {
794
+ display: flex;
795
+ align-items: center;
796
+ gap: 8px;
797
+ width: 100%;
798
+ padding: 7px 12px;
799
+ background: none;
800
+ border: none;
801
+ cursor: pointer;
802
+ text-align: left;
803
+ color: var(--text-primary);
804
+ font-size: 14px;
805
+ }
806
+ .mention-option:hover,
807
+ .mention-option.selected { background: color-mix(in srgb, var(--accent) 12%, transparent); }
808
+ .mention-option-name { font-weight: 600; color: var(--text-primary); }
809
+ .mention-option-handle { color: var(--text-muted); font-size: 13px; }
810
+
811
+ /* ── Auth page ───────────────────────────────────────────────────────────── */
812
+ .auth-page { display: flex; align-items: center; justify-content: center; min-height: 100vh; }
813
+ .auth-shell { width: 100%; max-width: 420px; padding: 40px 32px; background: var(--bg-sidebar); border-radius: var(--radius-lg); border: 1px solid var(--border); }
814
+ .auth-brand h1 { font-weight: 800; color: var(--accent); letter-spacing: -0.5px; }
815
+ .auth-brand p { color: var(--text-muted); margin-top: 4px; margin-bottom: 28px; }
816
+ .auth-tabs { display: flex; gap: 4px; margin-bottom: 24px; border-bottom: 1px solid var(--border); }
817
+ .auth-tabs a { background: none; border: none; padding: 8px 16px; font-size: 14px; font-weight: 600; cursor: pointer; color: var(--text-muted); border-bottom: 2px solid transparent; margin-bottom: -1px; transition: color var(--transition), border-color var(--transition); }
818
+ .auth-tabs a[aria-selected="true"] { color: var(--accent); border-bottom-color: var(--accent); }
819
+
820
+ .auth-form { display: flex; flex-direction: column; gap: 16px; }
821
+ .field { display: flex; flex-direction: column; gap: 6px; }
822
+ .field label { font-weight: 600; color: var(--text-secondary); }
823
+ .field input {
824
+ padding: 10px 14px; border-radius: var(--radius-md); border: 1px solid var(--border); background: var(--bg-input); color: var(--text-primary);
825
+ }
826
+ .field input:focus { outline: none; border-color: var(--accent); }
827
+
828
+ /* ── Buttons ─────────────────────────────────────────────────────────────── */
829
+ .btn-primary {
830
+ padding: 10px 20px; border: none; border-radius: var(--radius-md);
831
+ background: var(--accent); color: white; font-weight: 700;
832
+ cursor: pointer; transition: opacity var(--transition);
833
+ }
834
+ .btn-primary:hover { opacity: 0.85; }
835
+ .btn-ghost {
836
+ background: none; border: none; padding: 6px 12px; border-radius: var(--radius-sm);
837
+ color: var(--text-muted); cursor: pointer;
838
+ }
839
+ .btn-ghost:hover { background: var(--bg-hover); color: var(--text-primary); }
840
+ .btn-icon {
841
+ background: none; border: none; padding: 8px; border-radius: var(--radius-sm); color: var(--text-muted); cursor: pointer;
842
+ }
843
+ .btn-icon:hover { background: var(--bg-hover); color: var(--text-primary); }
844
+ .btn-icon.active { color: var(--accent); }
845
+ .btn-call-join {
846
+ padding: 6px 16px; background: var(--color-success); color: white; border: none; border-radius: var(--radius-sm);
847
+ font-weight: 600; cursor: pointer;
848
+ }
849
+ .btn-call-leave {
850
+ padding: 6px 16px; background: var(--color-danger); color: white; border: none; border-radius: var(--radius-sm);
851
+ font-weight: 600; cursor: pointer;
852
+ }
853
+ .btn-send { padding: 10px 18px; background: var(--accent); color: white; border: none; border-radius: var(--radius-md); font-size: 14px; font-weight: 700; cursor: pointer; }
854
+
855
+ /* ── Alerts ──────────────────────────────────────────────────────────────── */
856
+ .alert {
857
+ padding: 10px 14px;
858
+ border-radius: var(--radius-md);
859
+ margin: 0 auto;
860
+ position: absolute;
861
+ top: 3.5rem;
862
+ }
863
+
864
+ .alert-error {
865
+ background: color-mix(in srgb, var(--color-danger) 15%, transparent);
866
+ color: var(--color-danger);
867
+ border: 1px solid var(--color-danger);
868
+ opacity: 1;
869
+ animation: fadeOut 2s forwards;
870
+ }
871
+
872
+ @keyframes fadeOut {
873
+ 0% { opacity: 1;}
874
+ 10% { opacity: .9;}
875
+ 20% { opacity: .8;}
876
+ 30% { opacity: .7;}
877
+ 40% { opacity: .6;}
878
+ 50% { opacity: .5;}
879
+ 60% { opacity: .4;}
880
+ 70% { opacity: .3;}
881
+ 80% { opacity: .2;}
882
+ 90% { opacity: .1;}
883
+ 100% { opacity: 0;}
884
+ }
885
+
886
+ /* ── Theme picker ────────────────────────────────────────────────────────── */
887
+ select#theme-picker {
888
+ padding: 4px 8px; border-radius: var(--radius-sm); border: 1px solid var(--border); background: var(--bg-input); color: var(--text-primary);
889
+ cursor: pointer;
890
+ }
891
+
892
+ /* ── Utilities ───────────────────────────────────────────────────────────── */
893
+ .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; }
894
+ .inline { display: inline; }
895
+ mark { background: color-mix(in srgb, var(--accent) 30%, transparent); color: var(--text-primary); border-radius: 2px; }
896
+
897
+ /* ── Icon stand-ins (replace with SVG sprite or icon font) ───────────────── */
898
+ .icon-menu::before { content: '☰'; }
899
+ .icon-search::before { content: '🔍'; font-size: 14px; }
900
+ .icon-mic::before { content: '🎙'; }
901
+ .icon-cam::before { content: '📷'; }
902
+ .icon-screen::before { content: '🖥'; }
903
+
904
+ .signin {
905
+ display: flex;
906
+ flex-direction: column;
907
+ margin: auto;
908
+ }
909
+
910
+ .signin footer {
911
+ position: fixed;
912
+ bottom: 0;
913
+ margin: auto;
914
+ }
915
+
916
+
917
+ /* ── Back button (mobile only) ───────────────────────────────────────────── */
918
+ .btn-back-mobile {
919
+ display: none;
920
+ }
921
+
922
+ /* ── Mobile ──────────────────────────────────────────────────────────────── */
923
+ @media (max-width: 700px) {
924
+ /* Both panels are full-screen and fixed; only one is on-screen at a time.
925
+ No z-index needed — they slide in opposite directions so they never overlap. */
926
+
927
+ /* Prevent iOS from scrolling the body when a touch starts on non-scrolling
928
+ areas. touch-action: none stops iOS treating these as scroll initiators
929
+ without affecting clicks/taps or the dvh keyboard-resize behaviour. */
930
+ .chat-header { touch-action: none; }
931
+ .composer { touch-action: pan-y; }
932
+
933
+ /* dvh = dynamic viewport height — shrinks when the keyboard opens,
934
+ so the panels resize and the composer/footer stay above the keyboard. */
935
+ .sidebar {
936
+ position: fixed;
937
+ top: 0;
938
+ left: 0;
939
+ right: 0;
940
+ height: 100dvh;
941
+ width: 100%;
942
+ transform: translateX(0);
943
+ transition: transform var(--transition);
944
+ overflow-y: auto;
945
+ }
946
+
947
+ .main-content {
948
+ position: fixed;
949
+ top: 0;
950
+ left: 0;
951
+ right: 0;
952
+ height: 100dvh;
953
+ width: 100%;
954
+ transform: translateX(100%);
955
+ transition: transform var(--transition);
956
+ overflow: hidden;
957
+ }
958
+
959
+ /* On channel pages (.main-content exists): panel visible, sidebar off-screen left.
960
+ :has() fires based on DOM structure — no JS class needed for the default state. */
961
+ body:has(.main-content) .sidebar {
962
+ transform: translateX(-100%);
963
+ }
964
+ body:has(.main-content) .main-content {
965
+ transform: translateX(0);
966
+ }
967
+
968
+ /* User tapped back / swiped: show sidebar, hide panel */
969
+ body.sidebar-open .sidebar {
970
+ transform: translateX(0);
971
+ }
972
+ body.sidebar-open .main-content {
973
+ transform: translateX(100%);
974
+ }
975
+
976
+ /* Show back button */
977
+ .btn-back-mobile {
978
+ display: flex;
979
+ align-items: center;
980
+ justify-content: center;
981
+ background: none;
982
+ border: none;
983
+ cursor: pointer;
984
+ color: var(--text-muted);
985
+ padding: 6px 10px 6px 4px;
986
+ font-size: 1.2rem;
987
+ border-radius: var(--radius-sm);
988
+ flex-shrink: 0;
989
+ }
990
+ .btn-back-mobile:hover { color: var(--text-primary); }
991
+
992
+ /* Actions button: no hover on touch — always visible for own messages */
993
+ .btn-msg-actions {
994
+ opacity: 1;
995
+ pointer-events: auto;
996
+ }
997
+ }
998
+
999
+ /* ── Tile panel ───────────────────────────────────────────────────────────── */
1000
+
1001
+ .tile-panel {
1002
+ display: none;
1003
+ flex-direction: column;
1004
+ background: var(--bg-sidebar);
1005
+ border-left: 1px solid var(--border);
1006
+ overflow: hidden;
1007
+ }
1008
+ .tile-panel.active { display: flex; }
1009
+
1010
+ .tile-panel-header {
1011
+ display: flex;
1012
+ align-items: center;
1013
+ padding: 6px 10px;
1014
+ gap: 6px;
1015
+ border-bottom: 1px solid var(--border);
1016
+ background: var(--bg-topbar);
1017
+ flex-shrink: 0;
1018
+ }
1019
+ .tile-panel-title {
1020
+ flex: 1;
1021
+ font-size: 12px;
1022
+ font-weight: 600;
1023
+ color: var(--text-muted);
1024
+ text-transform: uppercase;
1025
+ letter-spacing: 0.6px;
1026
+ }
1027
+
1028
+ /* ── Mode A: sidebar (desktop ≥ 1025px) ─────────────────────────────────── */
1029
+
1030
+ @media (min-width: 1025px) {
1031
+ .main-content.has-call {
1032
+ flex-direction: row;
1033
+ }
1034
+ .main-content.has-call .chat-panel {
1035
+ flex: 1;
1036
+ min-width: 0;
1037
+ height: 100%;
1038
+ }
1039
+ .tile-panel {
1040
+ width: 360px;
1041
+ flex-shrink: 0;
1042
+ height: 100vh;
1043
+ overflow-y: auto;
1044
+ }
1045
+ .tile-panel.collapsed {
1046
+ width: 40px;
1047
+ overflow: hidden;
1048
+ }
1049
+ .tile-panel.collapsed .tile-grid,
1050
+ .tile-panel.collapsed .tile-panel-title { display: none; }
1051
+ .tile-panel.collapsed .tile-panel-header {
1052
+ flex-direction: column;
1053
+ padding: 6px 4px;
1054
+ height: 100%;
1055
+ justify-content: flex-start;
1056
+ border-left: none;
1057
+ }
1058
+ }
1059
+
1060
+ /* ── Mode B: overlay (mobile < 1025px) ──────────────────────────────────── */
1061
+
1062
+ /* Only tablet needs position:relative — mobile .main-content is already
1063
+ position:fixed which also creates a containing block for the absolute tile panel. */
1064
+ @media (min-width: 701px) and (max-width: 1024px) {
1065
+ .main-content.has-call {
1066
+ position: relative;
1067
+ }
1068
+ }
1069
+
1070
+ @media (max-width: 1024px) {
1071
+ .tile-panel {
1072
+ position: absolute;
1073
+ top: 0; right: 0;
1074
+ width: min(220px, 45vw);
1075
+ max-height: 40vh;
1076
+ z-index: 20;
1077
+ border-left: 1px solid var(--border);
1078
+ border-bottom: 1px solid var(--border);
1079
+ border-radius: 0 0 0 var(--radius-md);
1080
+ background: color-mix(in srgb, var(--bg-sidebar) 92%, transparent);
1081
+ backdrop-filter: blur(4px);
1082
+ -webkit-backdrop-filter: blur(4px);
1083
+ overflow-y: auto;
1084
+ }
1085
+ .tile-panel.collapsed {
1086
+ width: auto;
1087
+ height: auto;
1088
+ max-height: none;
1089
+ border-radius: var(--radius-md);
1090
+ top: 8px; right: 8px;
1091
+ }
1092
+ .tile-panel.collapsed .tile-grid,
1093
+ .tile-panel.collapsed .tile-panel-title { display: none; }
1094
+ .tile-panel.collapsed .tile-panel-header {
1095
+ padding: 4px 8px;
1096
+ border-bottom: none;
1097
+ }
1098
+ }
1099
+
1100
+ /* ── Tile grid (inside .tile-panel in both modes) ───────────────────────── */
1101
+
1102
+ .tile-grid {
1103
+ display: flex;
1104
+ flex-wrap: wrap;
1105
+ gap: 6px;
1106
+ padding: 8px;
1107
+ flex: 1;
1108
+ overflow-y: auto;
1109
+ align-content: flex-start;
1110
+ }
1111
+
1112
+ /* Collapsed: hide video, keep audio alive */
1113
+ .tile-panel.collapsed .tile-grid video { visibility: hidden; }
1114
+
1115
+ /* Stream tiles: fill panel width, natural height */
1116
+ .stream-tile {
1117
+ position: relative;
1118
+ border-radius: var(--radius-sm);
1119
+ overflow: hidden;
1120
+ background: var(--bg-sidebar);
1121
+ flex: 1 1 100%;
1122
+ }
1123
+ .stream-tile video { width: 100%; height: auto; display: block; }
1124
+ .stream-tile .tile-label {
1125
+ position: absolute; top: 4px; left: 6px;
1126
+ font-size: 11px; font-weight: 600; color: white;
1127
+ text-shadow: 0 1px 3px rgba(0,0,0,0.6); pointer-events: none;
1128
+ }
1129
+ .tile-capture-wrap {
1130
+ position: absolute; top: 4px; right: 4px; z-index: 2;
1131
+ display: flex; gap: 4px; align-items: center;
1132
+ opacity: 0; transition: opacity 0.15s;
1133
+ }
1134
+ .stream-tile:hover .tile-capture-wrap { opacity: 1; }
1135
+ .stream-tile.pinned-tile .tile-capture-wrap { opacity: 1; }
1136
+ .tile-pin, .tile-capture {
1137
+ background: rgba(0,0,0,0.5); border: none;
1138
+ border-radius: var(--radius-sm); color: white;
1139
+ font-size: 13px; padding: 2px 6px;
1140
+ cursor: pointer; display: block;
1141
+ }
1142
+ .stream-tile.pinned-tile .tile-pin { color: #ffd700; }
1143
+ .tile-capture-menu {
1144
+ position: absolute; right: 0; top: calc(100% + 3px);
1145
+ background: rgba(0,0,0,0.75); border-radius: var(--radius-sm);
1146
+ display: flex; flex-direction: column; gap: 1px; padding: 4px;
1147
+ }
1148
+ .tile-capture-menu[hidden] { display: none; }
1149
+ .tile-capture-opt {
1150
+ background: none; border: none; color: white;
1151
+ font-size: 12px; padding: 3px 10px;
1152
+ cursor: pointer; text-align: left;
1153
+ border-radius: var(--radius-sm); white-space: nowrap;
1154
+ }
1155
+ .tile-capture-opt:hover { background: rgba(255,255,255,0.15); }
1156
+ .tile-countdown {
1157
+ position: absolute; inset: 0;
1158
+ display: flex; align-items: center; justify-content: center;
1159
+ font-size: 64px; font-weight: 700; color: white;
1160
+ text-shadow: 0 2px 10px rgba(0,0,0,0.8);
1161
+ pointer-events: none; z-index: 3;
1162
+ }
1163
+ .tile-countdown[hidden] { display: none; }
1164
+
1165
+ /* 5+ tiles: avatar strip */
1166
+ .tile-grid.avatars-only .stream-tile video { display: none; }
1167
+ .tile-grid.avatars-only .stream-tile {
1168
+ flex: 0 0 36px; height: 36px; border-radius: 50%; overflow: hidden;
1169
+ }
1170
+
1171
+ /* Pinned tile: move to top, single column always */
1172
+ .tile-grid.pinned .stream-tile.pinned-tile { order: -1; }
1173
+
1174
+ /* ── Call: status row (shown when a call is active, user hasn't joined) ──── */
1175
+ .call-status {
1176
+ display: flex;
1177
+ align-items: center;
1178
+ gap: 10px;
1179
+ padding: 6px 16px;
1180
+ background: var(--bg-topbar);
1181
+ border-bottom: 1px solid var(--border);
1182
+ font-size: 13px;
1183
+ }
1184
+ .call-status[hidden] { display: none; }
1185
+ .call-status-dot {
1186
+ width: 8px; height: 8px; border-radius: 50%;
1187
+ background: var(--color-success, #3ba55c); flex-shrink: 0;
1188
+ }
1189
+ .call-status-info { flex: 1; color: var(--text-secondary); }
1190
+ .call-status-avatars { display: flex; }
1191
+ .call-status-avatar {
1192
+ width: 22px; height: 22px; border-radius: 50%;
1193
+ background: var(--accent); border: 2px solid var(--bg-topbar);
1194
+ display: inline-flex; align-items: center; justify-content: center;
1195
+ font-size: 10px; font-weight: 700; color: white; margin-left: -4px;
1196
+ }
1197
+ .call-status-avatar:first-child { margin-left: 0; }
1198
+
1199
+ /* ── Call: in-call controls bar ─────────────────────────────────────────── */
1200
+ .call-controls-bar {
1201
+ display: none;
1202
+ align-items: center;
1203
+ gap: 8px;
1204
+ padding: 6px 16px;
1205
+ background: var(--bg-topbar);
1206
+ border-bottom: 1px solid var(--border);
1207
+ }
1208
+ .call-controls-bar.active { display: flex; }
1209
+ .call-peer-count { color: var(--text-muted); font-size: 13px; }
1210
+ .btn-leave {
1211
+ margin-left: auto;
1212
+ padding: 4px 12px;
1213
+ background: var(--color-danger, #ed4245);
1214
+ color: white; border: none; border-radius: var(--radius-sm);
1215
+ font-weight: 600; cursor: pointer; font-size: 13px;
1216
+ }
1217
+ .btn-leave:hover { opacity: 0.85; }
1218
+
1219
+ /* ── Device picker panel ──────────────────────────────────────────────────── */
1220
+ .device-picker {
1221
+ display: none;
1222
+ flex-direction: column;
1223
+ gap: 12px;
1224
+ padding: 12px 16px;
1225
+ background: var(--bg-topbar);
1226
+ border-bottom: 1px solid var(--border);
1227
+ }
1228
+ .device-picker.open { display: flex; }
1229
+
1230
+ .device-picker-row {
1231
+ display: flex;
1232
+ align-items: center;
1233
+ gap: 10px;
1234
+ font-size: 13px;
1235
+ color: var(--text-secondary);
1236
+ }
1237
+ .device-picker-row label { width: 80px; flex-shrink: 0; font-weight: 600; }
1238
+ .device-picker-row select {
1239
+ flex: 1;
1240
+ padding: 5px 8px;
1241
+ border-radius: var(--radius-sm);
1242
+ border: 1px solid var(--border);
1243
+ background: var(--bg-input);
1244
+ color: var(--text-primary);
1245
+ font-size: 13px;
1246
+ }
1247
+ .device-picker-row video {
1248
+ width: 80px; height: 45px;
1249
+ border-radius: var(--radius-sm);
1250
+ object-fit: cover;
1251
+ background: var(--bg-sidebar);
1252
+ flex-shrink: 0;
1253
+ }
1254
+ .device-picker-row canvas {
1255
+ width: 80px; height: 12px;
1256
+ border-radius: 3px;
1257
+ background: var(--bg-sidebar);
1258
+ flex-shrink: 0;
1259
+ }
1260
+ .device-picker-footer {
1261
+ display: flex;
1262
+ justify-content: flex-end;
1263
+ gap: 8px;
1264
+ }
1265
+
1266
+ /* Warning indicator on gear button */
1267
+ .btn-icon.device-warning { color: var(--color-danger); }
1268
+ .btn-icon.device-warning::after {
1269
+ content: '!';
1270
+ font-size: 9px;
1271
+ font-weight: 700;
1272
+ color: var(--color-danger);
1273
+ vertical-align: super;
1274
+ }
1275
+
1276
+ /* Device disconnected toast */
1277
+ .device-warning-toast {
1278
+ position: fixed;
1279
+ bottom: 80px;
1280
+ left: 50%;
1281
+ transform: translateX(-50%);
1282
+ background: var(--color-danger);
1283
+ color: white;
1284
+ padding: 8px 16px;
1285
+ border-radius: var(--radius-md);
1286
+ font-size: 13px;
1287
+ font-weight: 600;
1288
+ z-index: 200;
1289
+ pointer-events: none;
1290
+ animation: toast-in 0.2s ease;
1291
+ }
1292
+ @keyframes toast-in {
1293
+ from { opacity: 0; transform: translateX(-50%) translateY(8px); }
1294
+ to { opacity: 1; transform: translateX(-50%) translateY(0); }
1295
+ }
1296
+
1297
+ /* ── Call: mini-bar (persists while navigating away during a call) ────────── */
1298
+ .call-mini-bar {
1299
+ display: none;
1300
+ align-items: center;
1301
+ gap: 8px;
1302
+ padding: 8px 12px;
1303
+ background: var(--bg-topbar);
1304
+ border-top: 1px solid var(--border);
1305
+ font-size: 13px;
1306
+ }
1307
+ .call-mini-bar.active { display: flex; }
1308
+ .call-mini-bar-channel {
1309
+ flex: 1; font-weight: 600; color: var(--text-primary);
1310
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
1311
+ cursor: pointer;
1312
+ }
1313
+ .call-mini-bar-channel:hover { color: var(--accent); }
1314
+
1315
+ @media (max-width: 1024px) {
1316
+ .call-mini-bar {
1317
+ position: sticky; bottom: 0; z-index: 10;
1318
+ border-bottom: 1px solid var(--border);
1319
+ }
1320
+ }
1321
+
1322
+ /* ── Call: sidebar badge (shows N in call on channel row) ─────────────────── */
1323
+ .channel-item .call-badge {
1324
+ display: none;
1325
+ margin-left: auto;
1326
+ font-size: 11px; font-weight: 700;
1327
+ color: var(--color-success, #3ba55c);
1328
+ align-items: center; gap: 3px;
1329
+ }
1330
+ .channel-item.call-active .call-badge { display: flex; }
1331
+ .channel-item.call-active .call-badge::before {
1332
+ content: '';
1333
+ width: 6px; height: 6px; border-radius: 50%;
1334
+ background: var(--color-success, #3ba55c);
1335
+ display: inline-block;
1336
+ }
1337
+
1338
+ /* ── Admin pages ──────────────────────────────────────────────────────────── */
1339
+
1340
+ .admin-topbar {
1341
+ display: flex;
1342
+ align-items: center;
1343
+ gap: 12px;
1344
+ padding: 0 20px;
1345
+ height: 52px;
1346
+ padding-top: env(safe-area-inset-top);
1347
+ height: calc(52px + env(safe-area-inset-top));
1348
+ background: var(--bg-topbar);
1349
+ border-bottom: 1px solid var(--border);
1350
+ flex-shrink: 0;
1351
+ }
1352
+
1353
+ .admin-topbar-brand {
1354
+ font-weight: 700;
1355
+ font-size: 15px;
1356
+ color: var(--text-primary);
1357
+ text-decoration: none;
1358
+ }
1359
+ .admin-topbar-brand:hover { color: var(--accent); }
1360
+
1361
+ .admin-topbar-user {
1362
+ font-size: 13px;
1363
+ color: var(--text-muted);
1364
+ }
1365
+
1366
+ .admin-topbar-actions {
1367
+ display: flex;
1368
+ align-items: center;
1369
+ gap: 8px;
1370
+ margin-left: auto;
1371
+ }
1372
+
1373
+ body:has(.admin-topbar) main {
1374
+ display: flex;
1375
+ flex-direction: column;
1376
+ height: 100%;
1377
+ overflow-y: auto;
1378
+ }
1379
+
1380
+ .admin-page {
1381
+ max-width: 900px;
1382
+ margin: 0 auto;
1383
+ padding: 32px 24px;
1384
+ }
1385
+
1386
+ .admin-nav {
1387
+ display: flex;
1388
+ gap: 4px;
1389
+ margin-bottom: 32px;
1390
+ border-bottom: 1px solid var(--border);
1391
+ padding-bottom: 12px;
1392
+ }
1393
+
1394
+ .admin-nav-link {
1395
+ font-size: 14px;
1396
+ color: var(--text-secondary);
1397
+ text-decoration: none;
1398
+ padding: 6px 14px;
1399
+ border-radius: 6px 6px 0 0;
1400
+ }
1401
+ .admin-nav-link:hover { color: var(--text-primary); }
1402
+ .admin-nav-link.active {
1403
+ color: var(--text-primary);
1404
+ background: var(--bg-input);
1405
+ font-weight: 600;
1406
+ }
1407
+
1408
+ .admin-section {
1409
+ margin-bottom: 40px;
1410
+ }
1411
+ .admin-section h2 {
1412
+ font-size: 15px;
1413
+ font-weight: 600;
1414
+ margin-bottom: 12px;
1415
+ color: var(--text-secondary);
1416
+ text-transform: uppercase;
1417
+ letter-spacing: 0.05em;
1418
+ }
1419
+
1420
+ .admin-notice {
1421
+ padding: 12px 16px;
1422
+ border-radius: 8px;
1423
+ margin-bottom: 24px;
1424
+ font-size: 14px;
1425
+ line-height: 1.5;
1426
+ }
1427
+ .admin-notice--success {
1428
+ background: color-mix(in srgb, var(--color-success, #3ba55c) 15%, transparent);
1429
+ border: 1px solid color-mix(in srgb, var(--color-success, #3ba55c) 40%, transparent);
1430
+ color: var(--text-primary);
1431
+ }
1432
+
1433
+ .admin-empty {
1434
+ color: var(--text-secondary);
1435
+ font-size: 14px;
1436
+ font-style: italic;
1437
+ }
1438
+
1439
+ .admin-hint {
1440
+ font-size: 13px;
1441
+ color: var(--text-secondary);
1442
+ margin-top: 8px;
1443
+ }
1444
+
1445
+ .admin-form {
1446
+ display: flex;
1447
+ flex-direction: column;
1448
+ gap: 12px;
1449
+ max-width: 480px;
1450
+ }
1451
+ .admin-form--inline {
1452
+ flex-direction: row;
1453
+ align-items: center;
1454
+ max-width: none;
1455
+ margin-bottom: 16px;
1456
+ }
1457
+
1458
+ .form-row {
1459
+ display: flex;
1460
+ flex-direction: column;
1461
+ gap: 4px;
1462
+ }
1463
+ .form-row label {
1464
+ font-size: 13px;
1465
+ font-weight: 500;
1466
+ color: var(--text-secondary);
1467
+ }
1468
+ .form-row input[type="text"],
1469
+ .form-row input[type="password"],
1470
+ .form-row input[type="number"] {
1471
+ padding: 7px 10px;
1472
+ border-radius: 6px;
1473
+ border: 1px solid var(--border);
1474
+ background: var(--bg-input);
1475
+ color: var(--text-primary);
1476
+ font-size: 14px;
1477
+ }
1478
+
1479
+ .checkbox-label {
1480
+ display: flex;
1481
+ align-items: center;
1482
+ gap: 8px;
1483
+ font-size: 14px;
1484
+ cursor: pointer;
1485
+ }
1486
+
1487
+ .token-display {
1488
+ display: inline-block;
1489
+ margin-top: 8px;
1490
+ padding: 8px 12px;
1491
+ background: var(--bg-input);
1492
+ border-radius: 6px;
1493
+ font-family: monospace;
1494
+ font-size: 13px;
1495
+ word-break: break-all;
1496
+ user-select: all;
1497
+ }
1498
+
1499
+ .invite-link {
1500
+ display: inline-block;
1501
+ margin-top: 8px;
1502
+ font-size: 14px;
1503
+ word-break: break-all;
1504
+ color: var(--accent);
1505
+ user-select: all;
1506
+ }
1507
+
1508
+ .data-table {
1509
+ width: 100%;
1510
+ border-collapse: collapse;
1511
+ font-size: 14px;
1512
+ }
1513
+ .data-table th {
1514
+ text-align: left;
1515
+ padding: 8px 12px;
1516
+ color: var(--text-secondary);
1517
+ font-weight: 500;
1518
+ font-size: 12px;
1519
+ text-transform: uppercase;
1520
+ letter-spacing: 0.05em;
1521
+ border-bottom: 1px solid var(--border);
1522
+ }
1523
+ .data-table td {
1524
+ padding: 10px 12px;
1525
+ border-bottom: 1px solid var(--border);
1526
+ color: var(--text-primary);
1527
+ }
1528
+ .data-table tr:last-child td { border-bottom: none; }
1529
+
1530
+ .tag {
1531
+ display: inline-block;
1532
+ padding: 2px 8px;
1533
+ border-radius: 100px;
1534
+ font-size: 11px;
1535
+ font-weight: 600;
1536
+ text-transform: uppercase;
1537
+ letter-spacing: 0.04em;
1538
+ }
1539
+ .tag--admin { background: color-mix(in srgb, var(--accent) 20%, transparent); color: var(--accent); }
1540
+ .tag--bot { background: color-mix(in srgb, #9b59b6 20%, transparent); color: #9b59b6; }
1541
+ .tag--warn { background: color-mix(in srgb, #f39c12 20%, transparent); color: #f39c12; }
1542
+
1543
+ .btn-danger { color: var(--color-danger, #e74c3c); }
1544
+ .btn-danger:hover { background: color-mix(in srgb, var(--color-danger, #e74c3c) 15%, transparent); }
1545
+
1546
+ form.inline { display: inline; }
1547
+
1548
+ /* ── File uploads ─────────────────────────────────────────────────────────── */
1549
+
1550
+ .attachment-chips {
1551
+ width: 100%;
1552
+ order: -1;
1553
+ display: flex;
1554
+ flex-wrap: wrap;
1555
+ gap: 6px;
1556
+ padding: 4px 0 2px;
1557
+ }
1558
+ .attachment-chips:empty { display: none; }
1559
+ .attachment-chips[hidden] { display: none; }
1560
+
1561
+ .attachment-chip {
1562
+ display: inline-flex;
1563
+ align-items: center;
1564
+ gap: 4px;
1565
+ padding: 2px 8px;
1566
+ border-radius: 999px;
1567
+ background: var(--bg-input);
1568
+ border: 1px solid var(--border);
1569
+ font-size: 12px;
1570
+ color: var(--text-secondary);
1571
+ max-width: 200px;
1572
+ }
1573
+ .attachment-chip-name {
1574
+ overflow: hidden;
1575
+ text-overflow: ellipsis;
1576
+ white-space: nowrap;
1577
+ }
1578
+ .attachment-chip-remove {
1579
+ background: none;
1580
+ border: none;
1581
+ cursor: pointer;
1582
+ color: var(--text-muted);
1583
+ font-size: 14px;
1584
+ line-height: 1;
1585
+ padding: 0 2px;
1586
+ }
1587
+ .attachment-chip-remove:hover { color: var(--color-danger, #e74c3c); }
1588
+ .attachment-chip-error {
1589
+ background: color-mix(in srgb, var(--color-danger, #e74c3c) 15%, transparent);
1590
+ border-color: var(--color-danger, #e74c3c);
1591
+ color: var(--color-danger, #e74c3c);
1592
+ max-width: none;
1593
+ }
1594
+
1595
+ /* Drop overlay shown when dragging a file over the composer */
1596
+ .drop-overlay {
1597
+ position: absolute;
1598
+ inset: 0;
1599
+ background: color-mix(in srgb, var(--accent) 15%, transparent);
1600
+ border: 2px dashed var(--accent);
1601
+ border-radius: var(--radius-md);
1602
+ display: flex;
1603
+ align-items: center;
1604
+ justify-content: center;
1605
+ font-weight: 600;
1606
+ color: var(--accent);
1607
+ pointer-events: none;
1608
+ z-index: 10;
1609
+ }
1610
+ .drop-overlay[hidden] { display: none; }
1611
+
1612
+ /* Attach button */
1613
+ .btn-attach {
1614
+ flex-shrink: 0;
1615
+ font-size: 18px;
1616
+ }
1617
+
1618
+ /* Attachment rendering inside messages */
1619
+ .attachment-image-link { display: block; margin-top: 6px; }
1620
+ .attachment-image {
1621
+ max-width: min(400px, 100%);
1622
+ max-height: 300px;
1623
+ border-radius: var(--radius-md);
1624
+ object-fit: contain;
1625
+ display: block;
1626
+ }
1627
+ .attachment-file {
1628
+ display: inline-flex;
1629
+ align-items: center;
1630
+ gap: 6px;
1631
+ margin-top: 6px;
1632
+ padding: 6px 12px;
1633
+ border-radius: var(--radius-md);
1634
+ background: var(--bg-input);
1635
+ border: 1px solid var(--border);
1636
+ text-decoration: none;
1637
+ color: var(--text-secondary);
1638
+ font-size: 13px;
1639
+ max-width: 340px;
1640
+ }
1641
+ .attachment-file:hover { background: var(--bg-hover); color: var(--text-primary); }
1642
+ .attachment-file-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1643
+ .attachment-file-size { color: var(--text-muted); flex-shrink: 0; }
1644
+
1645
+ /* Sidebar: channel link highlighted when a file is hovered over it */
1646
+ .channel-link.file-drop-hover {
1647
+ background: color-mix(in srgb, var(--accent) 20%, transparent);
1648
+ border-radius: var(--radius-sm);
1649
+ outline: 2px dashed var(--accent);
1650
+ outline-offset: -2px;
1651
+ }
1652
+
1653
+ /* Sidebar toast notification */
1654
+ .sidebar-toast {
1655
+ position: absolute;
1656
+ bottom: 60px;
1657
+ left: 12px;
1658
+ right: 12px;
1659
+ padding: 8px 12px;
1660
+ background: var(--bg-topbar);
1661
+ border: 1px solid var(--border);
1662
+ border-radius: var(--radius-md);
1663
+ font-size: 13px;
1664
+ color: var(--text-primary);
1665
+ z-index: 200;
1666
+ animation: toast-in 0.15s ease;
1667
+ }
1668
+ @keyframes toast-in {
1669
+ from { opacity: 0; transform: translateY(8px); }
1670
+ to { opacity: 1; transform: translateY(0); }
1671
+ }
1672
+
1673
+ /* ── Message editing ──────────────────────────────────────────────────────── */
1674
+
1675
+ .message-edited {
1676
+ font-size: 11px;
1677
+ color: var(--text-muted);
1678
+ margin-left: 4px;
1679
+ }
1680
+
1681
+ .message-edit-input {
1682
+ width: 100%;
1683
+ min-height: 60px;
1684
+ padding: 6px 8px;
1685
+ font: inherit;
1686
+ font-size: inherit;
1687
+ line-height: 1.5;
1688
+ border: 1px solid var(--border);
1689
+ border-radius: var(--radius-sm);
1690
+ background: var(--bg-input, var(--bg-sidebar));
1691
+ color: var(--text-primary);
1692
+ resize: vertical;
1693
+ box-sizing: border-box;
1694
+ grid-column: 1 / -1;
1695
+ }
1696
+
1697
+ .message-edit-toolbar {
1698
+ display: flex;
1699
+ gap: 6px;
1700
+ margin-top: 6px;
1701
+ grid-column: 1 / -1;
1702
+ }
1703
+ .message-edit-toolbar .btn-primary {
1704
+ padding: 6px 14px;
1705
+ font-size: 14px;
1706
+ font-weight: 600;
1707
+ }
1708
+ .message-edit-toolbar .btn-ghost {
1709
+ font-size: 14px;
1710
+ }
1711
+
1712
+ /* … actions button on message hover */
1713
+ .btn-msg-actions {
1714
+ position: absolute;
1715
+ top: 2px;
1716
+ right: 4px;
1717
+ font-size: 16px;
1718
+ line-height: 1;
1719
+ opacity: 0;
1720
+ pointer-events: none;
1721
+ transition: opacity 0.1s;
1722
+ }
1723
+ article.message:hover .btn-msg-actions { opacity: 1; pointer-events: auto; }
1724
+ article.message { position: relative; }
1725
+
1726
+ /* Context menu */
1727
+ .msg-context-menu {
1728
+ position: absolute;
1729
+ z-index: 300;
1730
+ background: var(--bg-sidebar);
1731
+ border: 1px solid var(--border);
1732
+ border-radius: var(--radius-md);
1733
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
1734
+ min-width: 120px;
1735
+ padding: 4px 0;
1736
+ }
1737
+ .msg-context-menu-item {
1738
+ display: block;
1739
+ width: 100%;
1740
+ padding: 6px 14px;
1741
+ text-align: left;
1742
+ background: none;
1743
+ border: none;
1744
+ color: var(--text-primary);
1745
+ font-size: 13px;
1746
+ cursor: pointer;
1747
+ }
1748
+ .msg-context-menu-item:hover { background: color-mix(in srgb, var(--accent) 15%, transparent); }
1749
+ .msg-context-menu-item--danger { color: var(--color-danger, #e74c3c); }
1750
+ .msg-context-menu-item--danger:hover { background: color-mix(in srgb, var(--color-danger, #e74c3c) 12%, transparent); }
1751
+
1752
+ /* Bottom sheet (mobile long-press) */
1753
+ .action-sheet {
1754
+ position: fixed;
1755
+ bottom: 0;
1756
+ left: 0;
1757
+ right: 0;
1758
+ background: var(--bg-sidebar);
1759
+ border-top: 1px solid var(--border);
1760
+ border-radius: 12px 12px 0 0;
1761
+ z-index: 400;
1762
+ transform: translateY(100%);
1763
+ transition: transform 220ms ease-out;
1764
+ padding-bottom: env(safe-area-inset-bottom, 0);
1765
+ }
1766
+ .action-sheet.open { transform: translateY(0); }
1767
+ .action-sheet-handle {
1768
+ width: 40px;
1769
+ height: 4px;
1770
+ background: var(--border);
1771
+ border-radius: 2px;
1772
+ margin: 10px auto 6px;
1773
+ }
1774
+ .action-sheet-body { padding: 8px 0 16px; }
1775
+ .action-sheet-item {
1776
+ display: block;
1777
+ width: 100%;
1778
+ padding: 12px 20px;
1779
+ text-align: left;
1780
+ background: none;
1781
+ border: none;
1782
+ color: var(--text-primary);
1783
+ font-size: 15px;
1784
+ cursor: pointer;
1785
+ }
1786
+ .action-sheet-item:hover { background: color-mix(in srgb, var(--accent) 15%, transparent); }