diff --git a/packages/web/src/app/App.tsx b/packages/web/src/app/App.tsx
index 558fc63..20e9c3b 100644
--- a/packages/web/src/app/App.tsx
+++ b/packages/web/src/app/App.tsx
@@ -4,7 +4,7 @@ import type { Conversation, Message, MediaRef, PublicUser, User } from '@altrica
import { heartbeat, getAvatarUploadUrl, uploadToUrl, setAvatar } from '@altricade/core/api';
import { SessionProvider, useSession } from '../entities/session';
import { AuthForm } from '../features/auth';
-import { RealtimeProvider, useRealtime } from '../features/realtime';
+import { RealtimeProvider } from '../features/realtime';
import { useConversations, ConversationSidebar } from '../features/conversations';
import { ContactsPanel } from '../features/contacts';
import { ChatView } from '../features/messaging';
@@ -12,8 +12,21 @@ import { NotificationsProvider, useNotifications, unregisterWebPush } from '../f
import { apiConfig } from '../shared/api';
import { useTheme } from '../shared/theme';
import type { ThemePreference } from '../shared/theme';
+import {
+ SunIcon,
+ MoonIcon,
+ MonitorIcon,
+ PlusIcon,
+ CloseIcon,
+ LogOutIcon,
+ UsersIcon,
+} from '../shared/ui';
-const PREFERENCES: readonly ThemePreference[] = ['light', 'dark', 'system'];
+const PREFERENCES: readonly { value: ThemePreference; label: string; icon: ReactElement }[] = [
+ { value: 'light', label: 'Light', icon: },
+ { value: 'dark', label: 'Dark', icon: },
+ { value: 'system', label: 'System', icon: },
+];
const mediaLabel = (media: MediaRef | null): string => {
if (media === null) {
@@ -34,23 +47,85 @@ const mediaLabel = (media: MediaRef | null): string => {
const ThemeSwitch = (): ReactElement => {
const { preference, setPreference } = useTheme();
return (
-
+
{PREFERENCES.map((option) => (
))}
);
};
+const GroupComposer = ({
+ onCreate,
+}: {
+ onCreate: (title: string, members: string[]) => Promise
;
+}): ReactElement => {
+ const [title, setTitle] = useState('');
+ const [members, setMembers] = useState('');
+ const [error, setError] = useState(null);
+
+ const submit = async (event: SyntheticEvent): Promise => {
+ event.preventDefault();
+ const trimmed = title.trim();
+ if (trimmed === '') {
+ return;
+ }
+ const list = members
+ .split(',')
+ .map((name) => name.trim())
+ .filter((name) => name.length > 0);
+ setError(null);
+ try {
+ await onCreate(trimmed, list);
+ setTitle('');
+ setMembers('');
+ } catch {
+ setError('Could not create group');
+ }
+ };
+
+ return (
+
+
New group
+
+ {error !== null ?
{error}
: null}
+
+ );
+};
+
const Dashboard = ({ user, onLogout }: { user: User; onLogout: () => void }): ReactElement => {
const me: PublicUser = {
id: user.id,
@@ -59,9 +134,9 @@ const Dashboard = ({ user, onLogout }: { user: User; onLogout: () => void }): Re
avatarUrl: user.avatarUrl,
};
const { updateUser } = useSession();
- const { state } = useRealtime();
const { notify, setOpener } = useNotifications();
const [current, setCurrent] = useState(null);
+ const [composing, setComposing] = useState(false);
const convRef = useRef([]);
// Toast (or OS notification when hidden) for messages arriving in other chats.
@@ -86,15 +161,20 @@ const Dashboard = ({ user, onLogout }: { user: User; onLogout: () => void }): Re
);
convRef.current = conversations;
+ const openConversation = useCallback((conversation: Conversation): void => {
+ setCurrent(conversation);
+ setComposing(false);
+ }, []);
+
// Let notification taps (in-app toast or SW message) open the right chat.
useEffect(() => {
setOpener((conversationId) => {
const conversation = convRef.current.find((item) => item.id === conversationId);
if (conversation !== undefined) {
- setCurrent(conversation);
+ openConversation(conversation);
}
});
- }, [setOpener]);
+ }, [setOpener, openConversation]);
// Deep-link: /?conversation= (from a push opened in a fresh tab).
useEffect(() => {
@@ -135,57 +215,89 @@ const Dashboard = ({ user, onLogout }: { user: User; onLogout: () => void }): Re
};
}, []);
+ const startDirectAndOpen = async (username: string): Promise => {
+ openConversation(await startDirect(username));
+ };
+
+ const createGroupAndOpen = async (title: string, members: string[]): Promise => {
+ openConversation(await createGroupChat(title, members));
+ };
+
return (
-
-
-
-
-
- @{user.username} · socket: {state}
+
+
+
+ {current !== null ? (
+
+ ) : (
+
+
+
+ A
+
+
Select a conversation
+
Search for someone or start a new chat to begin messaging.
+
+
+ )}
);
};
@@ -195,8 +307,10 @@ const Shell = (): ReactElement => {
if (status === 'loading') {
return (
-
- Loading…
+
+
+ A
+
);
}
diff --git a/packages/web/src/app/index.css b/packages/web/src/app/index.css
index 3fab69a..e5df004 100644
--- a/packages/web/src/app/index.css
+++ b/packages/web/src/app/index.css
@@ -1,532 +1,1458 @@
+/* =============================================================================
+ Altricade — design system foundation
+ Colors come from ThemeProvider as --color- (see shared/theme/tokens.ts).
+ Everything below is the static primitive layer + base element styling.
+ Component sections are appended further down, grouped by feature.
+ ========================================================================== */
+
:root {
color-scheme: light dark;
- /* Fallbacks until ThemeProvider sets the resolved tokens on . */
+ /* Fallback colors (light) until ThemeProvider writes the resolved tokens. */
--color-background: #ffffff;
- --color-surface: #f4f5f7;
- --color-text: #0b0c0f;
- --color-textMuted: #5b6472;
- --color-accent: #2f6fed;
- --color-border: #e2e5ea;
+ --color-surfacePanel: #f7f8fa;
+ --color-surface: #eef0f4;
+ --color-surfaceHover: #f0f2f6;
+ --color-chatBackdrop: #f4f5f8;
+ --color-text: #0c0d10;
+ --color-textMuted: #606a7b;
+ --color-textFaint: #9aa3b2;
+ --color-accent: #4c6fff;
+ --color-accentHover: #3a5cf5;
+ --color-accentSoft: #eaeeff;
+ --color-onAccent: #ffffff;
+ --color-onAccentMuted: rgba(255, 255, 255, 0.72);
+ --color-bubbleOut: #4c6fff;
+ --color-bubbleIn: #ffffff;
+ --color-border: #e6e8ee;
+ --color-borderStrong: #d4d8e0;
+ --color-online: #22c55e;
+ --color-danger: #ef4444;
+
+ /* Spacing scale (4px base). */
+ --space-1: 0.25rem;
+ --space-2: 0.5rem;
+ --space-3: 0.75rem;
+ --space-4: 1rem;
+ --space-5: 1.5rem;
+ --space-6: 2rem;
+ --space-8: 3rem;
+
+ /* Radius scale. */
+ --radius-sm: 8px;
+ --radius-md: 12px;
+ --radius-lg: 16px;
+ --radius-xl: 22px;
+ --radius-full: 999px;
+
+ /* Type. */
+ --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
+ Arial, sans-serif;
+ --font-mono: ui-monospace, 'SF Mono', 'JetBrains Mono', 'Fira Code', monospace;
+ --text-xs: 0.75rem;
+ --text-sm: 0.8125rem;
+ --text-base: 0.9375rem;
+ --text-md: 1rem;
+ --text-lg: 1.125rem;
+ --text-xl: 1.375rem;
+ --text-2xl: 1.75rem;
+
+ /* Layout. */
+ --rail-width: 340px;
+ --header-height: 68px;
+
+ /* Motion. */
+ --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
+ --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
+ --dur-fast: 120ms;
+ --dur: 200ms;
+ --dur-slow: 320ms;
+
+ /* Elevation (light). */
+ --shadow-sm: 0 1px 2px rgba(16, 18, 27, 0.06), 0 1px 1px rgba(16, 18, 27, 0.04);
+ --shadow-md: 0 6px 20px rgba(16, 18, 27, 0.1);
+ --shadow-lg: 0 24px 60px rgba(16, 18, 27, 0.22);
}
-* {
+:root[data-theme='dark'] {
+ /* Elevation (dark) — deeper, since surfaces are near-black. */
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
+ --shadow-md: 0 8px 28px rgba(0, 0, 0, 0.5);
+ --shadow-lg: 0 28px 70px rgba(0, 0, 0, 0.7);
+}
+
+*,
+*::before,
+*::after {
box-sizing: border-box;
}
+html,
+body,
+#root {
+ height: 100%;
+}
+
body {
margin: 0;
- font-family: system-ui, -apple-system, sans-serif;
+ font-family: var(--font-sans);
+ font-size: var(--text-base);
+ line-height: 1.5;
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
background: var(--color-background);
color: var(--color-text);
transition:
- background 0.2s ease,
- color 0.2s ease;
+ background var(--dur) var(--ease),
+ color var(--dur) var(--ease);
}
-.app {
- max-width: 640px;
- margin: 0 auto;
- padding: 3rem 1.5rem;
-}
-
-.app h1 {
- color: var(--color-accent);
- margin-bottom: 0.25rem;
-}
-
-.app dl {
- display: grid;
- grid-template-columns: max-content 1fr;
- gap: 0.5rem 1rem;
- padding: 1rem;
- background: var(--color-surface);
- border: 1px solid var(--color-border);
- border-radius: 12px;
-}
-
-.app dt {
- color: var(--color-textMuted);
-}
-
-.app dd {
+h1,
+h2,
+h3,
+h4,
+p {
margin: 0;
- font-family: ui-monospace, monospace;
}
-.theme-switch {
- display: flex;
- gap: 0.5rem;
- margin-top: 1.5rem;
-}
-
-.theme-switch button {
- padding: 0.5rem 1rem;
- border-radius: 8px;
- border: 1px solid var(--color-border);
- background: var(--color-surface);
- color: var(--color-text);
+button {
+ font: inherit;
+ color: inherit;
cursor: pointer;
}
-.theme-switch button[aria-pressed='true'] {
- border-color: var(--color-accent);
+input,
+textarea {
+ font: inherit;
+ color: inherit;
+}
+
+a {
color: var(--color-accent);
+ text-decoration: none;
}
-.auth-form,
-.echo-form {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
- max-width: 320px;
- margin: 1rem 0;
+:focus-visible {
+ outline: 2px solid var(--color-accent);
+ outline-offset: 2px;
}
-.echo-form {
- flex-direction: row;
+/* Scrollbars — thin, theme-aware, only on hover-capable surfaces. */
+* {
+ scrollbar-width: thin;
+ scrollbar-color: var(--color-borderStrong) transparent;
}
-.auth-form input,
-.echo-form input {
- padding: 0.5rem;
- border-radius: 8px;
- border: 1px solid var(--color-border);
- background: var(--color-surface);
- color: var(--color-text);
+*::-webkit-scrollbar {
+ width: 10px;
+ height: 10px;
}
-.auth-form button,
-.echo-form button,
-.actions button {
- padding: 0.5rem 1rem;
- border-radius: 8px;
- border: 1px solid var(--color-accent);
- background: var(--color-accent);
- color: #fff;
- cursor: pointer;
+*::-webkit-scrollbar-thumb {
+ background: transparent;
+ border: 3px solid transparent;
+ border-radius: var(--radius-full);
+ background-clip: padding-box;
}
-.auth-form button:disabled,
-.echo-form button:disabled {
- opacity: 0.5;
- cursor: not-allowed;
+*:hover::-webkit-scrollbar-thumb {
+ background: var(--color-borderStrong);
+ background-clip: padding-box;
}
-.auth-toggle {
- background: none;
- border: none;
- color: var(--color-accent);
- cursor: pointer;
- padding: 0;
+*::-webkit-scrollbar-track {
+ background: transparent;
}
-.auth-error {
- color: #e5484d;
+::selection {
+ background: color-mix(in srgb, var(--color-accent) 26%, transparent);
}
-.realtime dl {
- display: grid;
- grid-template-columns: max-content 1fr;
- gap: 0.25rem 1rem;
-}
-
-.event-log {
- font-family: ui-monospace, monospace;
- background: var(--color-surface);
- border: 1px solid var(--color-border);
- border-radius: 8px;
- padding: 0.75rem 1.25rem;
- max-height: 240px;
- overflow-y: auto;
-}
-
-.actions {
- margin: 1rem 0;
-}
-
-/* Chat layout */
-.layout {
- display: flex;
- flex-direction: column;
- height: 100vh;
-}
-
-.topbar {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 0.75rem 1rem;
- border-bottom: 1px solid var(--color-border);
- background: var(--color-surface);
-}
-
-.topbar-actions {
- display: flex;
- align-items: center;
- gap: 0.75rem;
-}
-
-.workspace {
- display: flex;
- flex: 1;
- min-height: 0;
-}
-
-.sidebar {
- width: 260px;
- border-right: 1px solid var(--color-border);
- padding: 1rem;
- overflow-y: auto;
-}
-
-.sidebar-form {
- display: flex;
- gap: 0.5rem;
- margin-bottom: 1rem;
-}
-
-.sidebar-form input {
- flex: 1;
- min-width: 0;
- padding: 0.4rem;
- border-radius: 8px;
- border: 1px solid var(--color-border);
- background: var(--color-background);
- color: var(--color-text);
-}
-
-.room-list {
- list-style: none;
- padding: 0;
- margin: 0 0 1rem;
- display: flex;
- flex-direction: column;
- gap: 0.25rem;
-}
-
-.room-list button {
- width: 100%;
- text-align: left;
- padding: 0.5rem 0.75rem;
- border-radius: 8px;
- border: 1px solid transparent;
- background: none;
- color: var(--color-text);
- cursor: pointer;
-}
-
-.room-list button[aria-pressed='true'] {
- background: var(--color-surface);
- border-color: var(--color-accent);
- color: var(--color-accent);
-}
-
-.chat {
- flex: 1;
- display: flex;
- flex-direction: column;
- min-height: 0;
- padding: 1rem;
-}
-
-.chat-empty {
- align-items: center;
- justify-content: center;
- color: var(--color-textMuted);
-}
-
-.message-list {
- list-style: none;
- margin: 0;
- padding: 0;
- flex: 1;
- overflow-y: auto;
- display: flex;
- flex-direction: column;
- gap: 0.4rem;
-}
-
-.message-list li {
- padding: 0.4rem 0.6rem;
- border-radius: 8px;
- background: var(--color-surface);
-}
-
-.message-list li.mine {
- align-self: flex-end;
- background: var(--color-accent);
- color: #fff;
-}
-
-.msg-author {
- font-weight: 600;
- margin-right: 0.5rem;
-}
-
-.composer {
- display: flex;
- gap: 0.5rem;
- margin-top: 0.75rem;
-}
-
-.composer input {
- flex: 1;
- padding: 0.6rem;
- border-radius: 8px;
- border: 1px solid var(--color-border);
- background: var(--color-surface);
- color: var(--color-text);
-}
-
-.sidebar-column {
- width: 280px;
- border-right: 1px solid var(--color-border);
- overflow-y: auto;
- display: flex;
- flex-direction: column;
-}
-
-.sidebar-column .sidebar {
- width: auto;
- border-right: none;
- border-bottom: 1px solid var(--color-border);
-}
-
-.sidebar-heading {
- font-size: 0.75rem;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- color: var(--color-textMuted);
- margin: 1rem 0 0.5rem;
-}
-
-.search-input {
- width: 100%;
- padding: 0.5rem;
- border-radius: 8px;
- border: 1px solid var(--color-border);
- background: var(--color-background);
- color: var(--color-text);
-}
-
-.search-results {
- list-style: none;
- padding: 0;
- margin: 0.5rem 0;
- border: 1px solid var(--color-border);
- border-radius: 8px;
- overflow: hidden;
-}
-
-.search-results button,
-.contact-list button {
- width: 100%;
- text-align: left;
- padding: 0.5rem 0.75rem;
- border: none;
- background: none;
- color: var(--color-text);
- cursor: pointer;
-}
+/* -------- shared primitives -------- */
.muted {
color: var(--color-textMuted);
}
-.contacts {
- padding: 1rem;
-}
-
-.contact-list {
- list-style: none;
- padding: 0;
- margin: 0;
-}
-
-.contact-list li {
- display: flex;
- align-items: center;
- justify-content: space-between;
-}
-
-.contact-remove {
- border: none;
- background: none;
- color: var(--color-textMuted);
- cursor: pointer;
-}
-
-.chat-title {
- margin: 0 0 0.75rem;
- padding-bottom: 0.5rem;
- border-bottom: 1px solid var(--color-border);
-}
-
-.group-form {
- flex-direction: column;
- align-items: stretch;
-}
-
-.online-dot {
- display: inline-block;
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background: #30a46c;
- margin-right: 0.4rem;
-}
-
-.unread-badge {
- display: inline-block;
- min-width: 1.2rem;
- padding: 0 0.35rem;
- margin-left: 0.4rem;
- border-radius: 999px;
- background: var(--color-accent);
- color: #fff;
- font-size: 0.7rem;
- text-align: center;
-}
-
-.msg-row {
- display: flex;
- align-items: baseline;
- gap: 0.4rem;
-}
-
-.read-mark {
- margin-left: auto;
- font-size: 0.75rem;
- opacity: 0.8;
-}
-
-.msg-actions {
- display: none;
- gap: 0.25rem;
- margin-top: 0.2rem;
-}
-
-.message-list li:hover .msg-actions {
- display: flex;
-}
-
-.react-btn,
-.link-btn {
- border: none;
- background: none;
- cursor: pointer;
- font-size: 0.8rem;
- padding: 0 0.15rem;
- color: inherit;
-}
-
-.link-btn {
- color: var(--color-textMuted);
- text-decoration: underline;
-}
-
-.reactions {
- display: flex;
- gap: 0.25rem;
- margin-top: 0.25rem;
-}
-
-.reaction {
- border: 1px solid var(--color-border);
- background: var(--color-background);
- color: var(--color-text);
- border-radius: 999px;
- padding: 0 0.5rem;
- font-size: 0.8rem;
- cursor: pointer;
-}
-
-.reaction.mine-reaction {
- border-color: var(--color-accent);
- color: var(--color-accent);
-}
-
-.typing {
- font-style: italic;
- padding: 0 0.25rem;
-}
-
-.topbar-user {
- display: flex;
- align-items: center;
- gap: 0.6rem;
-}
-
.avatar {
- width: 32px;
- height: 32px;
- border-radius: 50%;
+ flex: 0 0 auto;
+ width: 42px;
+ height: 42px;
+ border-radius: var(--radius-full);
object-fit: cover;
display: inline-flex;
align-items: center;
justify-content: center;
+ font-weight: 600;
+ user-select: none;
}
.avatar-placeholder {
- background: var(--color-accent);
- color: #fff;
+ background: linear-gradient(140deg, var(--color-accent), var(--color-accentHover));
+ color: var(--color-onAccent);
text-transform: uppercase;
- font-weight: 600;
-}
-
-.avatar-edit {
- cursor: pointer;
}
+/* Generic square icon button used across chrome (header, composer, rows). */
.icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
- width: 38px;
- height: 38px;
+ width: 40px;
+ height: 40px;
flex: 0 0 auto;
padding: 0;
- border: 1px solid transparent;
- border-radius: 8px;
+ border: none;
+ border-radius: var(--radius-full);
background: none;
color: var(--color-textMuted);
- cursor: pointer;
+ transition:
+ background var(--dur-fast) var(--ease),
+ color var(--dur-fast) var(--ease),
+ transform var(--dur-fast) var(--ease);
}
.icon-btn:hover {
- background: var(--color-surface);
+ background: var(--color-surfaceHover);
color: var(--color-text);
}
-.icon-btn.send {
- background: var(--color-accent);
- border-color: var(--color-accent);
- color: #fff;
+.icon-btn:active {
+ transform: scale(0.94);
}
+.icon-btn.accent {
+ background: var(--color-accent);
+ color: var(--color-onAccent);
+}
+
+.icon-btn.accent:hover {
+ background: var(--color-accentHover);
+}
+
+/* =============================================================================
+ Form primitives — fields & buttons
+ ========================================================================== */
+
+.field {
+ width: 100%;
+ padding: 0.65rem 0.85rem;
+ border-radius: var(--radius-md);
+ border: 1px solid var(--color-border);
+ background: var(--color-background);
+ color: var(--color-text);
+ transition:
+ border-color var(--dur-fast) var(--ease),
+ box-shadow var(--dur-fast) var(--ease);
+}
+
+.field::placeholder {
+ color: var(--color-textFaint);
+}
+
+.field:focus {
+ outline: none;
+ border-color: var(--color-accent);
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent) 20%, transparent);
+}
+
+.field-label {
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
+ font-size: var(--text-sm);
+ font-weight: 500;
+ color: var(--color-textMuted);
+}
+
+.btn-primary {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ padding: 0.7rem 1.1rem;
+ border: none;
+ border-radius: var(--radius-md);
+ background: var(--color-accent);
+ color: var(--color-onAccent);
+ font-weight: 600;
+ transition:
+ background var(--dur-fast) var(--ease),
+ transform var(--dur-fast) var(--ease),
+ opacity var(--dur-fast) var(--ease);
+}
+
+.btn-primary:hover:not(:disabled) {
+ background: var(--color-accentHover);
+}
+
+.btn-primary:active:not(:disabled) {
+ transform: scale(0.99);
+}
+
+.btn-primary:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.btn-sm {
+ padding: 0.55rem 0.9rem;
+ font-size: var(--text-sm);
+}
+
+.link {
+ border: none;
+ background: none;
+ padding: 0;
+ color: var(--color-accent);
+ font-weight: 600;
+}
+
+.link:hover {
+ text-decoration: underline;
+}
+
+.form-error {
+ color: var(--color-danger);
+ font-size: var(--text-sm);
+}
+
+/* =============================================================================
+ Splash + Auth
+ ========================================================================== */
+
+.splash {
+ display: grid;
+ place-items: center;
+ height: 100%;
+ background: var(--color-background);
+}
+
+.splash-mark,
+.auth-mark,
+.rail-mark,
+.chat-empty-mark {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 800;
+ color: var(--color-onAccent);
+ background: linear-gradient(140deg, var(--color-accent), var(--color-accentHover));
+ border-radius: 30%;
+ user-select: none;
+}
+
+.splash-mark {
+ width: 64px;
+ height: 64px;
+ font-size: 2rem;
+ animation: splash-pulse 1.4s var(--ease) infinite;
+}
+
+@keyframes splash-pulse {
+ 0%,
+ 100% {
+ opacity: 1;
+ transform: scale(1);
+ }
+ 50% {
+ opacity: 0.65;
+ transform: scale(0.94);
+ }
+}
+
+.auth {
+ display: grid;
+ place-items: center;
+ height: 100%;
+ padding: 1.5rem;
+ background:
+ radial-gradient(120% 120% at 15% 0%, color-mix(in srgb, var(--color-accent) 12%, transparent), transparent 45%),
+ var(--color-chatBackdrop);
+}
+
+.auth-card {
+ width: 100%;
+ max-width: 400px;
+ padding: 2.25rem;
+ border-radius: var(--radius-xl);
+ background: var(--color-background);
+ border: 1px solid var(--color-border);
+ box-shadow: var(--shadow-lg);
+}
+
+.auth-brand {
+ text-align: center;
+ margin-bottom: 1.75rem;
+}
+
+.auth-mark {
+ width: 52px;
+ height: 52px;
+ font-size: 1.6rem;
+ margin-bottom: 0.9rem;
+}
+
+.auth-wordmark {
+ font-size: var(--text-2xl);
+ font-weight: 700;
+ letter-spacing: -0.02em;
+}
+
+.auth-tagline {
+ color: var(--color-textMuted);
+ margin-top: 0.25rem;
+}
+
+.auth-form {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.auth-form .btn-primary {
+ margin-top: 0.35rem;
+}
+
+.auth-switch {
+ margin-top: 1.5rem;
+ text-align: center;
+ font-size: var(--text-sm);
+ color: var(--color-textMuted);
+}
+
+/* =============================================================================
+ App shell + conversation rail
+ ========================================================================== */
+
+.app-shell {
+ display: grid;
+ grid-template-columns: var(--rail-width) 1fr;
+ height: 100%;
+ min-height: 0;
+}
+
+.rail {
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ background: var(--color-surfacePanel);
+ border-right: 1px solid var(--color-border);
+}
+
+.rail-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.5rem;
+ padding: 0 0.85rem;
+ height: var(--header-height);
+ flex: 0 0 auto;
+}
+
+.rail-brand {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.6rem;
+ min-width: 0;
+}
+
+.rail-mark {
+ width: 34px;
+ height: 34px;
+ font-size: 1.05rem;
+}
+
+.rail-title {
+ font-weight: 700;
+ font-size: var(--text-lg);
+ letter-spacing: -0.02em;
+}
+
+.rail-header-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.35rem;
+}
+
+.theme-switch {
+ display: inline-flex;
+ padding: 3px;
+ gap: 2px;
+ border-radius: var(--radius-full);
+ background: var(--color-surface);
+}
+
+.theme-option {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 30px;
+ height: 30px;
+ border: none;
+ border-radius: var(--radius-full);
+ background: none;
+ color: var(--color-textFaint);
+ transition:
+ background var(--dur-fast) var(--ease),
+ color var(--dur-fast) var(--ease);
+}
+
+.theme-option:hover {
+ color: var(--color-text);
+}
+
+.theme-option[aria-pressed='true'] {
+ background: var(--color-background);
+ color: var(--color-accent);
+ box-shadow: var(--shadow-sm);
+}
+
+.rail-search {
+ position: relative;
+ display: flex;
+ align-items: center;
+ padding: 0 0.85rem 0.5rem;
+ flex: 0 0 auto;
+}
+
+.rail-search-icon {
+ position: absolute;
+ left: 1.55rem;
+ color: var(--color-textFaint);
+ pointer-events: none;
+}
+
+.rail-search-input {
+ width: 100%;
+ padding: 0.6rem 0.85rem 0.6rem 2.35rem;
+ border-radius: var(--radius-full);
+ border: 1px solid transparent;
+ background: var(--color-surface);
+ color: var(--color-text);
+ transition:
+ background var(--dur-fast) var(--ease),
+ border-color var(--dur-fast) var(--ease);
+}
+
+.rail-search-input::placeholder {
+ color: var(--color-textFaint);
+}
+
+.rail-search-input:focus {
+ outline: none;
+ background: var(--color-background);
+ border-color: var(--color-accent);
+}
+
+.rail-search-clear {
+ position: absolute;
+ right: 1.35rem;
+ display: inline-flex;
+ border: none;
+ background: none;
+ color: var(--color-textFaint);
+ padding: 0.15rem;
+ border-radius: var(--radius-full);
+}
+
+.rail-search-clear:hover {
+ color: var(--color-text);
+}
+
+.rail-scroll {
+ flex: 1;
+ min-height: 0;
+ overflow-y: auto;
+ padding: 0.25rem 0 0.5rem;
+}
+
+.rail-section-title {
+ padding: 0.5rem 0.6rem;
+ font-size: var(--text-xs);
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--color-textFaint);
+}
+
+.rail-section {
+ padding: 0 0.6rem;
+}
+
+.rail-empty {
+ padding: 1.25rem 1rem;
+ text-align: center;
+ color: var(--color-textFaint);
+ font-size: var(--text-sm);
+}
+
+.conv-list {
+ padding: 0 0.5rem;
+}
+
+.conv-row {
+ display: flex;
+ align-items: center;
+ gap: 0.7rem;
+ width: 100%;
+ padding: 0.55rem 0.6rem;
+ border: none;
+ border-radius: var(--radius-lg);
+ background: none;
+ text-align: left;
+ transition: background var(--dur-fast) var(--ease);
+}
+
+.conv-row:hover {
+ background: var(--color-surfaceHover);
+}
+
+.conv-row[aria-pressed='true'] {
+ background: var(--color-accentSoft);
+}
+
+.conv-avatar-wrap {
+ position: relative;
+ flex: 0 0 auto;
+ line-height: 0;
+}
+
+.online-dot {
+ position: absolute;
+ right: -1px;
+ bottom: -1px;
+ width: 12px;
+ height: 12px;
+ border-radius: var(--radius-full);
+ background: var(--color-online);
+ border: 2.5px solid var(--color-surfacePanel);
+}
+
+.conv-row[aria-pressed='true'] .online-dot {
+ border-color: color-mix(in srgb, var(--color-accentSoft) 100%, var(--color-surfacePanel));
+}
+
+.conv-main {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 0.15rem;
+}
+
+.conv-top {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 0.5rem;
+}
+
+.conv-name {
+ font-weight: 600;
+ font-size: var(--text-base);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.conv-time {
+ flex: 0 0 auto;
+ font-size: var(--text-xs);
+ color: var(--color-textFaint);
+}
+
+.conv-sub {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.5rem;
+}
+
+.conv-preview {
+ min-width: 0;
+ font-size: var(--text-sm);
+ color: var(--color-textMuted);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.unread-badge {
+ flex: 0 0 auto;
+ min-width: 20px;
+ height: 20px;
+ padding: 0 6px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--radius-full);
+ background: var(--color-accent);
+ color: var(--color-onAccent);
+ font-size: var(--text-xs);
+ font-weight: 600;
+}
+
+/* Compose panel (new group + contacts) */
+.compose {
+ padding: 0.25rem 0.85rem 1rem;
+}
+
+.compose-block {
+ padding: 0.5rem 0;
+}
+
+.compose-block + .compose-block {
+ border-top: 1px solid var(--color-border);
+ margin-top: 0.5rem;
+}
+
+.stack-form {
+ display: flex;
+ flex-direction: column;
+ gap: 0.55rem;
+}
+
+.inline-add {
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+}
+
+.contact-list {
+ list-style: none;
+ margin: 0.5rem 0 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 0.15rem;
+}
+
+.contact-row {
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+}
+
+.contact-open {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ gap: 0.6rem;
+ padding: 0.4rem 0.5rem;
+ border: none;
+ border-radius: var(--radius-md);
+ background: none;
+ text-align: left;
+ transition: background var(--dur-fast) var(--ease);
+}
+
+.contact-open:hover {
+ background: var(--color-surfaceHover);
+}
+
+.contact-avatar {
+ width: 34px;
+ height: 34px;
+ font-size: var(--text-sm);
+}
+
+.contact-names {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.contact-name {
+ font-weight: 500;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.contact-handle {
+ font-size: var(--text-xs);
+}
+
+.contact-remove {
+ width: 34px;
+ height: 34px;
+ color: var(--color-textFaint);
+}
+
+.contact-remove:hover {
+ color: var(--color-danger);
+ background: color-mix(in srgb, var(--color-danger) 12%, transparent);
+}
+
+/* Rail footer (account) */
+.rail-footer {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.6rem 0.85rem;
+ flex: 0 0 auto;
+ border-top: 1px solid var(--color-border);
+}
+
+.rail-account {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ gap: 0.6rem;
+ padding: 0.35rem;
+ border-radius: var(--radius-md);
+ cursor: pointer;
+ transition: background var(--dur-fast) var(--ease);
+}
+
+.rail-account:hover {
+ background: var(--color-surfaceHover);
+}
+
+.rail-account-avatar {
+ width: 38px;
+ height: 38px;
+ font-size: var(--text-base);
+}
+
+.rail-account-names {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.rail-account-name {
+ font-weight: 600;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.rail-account-handle {
+ font-size: var(--text-xs);
+}
+
+/* =============================================================================
+ Chat pane — empty state (full chat UI added in a later section)
+ ========================================================================== */
+
+.chat-pane {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ min-height: 0;
+ background: var(--color-chatBackdrop);
+}
+
+.chat-empty {
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+}
+
+.chat-empty-inner {
+ max-width: 320px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.chat-empty-mark {
+ width: 56px;
+ height: 56px;
+ font-size: 1.6rem;
+ margin-bottom: 0.5rem;
+ opacity: 0.9;
+}
+
+/* =============================================================================
+ Chat pane — header, scrollback, bubbles, composer
+ ========================================================================== */
+
+.chat-pane {
+ position: relative;
+}
+
+/* Subtle depth on the chat backdrop — barely-there, not a busy pattern. */
+.chat-pane::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ background:
+ radial-gradient(60% 55% at 78% 0%, color-mix(in srgb, var(--color-accent) 6%, transparent), transparent 70%),
+ radial-gradient(50% 50% at 0% 100%, color-mix(in srgb, var(--color-accent) 5%, transparent), transparent 70%);
+}
+
+.chat-header {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ height: var(--header-height);
+ flex: 0 0 auto;
+ padding: 0 1.1rem;
+ background: color-mix(in srgb, var(--color-surfacePanel) 82%, transparent);
+ backdrop-filter: blur(12px);
+ border-bottom: 1px solid var(--color-border);
+}
+
+.chat-header-avatar {
+ width: 40px;
+ height: 40px;
+}
+
+.chat-header-text {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ line-height: 1.25;
+}
+
+.chat-header-title {
+ font-weight: 600;
+ font-size: var(--text-md);
+}
+
+.chat-header-sub {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
+ font-size: var(--text-xs);
+ color: var(--color-textMuted);
+}
+
+.chat-header-sub.typing-sub {
+ color: var(--color-accent);
+ font-style: italic;
+}
+
+.online-inline {
+ width: 7px;
+ height: 7px;
+ border-radius: var(--radius-full);
+ background: var(--color-online);
+}
+
+.chat-loading {
+ text-align: center;
+ padding: 0.5rem;
+ font-size: var(--text-sm);
+}
+
+.message-scroll {
+ position: relative;
+ z-index: 1;
+ flex: 1;
+ min-height: 0;
+ overflow-y: auto;
+ padding: 1rem clamp(0.75rem, 4vw, 3rem);
+}
+
+.message-list {
+ display: flex;
+ flex-direction: column;
+ max-width: 900px;
+ margin: 0 auto;
+}
+
+/* Date separator */
+.day-sep {
+ display: flex;
+ justify-content: center;
+ margin: 0.75rem 0;
+}
+
+.day-sep span {
+ padding: 0.2rem 0.75rem;
+ border-radius: var(--radius-full);
+ background: color-mix(in srgb, var(--color-surfacePanel) 85%, transparent);
+ backdrop-filter: blur(6px);
+ font-size: var(--text-xs);
+ font-weight: 600;
+ color: var(--color-textMuted);
+ box-shadow: var(--shadow-sm);
+}
+
+/* Message line + grouping */
+.msg-line {
+ display: flex;
+ align-items: flex-end;
+ gap: 0.5rem;
+ padding: 1px 0;
+}
+
+.msg-line.mine {
+ justify-content: flex-end;
+}
+
+.msg-line.group-end {
+ margin-bottom: 0.5rem;
+}
+
+.msg-avatar {
+ width: 30px;
+ height: 30px;
+ font-size: var(--text-xs);
+ align-self: flex-end;
+}
+
+.msg-avatar-spacer {
+ width: 30px;
+ flex: 0 0 auto;
+}
+
+.bubble-wrap {
+ display: flex;
+ flex-direction: column;
+ max-width: min(74%, 560px);
+ min-width: 0;
+}
+
+.msg-line.mine .bubble-wrap {
+ align-items: flex-end;
+}
+
+.msg-sender {
+ margin: 0 0 2px 0.75rem;
+ font-size: var(--text-xs);
+ font-weight: 600;
+ color: var(--color-accent);
+}
+
+/* Bubble */
+.bubble {
+ position: relative;
+ padding: 0.42rem 0.7rem;
+ border-radius: var(--radius-lg);
+ font-size: var(--text-base);
+ line-height: 1.4;
+}
+
+.bubble::after {
+ content: '';
+ display: block;
+ clear: both;
+}
+
+.bubble-mine {
+ background: var(--color-bubbleOut);
+ color: var(--color-onAccent);
+}
+
+.bubble-mine:not(.is-start) {
+ border-top-right-radius: 6px;
+}
+
+.bubble-mine:not(.is-end) {
+ border-bottom-right-radius: 6px;
+}
+
+.bubble-theirs {
+ background: var(--color-bubbleIn);
+ color: var(--color-text);
+ box-shadow: var(--shadow-sm);
+}
+
+.bubble-theirs:not(.is-start) {
+ border-top-left-radius: 6px;
+}
+
+.bubble-theirs:not(.is-end) {
+ border-bottom-left-radius: 6px;
+}
+
+.bubble.has-media {
+ padding: 0.28rem;
+}
+
+.bubble.has-media .bubble-text {
+ display: block;
+ padding: 0.25rem 0.45rem 0.1rem;
+}
+
+.bubble-text {
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+}
+
+.bubble-deleted {
+ font-style: italic;
+ opacity: 0.7;
+}
+
+.bubble-meta {
+ float: right;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.2rem;
+ margin: 0.35rem 0 -0.1rem 0.6rem;
+ font-size: 0.68rem;
+ line-height: 1;
+ user-select: none;
+}
+
+.bubble-mine .bubble-meta {
+ color: var(--color-onAccentMuted);
+}
+
+.bubble-theirs .bubble-meta {
+ color: var(--color-textFaint);
+}
+
+.bubble.has-media .bubble-meta {
+ padding: 0 0.45rem 0.2rem 0.6rem;
+}
+
+.bubble-edited {
+ font-style: italic;
+}
+
+.tick {
+ display: inline-flex;
+ color: var(--color-onAccentMuted);
+}
+
+.tick-read {
+ color: var(--color-onAccent);
+}
+
+/* Hover toolbar */
+.msg-tools {
+ position: absolute;
+ top: -1rem;
+ z-index: 3;
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ padding: 3px;
+ border-radius: var(--radius-full);
+ background: var(--color-bubbleIn);
+ border: 1px solid var(--color-border);
+ box-shadow: var(--shadow-md);
+ opacity: 0;
+ transform: translateY(0.3rem) scale(0.96);
+ pointer-events: none;
+ transition:
+ opacity var(--dur-fast) var(--ease),
+ transform var(--dur-fast) var(--ease-out);
+}
+
+.msg-line:hover .msg-tools {
+ opacity: 1;
+ transform: none;
+ pointer-events: auto;
+}
+
+.msg-line.mine .msg-tools {
+ right: 0.25rem;
+}
+
+.msg-line.theirs .msg-tools {
+ left: 0.25rem;
+}
+
+.tool-react {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 30px;
+ height: 30px;
+ border: none;
+ border-radius: var(--radius-full);
+ background: none;
+ font-size: 1rem;
+ line-height: 1;
+ transition:
+ background var(--dur-fast) var(--ease),
+ transform var(--dur-fast) var(--ease-out);
+}
+
+.tool-react:hover {
+ background: var(--color-surfaceHover);
+ transform: scale(1.25);
+}
+
+.tool-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 30px;
+ height: 30px;
+ border: none;
+ border-radius: var(--radius-full);
+ background: none;
+ color: var(--color-textMuted);
+ transition:
+ background var(--dur-fast) var(--ease),
+ color var(--dur-fast) var(--ease);
+}
+
+.tool-btn:hover {
+ background: var(--color-surfaceHover);
+ color: var(--color-text);
+}
+
+.tool-danger:hover {
+ color: var(--color-danger);
+ background: color-mix(in srgb, var(--color-danger) 12%, transparent);
+}
+
+.tool-divider {
+ width: 1px;
+ height: 18px;
+ margin: 0 2px;
+ background: var(--color-border);
+}
+
+/* Reactions */
+.reactions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ margin-top: 5px;
+}
+
+.msg-line.mine .reactions {
+ justify-content: flex-end;
+}
+
+.reaction {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.25rem;
+ padding: 0.1rem 0.5rem;
+ border-radius: var(--radius-full);
+ border: 1px solid var(--color-border);
+ background: var(--color-bubbleIn);
+ color: var(--color-text);
+ font-size: var(--text-sm);
+ line-height: 1.4;
+ transition:
+ background var(--dur-fast) var(--ease),
+ border-color var(--dur-fast) var(--ease),
+ transform var(--dur-fast) var(--ease-out);
+}
+
+.reaction:hover {
+ transform: translateY(-1px);
+}
+
+.reaction.is-mine {
+ background: var(--color-accentSoft);
+ border-color: color-mix(in srgb, var(--color-accent) 45%, transparent);
+ color: var(--color-accent);
+}
+
+.reaction-count {
+ font-weight: 600;
+ font-variant-numeric: tabular-nums;
+}
+
+/* Inline edit */
+.bubble-edit {
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
+ min-width: 220px;
+}
+
+.edit-input {
+ width: 100%;
+ min-height: 2.4rem;
+ resize: vertical;
+ border: none;
+ background: color-mix(in srgb, currentColor 8%, transparent);
+ color: inherit;
+ border-radius: var(--radius-sm);
+ padding: 0.35rem 0.5rem;
+ font: inherit;
+}
+
+.edit-input:focus {
+ outline: none;
+}
+
+.edit-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 0.3rem;
+}
+
+.edit-actions .icon-btn {
+ width: 30px;
+ height: 30px;
+}
+
+.edit-cancel {
+ color: var(--color-onAccentMuted);
+}
+
+.edit-cancel:hover {
+ background: color-mix(in srgb, currentColor 15%, transparent);
+ color: var(--color-onAccent);
+}
+
+/* =============================================================================
+ Media (Part-3 baseline — enriched in the media section below)
+ ========================================================================== */
+
+.media-loading {
+ display: inline-block;
+ padding: 0.35rem 0.5rem;
+ font-size: var(--text-sm);
+ opacity: 0.7;
+}
+
+.media-img {
+ max-width: min(300px, 68vw);
+ max-height: 340px;
+ border-radius: var(--radius-md);
+ display: block;
+ cursor: zoom-in;
+}
+
+.media-video {
+ max-width: min(340px, 72vw);
+ border-radius: var(--radius-md);
+ display: block;
+}
+
+.media-audio {
+ min-width: 240px;
+ height: 40px;
+}
+
+.media-file {
+ display: flex;
+ align-items: center;
+ gap: 0.65rem;
+ min-width: 220px;
+ padding: 0.5rem 0.6rem;
+ border-radius: var(--radius-md);
+ color: inherit;
+ text-decoration: none;
+ background: color-mix(in srgb, currentColor 8%, transparent);
+}
+
+.media-file-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 40px;
+ flex: 0 0 auto;
+ border-radius: var(--radius-sm);
+ background: color-mix(in srgb, currentColor 14%, transparent);
+}
+
+.media-file-meta {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.media-file-name {
+ font-size: var(--text-sm);
+ font-weight: 600;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.media-file-size {
+ font-size: var(--text-xs);
+ opacity: 0.7;
+}
+
+.media-file-dl {
+ flex: 0 0 auto;
+ opacity: 0.75;
+}
+
+/* =============================================================================
+ Composer
+ ========================================================================== */
+
+.composer {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ align-items: flex-end;
+ gap: 0.35rem;
+ padding: 0.6rem clamp(0.6rem, 3vw, 1.5rem);
+ background: var(--color-surfacePanel);
+ border-top: 1px solid var(--color-border);
+}
+
+.composer-input {
+ flex: 1;
+ min-width: 0;
+ max-height: 8.5rem;
+ resize: none;
+ field-sizing: content;
+ padding: 0.55rem 1rem;
+ border-radius: var(--radius-xl);
+ border: 1px solid var(--color-border);
+ background: var(--color-background);
+ color: var(--color-text);
+ line-height: 1.4;
+}
+
+.composer-input::placeholder {
+ color: var(--color-textFaint);
+}
+
+.composer-input:focus {
+ outline: none;
+ border-color: var(--color-accent);
+}
+
+.composer-send {
+ transition:
+ background var(--dur-fast) var(--ease),
+ opacity var(--dur-fast) var(--ease),
+ transform var(--dur-fast) var(--ease);
+}
+
+.composer-send:disabled {
+ opacity: 0.45;
+ background: var(--color-surface);
+ color: var(--color-textFaint);
+ cursor: not-allowed;
+}
+
+/* Recording bar */
.recording-bar {
align-items: center;
+ gap: 0.6rem;
}
.record-dot {
width: 10px;
height: 10px;
- border-radius: 50%;
- background: #e5484d;
- animation: record-pulse 1s ease-in-out infinite;
+ flex: 0 0 auto;
+ border-radius: var(--radius-full);
+ background: var(--color-danger);
+ animation: record-pulse 1s var(--ease) infinite;
}
.record-label {
flex: 1;
+ font-size: var(--text-sm);
color: var(--color-text);
- font-size: 0.9rem;
}
.record-preview {
- width: 72px;
- height: 72px;
- border-radius: 8px;
+ width: 64px;
+ height: 64px;
+ border-radius: var(--radius-md);
object-fit: cover;
background: #000;
}
@@ -537,63 +1463,310 @@ body {
opacity: 1;
}
50% {
- opacity: 0.3;
+ opacity: 0.25;
}
}
-.media-img {
- max-width: 260px;
- max-height: 260px;
- border-radius: 8px;
- display: block;
-}
+/* =============================================================================
+ Media renderers — voice waveform, round video, thumbnails, lightbox
+ ========================================================================== */
-.media-video {
- max-width: 320px;
- border-radius: 8px;
- display: block;
-}
-
-.media-file {
- display: inline-flex;
+/* Voice note — custom waveform player */
+.voice {
+ display: flex;
align-items: center;
- gap: 0.35rem;
- color: var(--color-accent);
+ gap: 0.6rem;
+ min-width: 232px;
+ max-width: 300px;
+ padding: 0.15rem 0.15rem 0.15rem 0.05rem;
}
-/* In-app notification toasts */
+.voice-play {
+ display: grid;
+ place-items: center;
+ width: 40px;
+ height: 40px;
+ flex: 0 0 auto;
+ border: none;
+ border-radius: var(--radius-full);
+ background: var(--color-accent);
+ color: var(--color-onAccent);
+ transition: transform var(--dur-fast) var(--ease);
+}
+
+.voice-play:active {
+ transform: scale(0.92);
+}
+
+.voice-mine .voice-play {
+ background: var(--color-onAccent);
+ color: var(--color-bubbleOut);
+}
+
+.voice-wave {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ height: 30px;
+ cursor: pointer;
+}
+
+.wave-bar {
+ flex: 1;
+ min-width: 2px;
+ min-height: 3px;
+ border-radius: 2px;
+ background: var(--color-textFaint);
+ transition: background var(--dur-fast) var(--ease);
+}
+
+.wave-bar.is-played {
+ background: var(--color-accent);
+}
+
+.voice-mine .wave-bar {
+ background: var(--color-onAccentMuted);
+}
+
+.voice-mine .wave-bar.is-played {
+ background: var(--color-onAccent);
+}
+
+.voice-time {
+ flex: 0 0 auto;
+ min-width: 30px;
+ font-size: 0.72rem;
+ font-variant-numeric: tabular-nums;
+ opacity: 0.85;
+}
+
+/* Round video message */
+.round-video {
+ position: relative;
+ display: block;
+ width: 208px;
+ height: 208px;
+ padding: 0;
+ border: none;
+ border-radius: var(--radius-full);
+ overflow: hidden;
+ background: #000;
+ cursor: pointer;
+}
+
+.round-video-el {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+}
+
+.round-video-overlay {
+ position: absolute;
+ inset: 0;
+ display: grid;
+ place-items: center;
+ color: #fff;
+ background: rgba(0, 0, 0, 0.28);
+ transition: background var(--dur) var(--ease);
+}
+
+.round-video-time {
+ position: absolute;
+ bottom: 0.6rem;
+ left: 50%;
+ transform: translateX(-50%);
+ padding: 0.1rem 0.5rem;
+ border-radius: var(--radius-full);
+ background: rgba(0, 0, 0, 0.5);
+ color: #fff;
+ font-size: 0.7rem;
+ font-variant-numeric: tabular-nums;
+}
+
+/* Image + video-file thumbnails */
+.media-image-btn,
+.media-video-frame {
+ display: block;
+ position: relative;
+ padding: 0;
+ border: none;
+ background: none;
+ line-height: 0;
+ cursor: pointer;
+ border-radius: var(--radius-md);
+}
+
+.media-video-frame .media-video {
+ pointer-events: none;
+}
+
+.media-play-overlay {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ display: grid;
+ place-items: center;
+ width: 54px;
+ height: 54px;
+ border-radius: var(--radius-full);
+ background: rgba(0, 0, 0, 0.5);
+ color: #fff;
+ backdrop-filter: blur(2px);
+}
+
+/* Lightbox viewer */
+.viewer {
+ position: fixed;
+ inset: 0;
+ z-index: 1000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(8, 9, 13, 0.86);
+ backdrop-filter: blur(16px);
+ animation: viewer-in var(--dur) var(--ease);
+}
+
+@keyframes viewer-in {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+.viewer-toolbar {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ padding: 0.9rem 1.1rem;
+ color: #fff;
+}
+
+.viewer-name {
+ min-width: 0;
+ font-size: var(--text-sm);
+ opacity: 0.85;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.viewer-actions {
+ display: flex;
+ gap: 0.35rem;
+ flex: 0 0 auto;
+}
+
+.viewer-btn {
+ color: #fff;
+}
+
+.viewer-btn:hover {
+ background: rgba(255, 255, 255, 0.16);
+ color: #fff;
+}
+
+.viewer-stage {
+ max-width: 92vw;
+ max-height: 88vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.viewer-img {
+ max-width: 92vw;
+ max-height: 88vh;
+ object-fit: contain;
+ border-radius: var(--radius-sm);
+ user-select: none;
+ touch-action: none;
+ will-change: transform;
+}
+
+.viewer-video {
+ max-width: 92vw;
+ max-height: 88vh;
+ border-radius: var(--radius-md);
+}
+
+/* Reduced-motion: drop the non-essential animations. */
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.001ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.001ms !important;
+ }
+}
+
+/* Bare media (round video notes) — no bubble chrome. */
+.bubble.bubble-bare {
+ background: transparent;
+ box-shadow: none;
+ padding: 0;
+ border-radius: 0;
+}
+
+.bubble.bubble-bare .bubble-meta {
+ color: var(--color-textFaint);
+ padding: 0;
+ margin-top: 0.3rem;
+}
+
+/* =============================================================================
+ Notification toasts
+ ========================================================================== */
+
.toast-stack {
position: fixed;
top: 1rem;
right: 1rem;
+ z-index: 1100;
display: flex;
flex-direction: column;
- gap: 0.5rem;
- z-index: 1000;
+ gap: 0.6rem;
max-width: min(360px, calc(100vw - 2rem));
}
.toast {
display: flex;
flex-direction: column;
- gap: 0.15rem;
+ gap: 0.2rem;
text-align: left;
padding: 0.75rem 1rem;
- border-radius: 12px;
+ border-radius: var(--radius-md);
border: 1px solid var(--color-border);
- background: var(--color-surface);
+ background: var(--color-surfacePanel);
color: var(--color-text);
- box-shadow: 0 6px 20px rgb(0 0 0 / 18%);
+ box-shadow: var(--shadow-md);
cursor: pointer;
- animation: toast-in 0.18s ease;
+ animation: toast-in var(--dur) var(--ease-out);
+ transition: transform var(--dur-fast) var(--ease);
+}
+
+.toast:hover {
+ transform: translateY(-1px);
}
.toast-title {
- font-size: 0.9rem;
+ font-size: var(--text-sm);
+ font-weight: 600;
}
.toast-body {
- font-size: 0.85rem;
+ font-size: var(--text-sm);
color: var(--color-textMuted);
overflow: hidden;
text-overflow: ellipsis;
@@ -603,7 +1776,7 @@ body {
@keyframes toast-in {
from {
opacity: 0;
- transform: translateY(-8px);
+ transform: translateY(-0.5rem);
}
to {
opacity: 1;
diff --git a/packages/web/src/features/auth/ui/AuthForm.tsx b/packages/web/src/features/auth/ui/AuthForm.tsx
index b25abc0..170eb00 100644
--- a/packages/web/src/features/auth/ui/AuthForm.tsx
+++ b/packages/web/src/features/auth/ui/AuthForm.tsx
@@ -32,56 +32,75 @@ export const AuthForm = (): ReactElement => {
};
return (
-
- Altricade
- {mode === 'login' ? 'Log in' : 'Create an account'}
-