@import 'tailwindcss'; /* * Дизайн-токены (docs/client-web.md §3). Тёмная тема по умолчанию, * светлая — тот же набор токенов. Акцент сервера переопределяет --color-accent * в скоупе сервера. */ @theme { --color-bg: oklch(0.19 0.02 265); --color-surface-1: oklch(0.23 0.02 265); --color-surface-2: oklch(0.27 0.02 265); --color-surface-3: oklch(0.32 0.02 265); --color-fg: oklch(0.96 0.005 265); --color-fg-muted: oklch(0.72 0.01 265); --color-accent: oklch(0.66 0.16 265); --color-accent-fg: oklch(0.99 0.005 265); --color-danger: oklch(0.62 0.19 25); --color-success: oklch(0.72 0.15 150); --color-warning: oklch(0.79 0.14 85); /* Тёмно-голубой: точка статуса «невидимый» в разделе друзей. */ --color-info: oklch(0.45 0.09 240); --color-border: oklch(0.35 0.02 265); --radius-sm: 6px; --radius-md: 10px; --radius-lg: 14px; --shadow-1: 0 1px 2px rgb(0 0 0 / 0.3); --shadow-2: 0 4px 12px rgb(0 0 0 / 0.35); --shadow-3: 0 12px 32px rgb(0 0 0 / 0.4); --duration-fast: 120ms; --duration-base: 200ms; /* Шаг каскадного появления элементов списка (см. .gl-stagger). */ --stagger-step: 28ms; --ease-out: cubic-bezier(0.16, 1, 0.3, 1); } :root[data-theme='light'] { --color-bg: oklch(0.98 0.003 265); --color-surface-1: oklch(1 0 0); --color-surface-2: oklch(0.96 0.004 265); --color-surface-3: oklch(0.92 0.006 265); --color-fg: oklch(0.2 0.02 265); --color-fg-muted: oklch(0.48 0.015 265); --color-border: oklch(0.87 0.008 265); --shadow-1: 0 1px 2px rgb(0 0 0 / 0.08); --shadow-2: 0 4px 12px rgb(0 0 0 / 0.1); --shadow-3: 0 12px 32px rgb(0 0 0 / 0.12); } html, body, #root { height: 100%; } body { margin: 0; background-color: var(--color-bg); color: var(--color-fg); font-family: system-ui, -apple-system, 'Segoe UI', Inter, sans-serif; font-size: 14px; line-height: 1.45; } :focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; } /* * Анимации клиента (docs/client-web.md §3): сдержанные, быстрые, без * зависимостей. Ключевые кадры и классы живут здесь, а не в inline-стилях, * чтобы их можно было отключить одним media-запросом ниже. * * Все входные анимации используют fill-mode `backwards`: до старта элемент * уже в начальном кадре (это нужно каскаду с задержками), а после финиша * отдаёт управление обычным стилям — hover-трансформации продолжают работать. */ /* Появление страницы при смене маршрута: fade + сдвиг на 6px. */ @keyframes gl-page-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } } .gl-page { animation: gl-page-in var(--duration-base) var(--ease-out) backwards; } /* Затемнение подложки модального окна. */ @keyframes gl-backdrop-in { from { background-color: rgb(0 0 0 / 0); } to { background-color: rgb(0 0 0 / 0.6); } } @keyframes gl-backdrop-out { from { background-color: rgb(0 0 0 / 0.6); } to { background-color: rgb(0 0 0 / 0); } } .gl-backdrop-in { animation: gl-backdrop-in var(--duration-fast) var(--ease-out) backwards; } .gl-backdrop-out { animation: gl-backdrop-out var(--duration-fast) var(--ease-out) forwards; } /* Панель модального окна: scale(0.96 → 1) с небольшим подъёмом. */ @keyframes gl-pop-in { from { opacity: 0; transform: scale(0.96) translateY(8px); } to { opacity: 1; transform: none; } } @keyframes gl-pop-out { from { opacity: 1; transform: none; } to { opacity: 0; transform: scale(0.96) translateY(8px); } } .gl-pop-in { animation: gl-pop-in var(--duration-base) var(--ease-out) backwards; } .gl-pop-out { animation: gl-pop-out var(--duration-fast) var(--ease-out) forwards; } /* Всплывающее меню/поповер: fade + сдвиг от якоря (меню растёт вверх). */ @keyframes gl-popover-in { from { opacity: 0; transform: translateY(6px) scale(0.98); } to { opacity: 1; transform: none; } } .gl-popover { transform-origin: bottom; animation: gl-popover-in var(--duration-fast) var(--ease-out) backwards; } /* * Панели, которые позиционирует `useAnchoredPosition`: до замера они скрыты, * после — прижаты к рабочей области по left/top/max-height (задаёт хук). * position: fixed нужен, чтобы панель не уезжала за пределы окна. */ .gl-anchored-panel { position: fixed; z-index: 40; visibility: hidden; } .gl-anchored-panel[data-positioned='true'] { visibility: visible; } /* Направление анимации берём из data-placement: панель может развернуться. */ .gl-anchored-panel[data-placement='top'] { transform-origin: bottom; animation: gl-popover-in var(--duration-fast) var(--ease-out) backwards; } .gl-anchored-panel[data-placement='bottom'] { transform-origin: top; animation: gl-popover-down-in var(--duration-fast) var(--ease-out) backwards; } /* То же меню, но раскрывающееся вниз (действия у сообщения). */ @keyframes gl-popover-down-in { from { opacity: 0; transform: translateY(-6px) scale(0.98); } to { opacity: 1; transform: none; } } .gl-popover-down { transform-origin: top; animation: gl-popover-down-in var(--duration-fast) var(--ease-out) backwards; } /* Подсветка сообщения, к которому перешли из поиска или от ответа. */ @keyframes gl-highlight-fade { from { background-color: color-mix(in oklch, var(--color-accent) 28%, transparent); } to { background-color: transparent; } } .gl-highlight { animation: gl-highlight-fade 1.6s var(--ease-out) backwards; } /* * Каскадное появление списка. Задержка растёт только у первых восьми * элементов — дальше список появляется без ступенек, чтобы длинные * списки не «догоняли» пользователя. */ .gl-stagger > * { animation: gl-page-in var(--duration-base) var(--ease-out) backwards; } .gl-stagger > *:nth-child(1) { animation-delay: 0ms; } .gl-stagger > *:nth-child(2) { animation-delay: calc(var(--stagger-step) * 1); } .gl-stagger > *:nth-child(3) { animation-delay: calc(var(--stagger-step) * 2); } .gl-stagger > *:nth-child(4) { animation-delay: calc(var(--stagger-step) * 3); } .gl-stagger > *:nth-child(5) { animation-delay: calc(var(--stagger-step) * 4); } .gl-stagger > *:nth-child(6) { animation-delay: calc(var(--stagger-step) * 5); } .gl-stagger > *:nth-child(7) { animation-delay: calc(var(--stagger-step) * 6); } .gl-stagger > *:nth-child(8) { animation-delay: calc(var(--stagger-step) * 7); } .gl-stagger > *:nth-child(n + 9) { animation-delay: 0ms; } /* Индикатор активной комнаты: полоска слева плавно «вырастает». */ @keyframes gl-indicator-in { from { opacity: 0; transform: translateY(-50%) scaleY(0.3); } to { opacity: 1; transform: translateY(-50%) scaleY(1); } } .gl-indicator::before { content: ''; position: absolute; left: 0; top: 50%; width: 3px; height: 60%; border-radius: 999px; background-color: var(--color-accent); transform: translateY(-50%); animation: gl-indicator-in var(--duration-base) var(--ease-out) backwards; } /* Индикатор активного сервера в рейке: кольцо, проявляющееся из центра. */ @keyframes gl-ring-in { from { opacity: 0; transform: scale(0.88); } to { opacity: 1; transform: none; } } .gl-ring::after { content: ''; position: absolute; inset: -3px; border: 2px solid var(--color-accent); border-radius: var(--radius-lg); pointer-events: none; animation: gl-ring-in var(--duration-base) var(--ease-out) backwards; } /* Скелетон загрузки: полоса с бегущим блеском. */ @keyframes gl-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } } .gl-skeleton { display: block; border-radius: var(--radius-sm); background-color: var(--color-surface-2); background-image: linear-gradient( 90deg, var(--color-surface-2) 25%, var(--color-surface-3) 50%, var(--color-surface-2) 75% ); background-size: 200% 100%; animation: gl-shimmer 1.4s linear infinite; } /* * Список друзей раскрывается снизу вверх: высота контейнера растёт от нуля * (grid 0fr → 1fr), а содержимое въезжает снизу. Свёрнутый список не занимает * места и не доступен скринридеру (`aria-hidden` ставит компонент). */ @keyframes gl-reveal-up { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } } .gl-reveal-up { display: grid; grid-template-rows: 0fr; transition: grid-template-rows var(--duration-base) var(--ease-out); } .gl-reveal-up > * { min-height: 0; overflow: hidden; } .gl-reveal-up[data-state='open'] { grid-template-rows: 1fr; } .gl-reveal-up[data-state='open'] > * { animation: gl-reveal-up var(--duration-base) var(--ease-out) backwards; } /* Стрелка-раскрывашка: развёрнутое состояние поворачивает её вниз. */ .gl-caret { display: inline-block; transform: rotate(-90deg); transition: transform var(--duration-fast) var(--ease-out); } .gl-caret-open { display: inline-block; transform: rotate(0deg); transition: transform var(--duration-fast) var(--ease-out); } /* * Композер: поле ввода и зеркало подсветки должны переносить строки одинаково. * Полосу прокрутки резервируем всегда — иначе при переполнении textarea её * ширина сузила бы текстовую область только у поля, и подсветка разъехалась бы. */ .gl-composer-scroll { scrollbar-gutter: stable; } /* * Доступность: при `prefers-reduced-motion: reduce` анимации становятся * мгновенными. Задержки каскада тоже обнуляем — иначе элементы списка * появлялись бы по очереди без анимации. */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-delay: 0ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; transition-delay: 0ms !important; scroll-behavior: auto !important; } } /* * Эффекты ника (AGENT.md 7.4): встроенный набор CSS-анимаций. При * `prefers-reduced-motion: reduce` анимации гасятся общим правилом выше, * поэтому эффект остаётся статичным градиентом или свечением. */ .gl-nick { font-weight: 600; } .gl-nick-gradient { background-image: linear-gradient(90deg, #5865f2, #eb459e, #faa61a, #5865f2); background-size: 300% 100%; -webkit-background-clip: text; background-clip: text; color: transparent; animation: gl-nick-gradient 6s linear infinite; } @keyframes gl-nick-gradient { from { background-position: 0% 50%; } to { background-position: 300% 50%; } } .gl-nick-glow { animation: gl-nick-glow 2.4s ease-in-out infinite; } @keyframes gl-nick-glow { 0%, 100% { text-shadow: 0 0 4px currentColor; } 50% { text-shadow: 0 0 10px currentColor, 0 0 18px currentColor; } } .gl-nick-fire { background-image: linear-gradient(180deg, #ffd166, #ff7b00, #d62828); -webkit-background-clip: text; background-clip: text; color: transparent; animation: gl-nick-fire 1.6s ease-in-out infinite; } @keyframes gl-nick-fire { 0%, 100% { filter: brightness(1); } 50% { filter: brightness(1.25); } } .gl-nick-shimmer { background-image: linear-gradient(100deg, currentColor 20%, #ffffff 40%, currentColor 60%); background-size: 250% 100%; -webkit-background-clip: text; background-clip: text; animation: gl-nick-shimmer 3.2s linear infinite; } @keyframes gl-nick-shimmer { from { background-position: 150% 50%; } to { background-position: -50% 50%; } } .gl-nick-glitch { animation: gl-nick-glitch 2.2s steps(2, end) infinite; } @keyframes gl-nick-glitch { 0%, 92%, 100% { text-shadow: none; } 93% { text-shadow: -2px 0 #eb459e; } 95% { text-shadow: 2px 0 #00b0f4; } 97% { text-shadow: -1px 0 #faa61a; } } /* Рамка вокруг аватара: подложка под аватаром, как в AGENT.md 7.4. */ .gl-frame { position: relative; display: inline-flex; align-items: center; justify-content: center; } .gl-frame-image { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; pointer-events: none; } .gl-frame-inner { position: relative; z-index: 1; }