Первоначальная версия VidConf

This commit is contained in:
2026-07-23 01:04:01 +03:00
commit 896455381a
335 changed files with 61527 additions and 0 deletions

View File

@@ -0,0 +1,150 @@
/*
* VidConf — стили страницы «Администрирование».
* Табличные/toolbar-паттерны — 1:1 из design/mockups/admin.html
* (.section-tab/.toolbar-row/.search-wrap/.table-card/.badge-role-*).
* Вкладка «Настройки» в макете отсутствует — построена по тому же
* визуальному языку (.settings-card повторяет .table-card/.invite-card).
* page-shell/page-main/page-head — общие из lobby.css; кнопки, модалки и
* переключатель switch — из shell.css и calendar.css.
*
* Все правила скопированы под `.admin-page`, чтобы избежать конфликта имён
* классов (`.icon-btn`, `.chip`) с другими светлыми страницами оболочки —
* в проекте уже был прецедент такой коллизии (см. комментарий в
* my-conferences.css про порядок правил в исходнике).
*/
.admin-page .section-tabs { display: flex; gap: var(--space-2); margin-bottom: var(--space-5); border-bottom: 1px solid var(--color-border); }
.admin-page .section-tab { padding: 10px 4px; margin-right: var(--space-6); background: none; border: none; border-bottom: 2px solid transparent; font: var(--text-body-lg); font-weight: 600; color: var(--color-ink-500); cursor: pointer; }
.admin-page .section-tab:hover { color: var(--color-ink-900); }
.admin-page .section-tab.is-active { color: var(--color-ink-700); border-color: var(--color-accent-active); }
.admin-page .section-tabs-inline { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: var(--space-4); }
.admin-page .chip { display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; border-radius: var(--radius-full); border: 1.5px solid var(--color-border); background: var(--color-surface); font: var(--text-body); font-weight: 600; color: var(--color-ink-500); cursor: pointer; }
.admin-page .chip.is-active { border-color: var(--color-ink-700); color: var(--color-ink-700); background: var(--color-bg-alt); }
.admin-page .toolbar-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-5); gap: var(--space-4); flex-wrap: wrap; }
/* Группа «поиск + кнопка действия» слева в toolbar-row («Добавить
пользователя») — без неё третий элемент в flex-контейнере с
justify-content: space-between распределился бы отдельной колонкой, а не
встал рядом с поиском. */
.admin-page .toolbar-left { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.admin-page .search-wrap { position: relative; width: 320px; max-width: 100%; }
.admin-page .search-wrap .icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--color-ink-400); }
/* color/::placeholder — намеренно НЕ в тёмном скоупе: раньше их не было
вовсе, текст/плейсхолдер рисовались UA-дефолтом браузера, а не хардкодом
светлой темы, — в светлой это давало почти совпадающий с ink-900/ink-300
результат, а в тёмной ломалось (тёмный UA-текст на тёмном фоне). var()
резолвится в обеих темах сам, поэтому одно правило на все три скоупа
корректно и для светлой, и для тёмной — оборачивать не в чем и незачем. */
.admin-page .search-wrap input { width: 100%; font: var(--text-body); color: var(--color-ink-900); padding: 9px 12px 9px 38px; border-radius: var(--radius-full); border: 1px solid var(--color-border); background: var(--color-surface); }
.admin-page .search-wrap input::placeholder { color: var(--color-ink-300); }
.admin-page .search-wrap input:focus { outline: none; border-color: var(--color-focus-ring); }
.admin-page .toolbar-count { font: var(--text-body); color: var(--color-ink-500); white-space: nowrap; }
.admin-page .table-card { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-2xl); overflow: hidden; margin-bottom: var(--space-6); }
.admin-page table { width: 100%; border-collapse: collapse; }
.admin-page thead th { text-align: left; background: var(--color-bg-alt); font: var(--text-caption); color: var(--color-ink-500); padding: 12px 20px; text-transform: uppercase; }
.admin-page tbody td { padding: 14px 20px; font: var(--text-body-lg); color: var(--color-ink-900); border-top: 1px solid var(--color-border); }
.admin-page tbody tr:hover { background: var(--color-bg-alt); }
.admin-page .badge-role-admin { background: var(--color-ink-700); color: #fff; }
.admin-page .badge-role-user { background: var(--color-bg-alt); color: var(--color-ink-500); }
.admin-page .row-actions { display: flex; gap: 6px; }
.admin-page .icon-btn { width: 32px; height: 32px; border-radius: var(--radius-md); border: 1px solid var(--color-border); background: var(--color-surface); color: var(--color-ink-500); display: flex; align-items: center; justify-content: center; cursor: pointer; flex-shrink: 0; }
.admin-page .icon-btn:hover:not(:disabled) { border-color: var(--color-border-strong); color: var(--color-ink-900); }
.admin-page .icon-btn.danger:hover:not(:disabled) { border-color: var(--color-danger); color: var(--color-danger); background: var(--color-danger-bg); }
.admin-page .icon-btn:disabled { opacity: .45; cursor: not-allowed; }
.admin-page .user-cell { display: flex; align-items: center; gap: 10px; }
.admin-page .user-cell .avatar { width: 32px; height: 32px; font-size: 12px; }
.admin-page .user-cell div span { display: block; font: 13px var(--font-body); color: var(--color-ink-500); }
/* Клик по имени открывает карточку профиля — визуально
остаётся обычным текстом (не похоже на ссылку), подчёркивание — только hover. */
.admin-page .user-cell-name { background: none; border: none; padding: 0; font: inherit; color: inherit; cursor: pointer; text-align: left; }
.admin-page .user-cell-name:hover { text-decoration: underline; color: var(--color-ink-700); }
.admin-page select { font: var(--text-body); color: var(--color-ink-900); padding: 6px 10px; border-radius: var(--radius-md); border: 1px solid var(--color-border); background: var(--color-surface); font-family: var(--font-body); }
.admin-page select:disabled { opacity: .5; cursor: not-allowed; }
.admin-page select:focus { outline: none; border-color: var(--color-focus-ring); }
.admin-page .pagination-row { display: flex; justify-content: center; gap: var(--space-3); margin-bottom: var(--space-8); }
/* ---------- Вкладка «Команды» (нет в макете — паттерн toolbar-row/table-card) ---------- */
/* color/::placeholder — то же обоснование, что у .search-wrap input выше. */
.admin-page .team-add-form { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.admin-page .team-add-form input { width: 280px; max-width: 100%; font: var(--text-body); color: var(--color-ink-900); padding: 9px 14px; border-radius: var(--radius-full); border: 1px solid var(--color-border); background: var(--color-surface); }
.admin-page .team-add-form input::placeholder { color: var(--color-ink-300); }
.admin-page .team-add-form input:focus { outline: none; border-color: var(--color-focus-ring); }
.admin-page .team-rename-form input { width: 240px; max-width: 100%; font: var(--text-body-lg); color: var(--color-ink-900); padding: 6px 10px; border-radius: var(--radius-md); border: 1px solid var(--color-border-strong); background: var(--color-surface); }
.admin-page .team-rename-form input:focus { outline: none; border-color: var(--color-focus-ring); }
/* ---------- Вкладка «Настройки» (нет в макете — паттерн table-card/invite-card) ---------- */
/* Соседние карточки внутри строки грида должны быть одной высоты: без
align-items:stretch (значение по умолчанию для grid) каждая плитка
сжималась бы по своему контенту, и при разном наполнении («Модули» с
четырьмя переключателями против правой колонки, «Рассылка саммари»
против «Часового пояса») ряд выглядел рваным. */
.admin-page .settings-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-5); }
.admin-page .settings-card { display: flex; flex-direction: column; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-2xl); padding: var(--space-6); }
.admin-page .settings-card h2 { font: var(--text-h2); margin: 0 0 4px; color: var(--color-ink-900); }
.admin-page .settings-card .desc { font: var(--text-body); color: var(--color-ink-500); margin: 0 0 var(--space-4); }
.admin-page .settings-actions { grid-column: 1 / -1; display: flex; justify-content: flex-end; }
/* Первый ряд грида: левая плитка «Модули» (grid-item, растягивается по
умолчанию align-items:stretch) и правая колонка — вертикальный стек из
«Уровень AI» и «Эталон mail-домена». Стек как grid-item тоже растягивается
до высоты ряда; внутри него flex-grow на карточках распределяет остаток
высоты поровну между двумя правыми плитками, поэтому итоговая высота
левой и суммарная высота правых двух совпадают без фиксации пикселей.
Важно: flex-basis только auto и flex-shrink:0 — карточка не должна
ужиматься ниже высоты своего контента (список уровней AI/подпись+инпут),
иначе при `flex-basis: 0` контент переполняет сжатый бокс и визуально
наезжает на соседнюю плитку снизу (overflow: visible по умолчанию не
обрезает переполнение, а просто рисует поверх соседа). */
.admin-page .settings-stack { display: flex; flex-direction: column; gap: var(--space-5); }
.admin-page .settings-stack .settings-card { flex: 1 0 auto; }
/* Инпут эталонного домена дизейблится при выключенном тумблере верификации,
но значение поля при этом не сбрасывается (сохраняется в стейте формы). */
.admin-page .settings-card .field input:disabled { opacity: .55; cursor: not-allowed; background: var(--color-bg-alt); }
/* Тело карточки занимает всё оставшееся место после заголовка/описания —
именно за счёт flex:1 плитка ниже соседней по контенту получает
аккуратно распределённую пустоту, а не пустой хвост снизу. */
.admin-page .settings-card-body { display: flex; flex-direction: column; flex: 1; }
.admin-page .settings-card-body--spread { justify-content: space-between; }
.admin-page .settings-card-body--center { justify-content: center; }
.admin-page .ai-level-list { display: flex; flex-direction: column; gap: var(--space-3); }
.admin-page .ai-level-option { display: flex; align-items: flex-start; gap: 10px; padding: var(--space-3); border: 1px solid var(--color-border); border-radius: var(--radius-md); cursor: pointer; }
.admin-page .ai-level-option:hover { border-color: var(--color-border-strong); }
.admin-page .ai-level-option.is-disabled { cursor: not-allowed; opacity: .6; }
.admin-page .ai-level-option input[type="radio"] { margin-top: 3px; width: 16px; height: 16px; accent-color: var(--color-ink-700); flex-shrink: 0; }
.admin-page .ai-level-option strong { display: block; font: var(--text-body); font-weight: 600; color: var(--color-ink-900); }
/* align-items: flex-start (не center) — причины недоступности
Блок 1) могут быть длинными («модель X не скачана — запустите install.sh
с пресетом N») и переноситься на несколько строк; при center иконка
съезжала бы к середине блока текста, а не к его первой строке. */
.admin-page .ai-level-reason { display: flex; align-items: flex-start; gap: 5px; font: var(--text-caption); color: var(--color-warning); margin-top: 4px; }
.admin-page .ai-level-reason svg { flex-shrink: 0; margin-top: 1px; }
@media (max-width: 960px) {
.admin-page .settings-grid { grid-template-columns: 1fr; }
}
/* ---------- Футер админки ----------
Версия инстанса из `/api/health` (см. AdminPage.tsx) — до появления поля
на backend строка сокращается до одного «VidConf» без версии. */
.admin-page .admin-footer { margin-top: var(--space-6); padding-top: var(--space-4); border-top: 1px solid var(--color-border); font: var(--text-caption); color: var(--color-ink-400); text-align: center; }
/* =====================================================================
* Тёмная тема: заливка var(--color-ink-700) (индиго) сочеталась с жёстким
* color:#fff; в тёмной теме ink-700 = мята (светлый тон) — белый текст
* перестаёт читаться, заменено на var(--color-accent-text) (тёмный, для
* заливки мятой). См. design/mockups/dark/README.md, категория 1.
* ===================================================================== */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) .admin-page .badge-role-admin { color: var(--color-accent-text); }
}
:root[data-theme='dark'] .admin-page .badge-role-admin { color: var(--color-accent-text); }

View File

@@ -0,0 +1,270 @@
/*
* VidConf — стили экранов auth (вход/регистрация/подтверждение email).
* 1:1 воспроизводит design/mockups/auth.html.
*/
.layout { display: flex; min-height: 100vh; }
/* ---------- Брендовая панель ---------- */
.brand-panel {
width: 42%;
background: linear-gradient(160deg, #EDEFFA 0%, #F1F1F1 55%, #F7F7F8 100%);
padding: var(--space-12);
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
overflow: hidden;
border-right: 1px solid var(--color-border);
}
.brand-panel::after {
content: "";
position: absolute;
right: -120px;
bottom: -120px;
width: 380px;
height: 380px;
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, rgba(212, 242, 227, 0.45), rgba(212, 242, 227, 0) 70%);
}
.brand-copy { z-index: 1; }
.eyebrow {
font: var(--text-caption);
letter-spacing: 0.08em;
color: var(--color-ink-400);
margin: 0 0 var(--space-4);
}
.brand-headline {
font: var(--text-display-lg);
font-family: var(--font-display);
color: var(--color-ink-700);
margin: 0 0 var(--space-4);
max-width: 460px;
}
.brand-sub {
font: var(--text-body-lg);
color: var(--color-ink-500);
max-width: 420px;
margin: 0;
}
.brand-stats { display: flex; gap: var(--space-4); z-index: 1; margin-top: var(--space-8); }
.stat-glass {
background: var(--color-surface-glass);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.7);
box-shadow: var(--shadow-glass);
border-radius: var(--radius-xl);
padding: var(--space-4) var(--space-5);
min-width: 140px;
}
.stat-glass strong { display: block; font: var(--text-h1); font-family: var(--font-display); color: var(--color-ink-700); }
.stat-glass span { font: var(--text-caption); color: var(--color-ink-500); text-transform: none; letter-spacing: normal; }
/* ---------- Форма-панель ---------- */
.form-panel {
width: 58%;
padding: var(--space-12) var(--space-16);
display: flex;
flex-direction: column;
justify-content: center;
}
.screen-block { max-width: 440px; width: 100%; }
.screen-title { font: var(--text-h1); color: var(--color-ink-900); margin: 0 0 var(--space-8); }
.tabs { display: flex; gap: var(--space-2); margin-bottom: var(--space-6); background: var(--color-bg-alt); padding: 4px; border-radius: var(--radius-full); width: fit-content; }
.tab {
padding: 8px 20px;
border-radius: var(--radius-full);
border: none;
background: transparent;
font: var(--text-body);
color: var(--color-ink-500);
text-decoration: none;
display: inline-flex;
}
.tab.is-active { background: var(--color-surface); color: var(--color-ink-700); font-weight: 600; box-shadow: var(--shadow-sm); }
.field { margin-bottom: var(--space-5); }
.field label { display: block; font: var(--text-body); font-weight: 600; color: var(--color-ink-900); margin-bottom: var(--space-2); }
.input-wrap { position: relative; display: flex; align-items: center; }
.input-wrap .field-icon { position: absolute; left: 14px; color: var(--color-ink-400); pointer-events: none; }
.input-wrap input, .input-wrap select {
width: 100%;
font: var(--text-body-lg);
font-family: var(--font-body);
color: var(--color-ink-900);
padding: 11px 14px 11px 42px;
border-radius: var(--radius-md);
border: 1px solid var(--color-border);
background: var(--color-surface);
outline: none;
}
.input-wrap input::placeholder { color: var(--color-ink-300); }
.input-wrap input:hover, .input-wrap select:hover { border-color: var(--color-border-strong); }
.input-wrap input:focus, .input-wrap select:focus { border-color: var(--color-focus-ring); box-shadow: 0 0 0 3px rgba(59, 77, 149, 0.12); }
/* Нативный select в WebKit/Safari игнорирует border/border-radius/background
без явного отключения UA-рендеринга: без appearance:none контрол рисуется
системным виджетом («аквариумная» рамка ОС) поверх авторских стилей — этим
объясняется «браузерный дефолт» без скругления и с меньшей высотой. */
.input-wrap select {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
padding-right: 38px;
cursor: pointer;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%236976AC' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 14px center;
background-size: 16px;
}
.input-wrap .toggle-eye {
position: absolute;
right: 14px;
left: auto;
background: none;
border: none;
padding: 0;
cursor: pointer;
color: var(--color-ink-400);
display: flex;
}
.field.has-error input { border-color: var(--color-danger); box-shadow: 0 0 0 3px rgba(199, 54, 79, 0.1); }
.error-msg { display: flex; align-items: center; gap: 6px; margin-top: var(--space-2); font: var(--text-body); color: var(--color-danger); }
.hint-msg { margin-top: var(--space-2); font: var(--text-body); color: var(--color-ink-500); }
.row-between { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-6); }
.checkbox-row { display: flex; align-items: center; gap: 8px; font: var(--text-body); color: var(--color-ink-500); }
.link { color: var(--color-ink-700); font-weight: 600; text-decoration: none; background: none; border: none; cursor: pointer; font: var(--text-body); }
.link:hover { text-decoration: underline; }
.btn-block { width: 100%; }
.btn-primary.btn-lg { font: var(--text-body-lg); font-weight: 700; padding: 13px 24px; }
.btn-secondary.btn-lg { font: var(--text-body-lg); font-weight: 600; padding: 12px 24px; }
/* ---------- Подтверждение email ---------- */
.confirm-card {
max-width: 440px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-2xl);
padding: var(--space-8);
text-align: left;
}
.confirm-icon {
width: 56px;
height: 56px;
border-radius: var(--radius-full);
background: var(--color-success-bg);
color: var(--color-success);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: var(--space-5);
}
.confirm-card h3 { font: var(--text-h2); color: var(--color-ink-900); margin: 0 0 var(--space-2); }
.confirm-card p { font: var(--text-body-lg); color: var(--color-ink-500); margin: 0 0 var(--space-6); }
.confirm-card .mail-chip {
display: inline-flex;
align-items: center;
gap: 8px;
background: var(--color-bg-alt);
border-radius: var(--radius-full);
padding: 6px 14px;
font: var(--text-mono-sm);
color: var(--color-ink-700);
margin-bottom: var(--space-6);
}
.confirm-card.pending .confirm-icon { background: var(--color-warning-bg); color: var(--color-warning); }
.confirm-card.error .confirm-icon { background: var(--color-danger-bg); color: var(--color-danger); }
/* ---------- Переключатель темы: плавающая плашка (см. AuthLayout.tsx) ---------- */
.theme-toggle--floating {
position: fixed;
top: 24px;
right: 32px;
z-index: 50;
box-shadow: var(--shadow-md);
}
/* =====================================================================
* Тёмная тема: точечные патчи (design/mockups/dark/README.md, категория
* «декоративные градиенты/тени») — литеральные rgba построены на индиго/
* старом danger-hex светлой темы, поэтому не перекрашиваются через var().
* ===================================================================== */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) .brand-panel {
background: linear-gradient(160deg, #242424 0%, #1E1E1E 55%, #1A1A1A 100%);
}
:root:not([data-theme]) .brand-panel::after {
background: radial-gradient(circle at 30% 30%, rgba(127, 221, 168, 0.16), rgba(127, 221, 168, 0) 70%);
}
:root:not([data-theme]) .stat-glass { border-color: rgba(255, 255, 255, 0.08); }
}
:root[data-theme='dark'] .brand-panel {
background: linear-gradient(160deg, #242424 0%, #1E1E1E 55%, #1A1A1A 100%);
}
:root[data-theme='dark'] .brand-panel::after {
background: radial-gradient(circle at 30% 30%, rgba(127, 221, 168, 0.16), rgba(127, 221, 168, 0) 70%);
}
:root[data-theme='dark'] .stat-glass { border-color: rgba(255, 255, 255, 0.08); }
/* ---------- Узкие экраны ---------- */
/* ВАЖНО: этот блок должен идти строго ПОСЛЕДНИМ в файле — при равной
специфичности с безусловными правилами выше (`.brand-panel`, `.form-panel`
и т.п. — те же 0,1,0) побеждает порядок в исходнике, а не сам факт
совпадения media query (тот же баг уже ловили в lobby.css/.action, гейт 3
— см. комментарий там). Макет auth.html — desktop-only
(`html { min-width: 1280px }`), адаптива в нём нет.
Причина исходного бага: `.layout` — flex-row из двух панелей с width
в процентах и без flex-shrink:0, а `.brand-panel` — overflow:hidden (нужен
для декоративного круга). По спецификации flex, overflow:hidden отключает
автоматический min-width:auto (= min-content) и делает его 0 — поэтому
при узком viewport панель схлопывалась намного уже своего текста, а не
переносила его: overflow:hidden просто обрезал то, что не поместилось,
вместо переноса строк. Решение — не подбирать min-width, а на узких
экранах складывать колонки в одну: тогда ширина каждой панели равна 100%
и конкуренции за пространство между ними нет вовсе. */
@media (max-width: 900px) {
.layout {
flex-direction: column;
min-height: auto;
}
.brand-panel {
width: 100%;
min-width: 0;
padding: var(--space-8) var(--space-6) var(--space-6);
border-right: none;
border-bottom: 1px solid var(--color-border);
}
.brand-panel::after {
width: 220px;
height: 220px;
right: -90px;
bottom: -90px;
}
.brand-headline { max-width: none; }
.brand-sub { max-width: none; }
.brand-stats { flex-wrap: wrap; }
.stat-glass { flex: 1 1 140px; min-width: 0; }
.form-panel {
width: 100%;
min-width: 0;
padding: var(--space-8) var(--space-6);
justify-content: flex-start;
}
.screen-block { max-width: 100%; }
}
@media (max-width: 480px) {
/* Совсем узкий телефон (375px) — экономим отступы и убираем декоративный
круг: на компактном hero он ближе к тексту и уже не читается как акцент. */
.brand-panel { padding: var(--space-6) var(--space-4); }
.brand-panel::after { display: none; }
.brand-stats { gap: var(--space-3); }
.stat-glass { flex-basis: 100%; }
.form-panel { padding: var(--space-6) var(--space-4) var(--space-10); }
}

View File

@@ -0,0 +1,273 @@
/*
* VidConf — стили экрана календаря конференций: менеджер
* запланированных/закреплённых конференций (переговорных комнат и их
* занятости в модели нет). Базовая раскладка/токены — 1:1 из design/mockups/calendar.html
* (.layout-grid, .week-card, .create-card, .pin-block, .recurrence-panel).
* Тема FullCalendar адаптирована под dayGrid-«чипы» вместо timeGrid-слотов
* (см. комментарий в ConferenceCalendar.tsx).
*/
/* ---------- Грид: сетка вхождений + карточка создания/редактирования ---------- */
.layout-grid {
display: grid;
grid-template-columns: 1fr 400px;
gap: var(--space-6);
align-items: start;
}
/* min-width:0 — без этого грид-элемент с 1fr-треком не сжимается уже своего
min-content (тут это внутренняя таблица FullCalendar с 5 колонками) —
классический grid blowout: трек и вся страница раздуваются шире вьюпорта,
появляется горизонтальный скролл (см. тот же приём в lobby.css/.action). */
.layout-grid > * { min-width: 0; }
.week-card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-2xl);
padding: var(--space-6);
}
.week-toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-5); flex-wrap: wrap; gap: var(--space-3); }
.date-nav { display: flex; align-items: center; gap: var(--space-3); }
.date-nav-arrow {
width: 34px;
height: 34px;
flex-shrink: 0;
border-radius: 50%;
border: 1px solid var(--color-border);
background: var(--color-surface);
color: var(--color-ink-500);
display: flex;
align-items: center;
justify-content: center;
}
.date-nav-arrow:hover { border-color: var(--color-border-strong); color: var(--color-ink-900); }
.date-nav span { font: var(--text-h3); color: var(--color-ink-900); min-width: 200px; text-align: center; text-transform: capitalize; }
.date-nav .btn-secondary { flex-shrink: 0; white-space: nowrap; }
.view-toggle { display: flex; gap: 4px; background: var(--color-bg-alt); border: 1px solid var(--color-border); border-radius: var(--radius-full); padding: 4px; }
.view-toggle button { border: none; background: transparent; border-radius: var(--radius-full); padding: 7px 16px; font: var(--text-body); font-weight: 600; color: var(--color-ink-500); }
.view-toggle button.is-active { background: var(--color-surface); color: var(--color-ink-700); box-shadow: var(--shadow-sm); }
/* ---------- Тема FullCalendar (dayGrid) ---------- */
.vc-calendar { font-family: var(--font-body); }
.vc-calendar .fc { --fc-border-color: var(--color-border); --fc-page-bg-color: var(--color-surface); --fc-neutral-bg-color: var(--color-bg-alt); --fc-today-bg-color: rgba(212,242,227,.16); }
.vc-calendar .fc-theme-standard td, .vc-calendar .fc-theme-standard th { border-color: var(--color-border); }
.vc-calendar .fc-col-header-cell { background: var(--color-bg-alt); padding: 6px 0; }
.vc-calendar .fc-col-header-cell-cushion { font: var(--text-caption); color: var(--color-ink-500); text-decoration: none; padding: 6px 0; }
.vc-calendar .fc-daygrid-day-frame { min-height: 460px; padding: var(--space-2); }
.vc-calendar .fc-daygrid-day-number { font: var(--text-h3); color: var(--color-ink-900); padding: 4px 6px; }
.vc-calendar .fc-day-today { background: rgba(212,242,227,.16) !important; }
.vc-calendar .fc-day-today .fc-daygrid-day-number { color: var(--color-ink-700); }
.vc-calendar .fc-daygrid-day-events { margin: 0; display: flex; flex-direction: column; gap: var(--space-2); }
.vc-calendar .fc-daygrid-event-harness { margin: 0 !important; }
.vc-calendar .fc-daygrid-more-link { font: var(--text-caption); color: var(--color-ink-500); }
/*
* Событие-вхождение конференции — карточка-«чип» в духе .conf-chip мокапа.
* FullCalendar оборачивает событие в .fc-daygrid-event (наследует классы из
* `classNames`) — переопределяем его внешний вид под чип целиком, без
* отдельного контейнера.
*/
.vc-calendar .fc-daygrid-event.conf-chip {
display: flex !important;
flex-direction: column;
gap: 4px;
background: var(--color-surface) !important;
border: 1px solid var(--color-border) !important;
border-radius: var(--radius-md) !important;
padding: var(--space-3);
white-space: normal;
cursor: pointer;
transition: box-shadow var(--transition-fast), border-color var(--transition-fast);
}
.vc-calendar .fc-daygrid-event.conf-chip:hover { box-shadow: var(--shadow-sm); border-color: var(--color-border-strong); }
.vc-calendar .fc-daygrid-event.conf-chip .fc-event-time,
.vc-calendar .fc-daygrid-event.conf-chip .fc-event-title { display: none; } /* заменено собственным eventContent */
.chip-time { display: block; font: var(--text-caption); color: var(--color-ink-500); text-transform: none; letter-spacing: normal; }
.chip-title { display: flex; align-items: center; gap: 6px; font: var(--text-body); font-weight: 700; color: var(--color-ink-900); }
.chip-lock { width: 13px; height: 13px; flex-shrink: 0; }
.conf-badge { display: inline-flex; align-items: center; gap: 4px; padding: 2px 9px; border-radius: var(--radius-full); font: var(--text-caption); font-size: 11px; width: fit-content; }
.conf-badge.scheduled { background: var(--color-warning-bg); color: var(--color-warning); }
.conf-badge.pinned { background: var(--color-bg-alt); color: var(--color-ink-700); border: 1px solid var(--color-border-strong); }
.conf-badge.live { background: var(--color-success-bg); color: var(--color-success); }
.conf-badge.ended { background: var(--color-bg-alt); color: var(--color-ink-500); }
.vc-empty-hint { text-align: center; padding: var(--space-8); color: var(--color-ink-500); font: var(--text-body-lg); }
/* ---------- Карточка создания/редактирования конференции ---------- */
.create-card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-2xl);
padding: var(--space-6);
position: sticky;
top: 96px;
}
.create-card h2 { font: var(--text-h2); color: var(--color-ink-900); margin: 0 0 var(--space-5); display: flex; align-items: center; gap: 8px; }
.field-row { display: flex; gap: var(--space-3); }
.field-row .field { flex: 1; }
.next-occurrence { display: flex; align-items: center; gap: 8px; background: var(--color-bg-alt); border-radius: var(--radius-md); padding: 10px 14px; font: var(--text-body); color: var(--color-ink-700); font-weight: 600; margin-bottom: var(--space-5); }
/* Тумблер (используется и для «закрытая по паролю», и для «закрепить») */
.toggle-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); padding: var(--space-3) 0; border-top: 1px solid var(--color-border); }
.toggle-copy strong { display: block; font: var(--text-body); color: var(--color-ink-900); }
.toggle-copy span { font: 12px var(--font-body); color: var(--color-ink-500); }
.switch { position: relative; width: 44px; height: 24px; flex-shrink: 0; }
.switch input { opacity: 0; width: 0; height: 0; position: absolute; }
.slider { position: absolute; inset: 0; background: var(--color-border-strong); border-radius: var(--radius-full); transition: background var(--transition-fast); cursor: pointer; }
.slider::before { content:""; position:absolute; width:18px; height:18px; left:3px; top:3px; background:#fff; border-radius:50%; transition: transform var(--transition-fast); }
.switch input:checked + .slider { background: var(--color-accent-active); }
.switch input:checked + .slider::before { transform: translateX(20px); }
.switch input:focus-visible + .slider { outline: 2px solid var(--color-focus-ring); outline-offset: 2px; }
/* Disabled-переключатель (напр. анонс записи конференций в «Модулях»
админки) — приглушение вместо hover/checked-цвета. */
.switch input:disabled + .slider { opacity: .5; cursor: not-allowed; }
.password-block { margin-top: var(--space-4); }
/* ---------- Блок повторений (recurrence) ---------- */
.recurrence-panel {
display: flex;
flex-direction: column;
gap: var(--space-2);
margin-top: var(--space-4);
padding-top: var(--space-4);
border-top: 1px dashed var(--color-border);
}
.recurrence-option-row {
display: flex; align-items: center; gap: 10px;
padding: 10px 12px; border: 1px solid var(--color-border); border-radius: var(--radius-md);
cursor: pointer; font: var(--text-body); font-weight: 600; color: var(--color-ink-900);
transition: border-color var(--transition-fast), background var(--transition-fast);
}
.recurrence-option-row:has(input:checked) { border-color: var(--color-ink-700); background: var(--color-bg-alt); }
.recurrence-option-row input[type="radio"] { width: 16px; height: 16px; accent-color: var(--color-ink-700); flex-shrink: 0; }
.recurrence-sub { padding: var(--space-3) 12px 4px 38px; }
.weekday-toggles { display: flex; gap: 6px; flex-wrap: wrap; }
.weekday-chip {
width: 34px; height: 34px; border-radius: 50%;
border: 1.5px solid var(--color-border); background: var(--color-surface);
font: var(--text-caption); color: var(--color-ink-500);
display: flex; align-items: center; justify-content: center;
cursor: pointer;
}
.weekday-chip.is-selected { background: var(--color-ink-700); border-color: var(--color-ink-700); color: #fff; }
.recurrence-hint { font: 12px var(--font-body); color: var(--color-ink-500); margin-top: 8px; }
/* ---------- Кнопки действий карточки редактирования ---------- */
.btn-row { display: flex; gap: var(--space-3); margin-top: var(--space-5); }
.btn-row .btn { flex: 1; justify-content: center; }
.danger-link-row { text-align: center; margin-top: var(--space-4); }
.btn-text { background: none; border: none; color: var(--color-danger); font: var(--text-body); font-weight: 600; padding: 4px; cursor: pointer; }
.btn-text:hover { text-decoration: underline; }
.btn.full { width: 100%; justify-content: center; margin-top: var(--space-5); }
.btn-danger { background: var(--color-danger); color: #fff; font: var(--text-body); font-weight: 700; padding: 9px 18px; border-radius: var(--radius-full); border: none; cursor: pointer; display: inline-flex; align-items: center; gap: 8px; }
.btn-danger:hover:not(:disabled) { filter: brightness(0.92); }
/* ---------- Пикер участников (ParticipantsPicker) ----------
* Новый элемент без готового макета — собран из утверждённых паттернов:
* чипы как `.chip` фильтров «Моих конференций», выпадающий список результатов
* переиспользует классы дроплиста селекта (`.vc-select-panel`/`.vc-select-option`,
* §4.16), поле поиска — как `.search-wrap` админки.
*/
.participants-picker { position: relative; }
.participants-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: var(--space-3); }
.participant-chip {
display: inline-flex; align-items: center; gap: 6px;
padding: 4px 10px 4px 4px; border-radius: var(--radius-full);
border: 1px solid var(--color-border); background: var(--color-bg-alt);
font: var(--text-body); font-weight: 600; color: var(--color-ink-900);
}
.participant-chip.is-organizer { padding-right: 12px; border-color: var(--color-border-strong); }
.participant-chip-role { font: var(--text-caption); color: var(--color-ink-500); font-weight: 600; }
.participant-chip-remove {
width: 18px; height: 18px; flex-shrink: 0; border-radius: 50%; border: none;
background: transparent; color: var(--color-ink-400);
display: flex; align-items: center; justify-content: center; cursor: pointer;
}
.participant-chip-remove:hover:not(:disabled) { background: var(--color-border); color: var(--color-ink-900); }
.participant-chip-remove:disabled { cursor: not-allowed; opacity: .5; }
.participants-search-wrap { position: relative; }
.participants-search-wrap .icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--color-ink-400); }
.participants-search-wrap input {
width: 100%; font: var(--text-body-lg); color: var(--color-ink-900);
padding: 10px 14px 10px 38px; border-radius: var(--radius-md);
border: 1px solid var(--color-border); background: var(--color-surface); font-family: var(--font-body);
}
.participants-search-wrap input:focus { outline: none; border-color: var(--color-focus-ring); box-shadow: 0 0 0 2px var(--color-focus-ring); }
.participants-search-wrap input:disabled { background: var(--color-bg-alt); color: var(--color-ink-300); cursor: not-allowed; }
.participants-results { top: calc(100% + 6px); max-height: 240px; }
.participant-option-body { display: flex; align-items: center; gap: 8px; font: var(--text-body); color: var(--color-ink-900); }
.participants-results-hint { padding: 10px 14px; font: var(--text-body); color: var(--color-ink-500); }
/* ---------- Диалог быстрого просмотра вхождения (ConferenceOccurrenceDialog) ---------- */
.detail-row { display: flex; align-items: flex-start; gap: 10px; padding: 10px 0; border-bottom: 1px solid var(--color-border); font: var(--text-body); color: var(--color-ink-900); }
.detail-row:last-of-type { border-bottom: none; }
.detail-row svg { color: var(--color-ink-400); margin-top: 2px; flex-shrink: 0; }
.detail-row .label { color: var(--color-ink-500); min-width: 90px; flex-shrink: 0; }
.title-lock { width: 15px; height: 15px; margin-right: 6px; flex-shrink: 0; }
/* =====================================================================
* Тёмная тема: точечные патчи (design/mockups/dark/README.md).
* ===================================================================== */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) .vc-calendar .fc { --fc-today-bg-color: rgba(127, 221, 168, 0.10); }
:root:not([data-theme]) .vc-calendar .fc-day-today { background: rgba(127, 221, 168, 0.10) !important; }
:root:not([data-theme]) .weekday-chip.is-selected { color: var(--color-accent-text); }
:root:not([data-theme]) .btn-danger { background: var(--color-danger-solid); }
:root:not([data-theme]) .btn-danger:hover:not(:disabled) { background: var(--color-danger-solid-hover); filter: none; }
}
:root[data-theme='dark'] .vc-calendar .fc { --fc-today-bg-color: rgba(127, 221, 168, 0.10); }
:root[data-theme='dark'] .vc-calendar .fc-day-today { background: rgba(127, 221, 168, 0.10) !important; }
:root[data-theme='dark'] .weekday-chip.is-selected { color: var(--color-accent-text); }
:root[data-theme='dark'] .btn-danger { background: var(--color-danger-solid); }
:root[data-theme='dark'] .btn-danger:hover:not(:disabled) { background: var(--color-danger-solid-hover); filter: none; }
/* ---------- Узкие экраны ---------- */
/* ВАЖНО: этот блок должен идти строго ПОСЛЕДНИМ в файле — при равной
специфичности с безусловными правилами выше (`.create-card`, `.field-row`
и т.п. — те же 0,1,0/0,1,1) побеждает порядок в исходнике, а не сам факт
совпадения media query (тот же баг уже ловили в lobby.css/.action, гейт 3
— см. комментарий там). Макет calendar.html — desktop-only
(`html { min-width: 1280px }`, DESIGN_SYSTEM.md §«Breakpoints»), адаптива
в нём нет — раскладка ниже подобрана по здравому смыслу: календарь сверху,
карточка создания/редактирования — под ним (сохраняем порядок вёрстки).
*/
@media (max-width: 960px) {
/* Двухколоночный грид (сетка + сайдбар 400px) → одна колонка, сайдбар
перестаёт быть sticky (в потоке одной колонки залипание при скролле
страницы выглядит и работает хуже, чем простое следование за контентом). */
.layout-grid { grid-template-columns: 1fr; }
.create-card { position: static; top: auto; }
}
@media (max-width: 640px) {
.week-card, .create-card { padding: var(--space-4); }
.week-toolbar { flex-direction: column; align-items: stretch; }
.date-nav { justify-content: space-between; }
.date-nav span { min-width: 0; flex: 1; }
/* Недельная сетка FullCalendar (5 рабочих дней) не помещается на ~360px
содержимого карточки без потери читаемости — даём ей прокручиваться
ГОРИЗОНТАЛЬНО ВНУТРИ карточки (min-width задаёт компактную, но ещё
читаемую ширину колонки), а не раздувать всю страницу (см. min-width:0
на .layout-grid выше — здесь наоборот сознательно возвращаем разумный
min-width самому графику, но уже внутри своего overflow-контейнера). */
.vc-calendar { overflow-x: auto; }
.vc-calendar .fc { min-width: 560px; }
.field-row { flex-wrap: wrap; }
.recurrence-sub { padding-left: var(--space-4); }
}

View File

@@ -0,0 +1,80 @@
/*
* VidConf — стили экрана подключения к конференции.
* 1:1 воспроизводит design/mockups/join.html (3 состояния одного флоу).
* Экран публичный (доступен гостю без аккаунта), поэтому не переиспользует
* `.page-shell`/`ShellTopbar` — свой минимальный топбар с брендом.
*/
.join-shell { min-height: 100vh; display: flex; flex-direction: column; background: var(--color-bg); }
.join-topbar { display: flex; align-items: center; justify-content: space-between; padding: var(--space-5) var(--space-8); }
.join-main {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--space-6) var(--space-8) var(--space-16);
}
.screen-block { width: 100%; max-width: 440px; }
.flow-card { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-2xl); padding: var(--space-8); }
.flow-icon {
width: 52px; height: 52px;
border-radius: var(--radius-full);
background: var(--color-bg-alt);
color: var(--color-ink-700);
display: flex; align-items: center; justify-content: center;
margin: 0 auto var(--space-5);
}
.flow-icon .icon { width: 24px; height: 24px; }
.flow-card h1 { font: var(--text-h1); color: var(--color-ink-900); margin: 0 0 var(--space-2); text-align: center; }
.flow-card .flow-sub { font: var(--text-body-lg); color: var(--color-ink-500); margin: 0 0 var(--space-6); text-align: center; }
.field-optional { font-weight: 400; color: var(--color-ink-400); }
.input-wrap { position: relative; }
.input-wrap .icon {
position: absolute; left: 14px; top: 50%; transform: translateY(-50%);
color: var(--color-ink-400);
width: 20px; height: 20px;
}
.input-wrap input {
width: 100%; font: var(--text-body-lg); color: var(--color-ink-900);
padding: 11px 14px 11px 42px; border-radius: var(--radius-md); border: 1px solid var(--color-border);
background: var(--color-surface); outline: none;
}
.input-wrap input::placeholder { color: var(--color-ink-300); }
.input-wrap input:hover { border-color: var(--color-border-strong); }
.input-wrap input:focus { border-color: var(--color-focus-ring); box-shadow: 0 0 0 3px rgba(59, 77, 149, 0.12); }
.input-wrap .toggle-eye {
left: auto; right: 14px; top: 50%; transform: translateY(-50%);
background: none; border: none; padding: 0; cursor: pointer; color: var(--color-ink-400);
}
.field.has-error input { border-color: var(--color-danger); box-shadow: 0 0 0 3px rgba(199, 54, 79, 0.1); }
.error-msg { display: flex; align-items: center; gap: 6px; margin-top: var(--space-2); font: var(--text-body); color: var(--color-danger); }
.link-back { display: block; text-align: center; margin-top: var(--space-5); font: var(--text-body); font-weight: 600; color: var(--color-ink-500); text-decoration: none; background: none; border: none; width: 100%; cursor: pointer; }
.link-back:hover { color: var(--color-ink-700); }
/* Плашка целевой конференции (состояния «представиться»/«пароль») */
.target-conf { display: flex; align-items: center; gap: 12px; background: var(--color-bg-alt); border-radius: var(--radius-lg); padding: var(--space-4); margin-bottom: var(--space-6); }
.target-conf .icon-wrap {
width: 40px; height: 40px; border-radius: 50%;
background: var(--color-surface); border: 1px solid var(--color-border); color: var(--color-ink-700);
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.target-conf .icon-wrap .icon { width: 18px; height: 18px; }
.target-conf strong { display: block; font: var(--text-body); color: var(--color-ink-900); }
.target-conf span { font: var(--text-mono-sm); color: var(--color-ink-500); }
/* =====================================================================
* Тёмная тема: точечные патчи (design/mockups/dark/README.md).
* ===================================================================== */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) .input-wrap input:focus { box-shadow: 0 0 0 3px rgba(214, 168, 61, 0.18); }
:root:not([data-theme]) .field.has-error input { box-shadow: 0 0 0 3px rgba(235, 147, 161, 0.18); }
}
:root[data-theme='dark'] .input-wrap input:focus { box-shadow: 0 0 0 3px rgba(214, 168, 61, 0.18); }
:root[data-theme='dark'] .field.has-error input { box-shadow: 0 0 0 3px rgba(235, 147, 161, 0.18); }

View File

@@ -0,0 +1,210 @@
/*
* VidConf — стили экрана лобби.
*
* `.page-shell`/`.page-main`/`.page-head` — общая разметка светлой оболочки,
* переиспользуется и другими страницами (см. CalendarPage.tsx), поэтому эти
* три класса общие и не привязаны к разметке лобби.
*
* Остальное — 1:1 воспроизводит хаб действий design/mockups/lobby.html
* (мозаика из 4 действий + медальон логотипа в центре пересечения).
*/
.page-shell { min-height: 100vh; background: var(--color-bg); }
main.page-main { padding: var(--space-8); max-width: 1440px; margin: 0 auto; }
.page-head { display: flex; align-items: flex-end; justify-content: space-between; margin-bottom: var(--space-6); }
.page-head h1 { font: var(--text-display-lg); font-family: var(--font-display); color: var(--color-ink-700); margin: 0 0 6px; }
.page-head p { font: var(--text-body-lg); color: var(--color-ink-500); margin: 0; }
/* Общая адаптивная подстройка контейнера/шапки страниц оболочки (Calendar/
MyConferences/Admin) — узкие экраны не должны получать горизонтальную
прокрутку из-за фиксированных отступов space-8. Раскладка контента внутри
каждой страницы (сетки/сайдбары) — зона соответствующих блоков. */
@media (max-width: 640px) {
main.page-main { padding: var(--space-5) var(--space-4); }
.page-head { flex-direction: column; align-items: flex-start; gap: var(--space-3); }
}
/* ---------- Лобби-хаб (мозаика действий) ---------- */
.lobby-shell {
min-height: 100vh;
display: flex;
flex-direction: column;
background:
radial-gradient(1100px 520px at 10% -10%, rgba(59, 77, 149, 0.06), transparent 60%),
radial-gradient(900px 480px at 105% 110%, rgba(212, 242, 227, 0.28), transparent 55%),
var(--color-bg);
}
/* Тёмная тема: декоративные радиальные пятна фона построены на индиго/мятном
светлой темы (литеральные rgba, не var()) — патч в тон новой палитры
(янтарь/мята той же светлоты, что и в комнате конференции). */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) .lobby-shell {
background:
radial-gradient(1100px 520px at 10% -10%, rgba(214, 168, 61, 0.05), transparent 60%),
radial-gradient(900px 480px at 105% 110%, rgba(127, 221, 168, 0.14), transparent 55%),
var(--color-bg);
}
}
:root[data-theme='dark'] .lobby-shell {
background:
radial-gradient(1100px 520px at 10% -10%, rgba(214, 168, 61, 0.05), transparent 60%),
radial-gradient(900px 480px at 105% 110%, rgba(127, 221, 168, 0.14), transparent 55%),
var(--color-bg);
}
.hub-main {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-6) var(--space-8) var(--space-12);
}
.hub-wrap { display: flex; flex-direction: column; align-items: center; gap: var(--space-8); width: 100%; }
.hub-intro { text-align: center; max-width: 560px; }
.hub-intro .eyebrow { font: var(--text-caption); color: var(--color-ink-400); margin: 0 0 var(--space-2); }
.hub-intro h1 { font: var(--text-display-lg); font-family: var(--font-display); color: var(--color-ink-700); margin: 0; }
.hub {
position: relative;
width: min(1420px, 100%);
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 340px);
gap: var(--space-8);
}
/* Карточка-действие. Медальон с иконкой «сидит» на верхней кромке. */
.action {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: var(--space-3);
padding: var(--space-10) var(--space-12) var(--space-8);
margin-top: 34px;
/* min-width:0 — иначе grid-элемент с 1fr-треком не сжимается уже своего
min-content (длинные заглавные слова вроде «ПОДКЛЮЧИТЬСЯ» на Unbounded);
без этого узкий вьюпорт даёт classic grid blowout — трек и вся сетка
раздуваются шире контейнера, появляется горизонтальный скролл. */
min-width: 0;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-3xl);
text-decoration: none;
color: var(--color-ink-900);
box-shadow: var(--shadow-sm);
cursor: pointer;
font-family: inherit;
transition:
transform var(--transition-base),
box-shadow var(--transition-base),
border-color var(--transition-base);
}
.action:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); border-color: var(--color-border-strong); }
.action:active { transform: translateY(-1px); }
.action:disabled { cursor: not-allowed; opacity: 0.7; transform: none; }
.action-icon {
position: absolute;
top: -34px;
left: 50%;
transform: translateX(-50%);
width: 68px; height: 68px;
border-radius: 50%;
background: var(--color-surface);
border: var(--border-thick) solid var(--color-ink-700);
color: var(--color-ink-700);
display: flex; align-items: center; justify-content: center;
box-shadow: var(--shadow-md);
}
.action-icon .icon { width: 30px; height: 30px; }
.action-title {
margin: 0;
font: 600 40px/1.2 var(--font-display);
letter-spacing: 0.01em;
color: var(--color-ink-700);
text-transform: uppercase;
overflow-wrap: break-word;
}
.action-object { margin: 0; font: 600 21px/1.35 var(--font-body); color: var(--color-ink-900); overflow-wrap: break-word; }
.action-note { margin: 0; font: 400 17px/1.5 var(--font-body); color: var(--color-ink-500); overflow-wrap: break-word; }
/* Главное действие — CTA дизайн-системы (пастельная мята) */
.action--create { background: var(--color-accent); border-color: transparent; }
.action--create:hover:not(:disabled) { background: var(--color-accent-hover); border-color: transparent; }
.action--create:active:not(:disabled) { background: var(--color-accent-active); }
.action--create .action-title { font: 700 54px/1.1 var(--font-display); }
.action--create .action-title, .action--create .action-object { color: var(--color-accent-text); }
.action--create .action-note { color: var(--color-ink-700); }
.action--create .action-icon { background: var(--color-accent); border-color: var(--color-accent-text); color: var(--color-accent-text); }
/* ---------- Медальон VidConf в центре мозаики ---------- */
.hub-logo {
position: absolute;
top: 373px; left: 50%;
transform: translate(-50%, -50%);
width: 156px; height: 156px;
border-radius: 50%;
background: var(--color-surface);
border: var(--border-thick) solid var(--color-ink-700);
box-shadow: var(--shadow-lg);
display: flex; flex-direction: column;
align-items: center; justify-content: center;
gap: 6px;
z-index: 20;
pointer-events: none;
}
.hub-logo .logo-mark { width: 56px; height: 56px; color: var(--color-ink-700); }
.hub-logo .logo-word { font: 600 16px/1 var(--font-display); color: var(--color-ink-700); }
/* ---------- Появление ---------- */
@media (prefers-reduced-motion: no-preference) {
.action, .hub-logo { animation: vc-hub-rise 480ms cubic-bezier(0.22, 0.9, 0.3, 1) backwards; }
.action--create { animation-delay: 60ms; }
.action--calendar { animation-delay: 140ms; }
.action--join { animation-delay: 220ms; }
.action--mine { animation-delay: 300ms; }
.hub-logo { animation-delay: 420ms; animation-name: vc-hub-rise-logo; }
@keyframes vc-hub-rise {
from { opacity: 0; transform: translateY(14px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes vc-hub-rise-logo {
from { opacity: 0; transform: translate(-50%, calc(-50% + 14px)); }
to { opacity: 1; transform: translate(-50%, -50%); }
}
}
/* ---------- Узкие экраны ---------- */
/* ВАЖНО: этот блок должен идти строго ПОСЛЕДНИМ в файле. При равной
специфичности селекторов (`.action-title` и т.п. — 0,1,0 что здесь, что в
безусловных правилах выше) порядок в исходнике решает, какое правило
применится, а не сам факт совпадения media query — если этот блок стоит
раньше своих безусловных дублей, он будет молча перебит ими на любой
ширине. Так уже ловили баг (правки гейта, гейт 3): грид расползался на
375px, потому что этот блок раньше стоял сразу после базового `.hub {}`,
до `.action-title`/`.action--create .action-title` и т.п. */
@media (max-width: 960px) {
/* Мозаика 2×2 → одна колонка; фиксированная высота 340px больше не нужна —
тайлы подстраиваются под контент. Медальон-логотип в центре сетки на
узких экранах убираем совсем: он был позиционирован под чёткую 2×2
геометрию (см. .hub-logo выше) и на одну колонку не переносится. */
.hub-wrap { gap: var(--space-6); }
.hub { grid-template-columns: 1fr; grid-template-rows: repeat(4, auto); gap: var(--space-5); }
.hub-logo { display: none; }
.hub-main { padding: var(--space-5) var(--space-4) var(--space-8); }
.action { padding: var(--space-8) var(--space-5) var(--space-6); margin-top: 28px; }
.action-icon { width: 56px; height: 56px; top: -28px; }
.action-icon .icon { width: 26px; height: 26px; }
.action-title { font-size: 28px; }
.action--create .action-title { font-size: 34px; }
.action-object { font-size: 17px; }
.action-note { font-size: 15px; }
}

View File

@@ -0,0 +1,85 @@
/*
* VidConf — стили страницы «Мои конференции».
* 1:1 из design/mockups/my-conferences.html (.conf-grid/.conf-card/.pills-row
* /.code-pill). page-shell/page-main/page-head — общие из lobby.css,
* btn-primary/btn-secondary/modal-overlay — из shell.css.
*/
/* flex-wrap — иначе три чипа фильтра на узких экранах не помещаются в одну
строку и раздувают страницу горизонтально (ряд был жёстко nowrap). */
.filters { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: var(--space-6); }
.chip { display: inline-flex; align-items: center; gap: 6px; padding: 9px 16px; border-radius: var(--radius-full); border: 1.5px solid var(--color-border); background: var(--color-surface); font: var(--text-body); font-weight: 600; color: var(--color-ink-500); cursor: pointer; }
.chip.is-active { border-color: var(--color-ink-700); color: var(--color-ink-700); background: var(--color-bg-alt); }
.conf-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-5); }
/* min-width:0 — грид-элементы иначе не сжимаются уже своего min-content
(grid blowout), см. тот же приём в calendar.css/.layout-grid. */
.conf-grid > * { min-width: 0; }
.conf-card { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-2xl); padding: var(--space-6); display: flex; flex-direction: column; gap: var(--space-4); transition: box-shadow var(--transition-base), border-color var(--transition-base); }
.conf-card:hover { box-shadow: var(--shadow-md); border-color: var(--color-border-strong); }
.conf-card.is-ended { opacity: .85; }
.conf-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-3); }
.conf-title-wrap h3 { margin: 0 0 4px; font: var(--text-h3); color: var(--color-ink-900); display: flex; align-items: center; gap: 6px; }
.conf-title-wrap p { margin: 0; font: var(--text-body); color: var(--color-ink-500); }
.conf-badge { display: inline-flex; align-items: center; gap: 5px; padding: 4px 12px; border-radius: var(--radius-full); font: var(--text-caption); white-space: nowrap; }
.conf-badge.pinned { background: var(--color-bg-alt); color: var(--color-ink-700); border: 1px solid var(--color-border-strong); }
.conf-badge.scheduled { background: var(--color-warning-bg); color: var(--color-warning); }
.conf-badge.live { background: var(--color-success-bg); color: var(--color-success); }
.conf-badge.ended { background: var(--color-bg-alt); color: var(--color-ink-500); }
.conf-meta-row { display: flex; align-items: center; gap: var(--space-4); font: var(--text-body); color: var(--color-ink-500); }
.conf-meta-row .meta-item { display: flex; align-items: center; gap: 6px; }
.pills-row { display: flex; gap: var(--space-3); }
.pill-group { flex: 1; min-width: 0; }
.pill-label { display: block; font: var(--text-caption); color: var(--color-ink-400); margin-bottom: 4px; }
.code-pill { display: flex; align-items: center; gap: 6px; background: var(--color-bg-alt); border: 1px solid var(--color-border); border-radius: var(--radius-full); padding: 6px 6px 6px 14px; }
.pill-value { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: var(--text-mono-sm); color: var(--color-ink-700); }
.copy-btn { position: relative; width: 28px; height: 28px; flex-shrink: 0; border-radius: 50%; border: none; background: var(--color-surface); color: var(--color-ink-500); display: flex; align-items: center; justify-content: center; cursor: pointer; transition: color var(--transition-fast), box-shadow var(--transition-fast); }
.copy-btn:hover { color: var(--color-ink-700); box-shadow: var(--shadow-sm); }
.copy-btn .icon-check { color: var(--color-success); }
.copy-toast {
position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%);
background: var(--color-ink-900); color: #fff; font: var(--text-caption); text-transform: none; letter-spacing: normal;
padding: 4px 10px; border-radius: var(--radius-sm); white-space: nowrap;
}
.conf-card-footer { display: flex; gap: var(--space-3); margin-top: auto; }
.conf-card-footer .btn { flex: 1; justify-content: center; }
.icon-btn { width: 40px; height: 40px; flex-shrink: 0; border-radius: 50%; border: 1px solid var(--color-border); background: var(--color-surface); color: var(--color-ink-500); display: flex; align-items: center; justify-content: center; cursor: pointer; }
.icon-btn:hover { border-color: var(--color-border-strong); color: var(--color-ink-900); }
/* ---------- Модалка редактирования (переиспользует ConferenceFormCard) ---------- */
/* .create-card сама по себе — визуальная панель (фон/обводка/радиус/паддинг из
calendar.css); здесь только гасим её sticky-позиционирование календаря и
ограничиваем высоту внутри модалки. */
.modal-form-wrap { width: 100%; max-width: 480px; }
.modal-form-wrap .create-card { position: static; max-height: calc(100vh - var(--space-12)); overflow-y: auto; }
/* Тёмная тема: тултип «Скопировано» — заливка была var(--color-ink-900)
(тёмный индиго в светлой теме → стал бы светло-серым в тёмной, ломая
тёмный тултип поверх тёмной страницы); заменена на буквальный #333333
(= --color-room-surface-raised), текст остаётся var(--color-ink-900)
(теперь светлый) — тултип снова тёмный чип со светлым текстом. */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) .copy-toast { background: #333333; color: var(--color-ink-900); }
}
:root[data-theme='dark'] .copy-toast { background: #333333; color: var(--color-ink-900); }
/* ---------- Узкие экраны ---------- */
/* ВАЖНО: блок должен идти строго ПОСЛЕДНИМ в файле — при равной
специфичности с `.conf-grid` выше порядок в исходнике решает, какое
правило применится (тот же баг уже ловили в lobby.css/.action, гейт 3).
Макет my-conferences.html — desktop-only, адаптива в нём нет; 2 колонки
карточек на узких экранах схлопываются в 1. */
@media (max-width: 860px) {
.conf-grid { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
.conf-card { padding: var(--space-5); }
.pills-row { flex-direction: column; }
}

View File

@@ -0,0 +1,25 @@
/*
* VidConf — страница профиля. Новый экран без готового
* макета в design/mockups/ — собран из утверждённых паттернов оболочки:
* карточка `--radius-2xl`/`--shadow-sm` как у `.conf-card`/`.settings-card`,
* поля форм — `.field` (shell.css), аватар — общий `.avatar` (§1.5).
* page-shell/page-main/page-head — общие из lobby.css.
*/
.profile-card {
max-width: 480px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-2xl);
box-shadow: var(--shadow-sm);
padding: var(--space-8);
display: flex;
flex-direction: column;
gap: var(--space-6);
}
/* Второй блок карточки («Смена пароля») — отступ между карточками профиля. */
.profile-card + .profile-card { margin-top: var(--space-6); }
.profile-card h2 { font: var(--text-h3); color: var(--color-ink-900); margin: 0; }
.profile-avatar-block { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-3); }
.profile-avatar-actions { display: flex; align-items: center; gap: var(--space-3); }

View File

@@ -0,0 +1,482 @@
/*
* VidConf — стили экрана комнаты конференции (тёмная тема, [data-theme="room"]).
* Приближение к design/mockups/room.html.
*
* ВАЖНО (см. также комментарий в RoomPage.tsx): видео-сетка участников
* рендерится компонентами @livekit/components-react (GridLayout/FocusLayout/
* CarouselLayout/ParticipantTile) — их внутренняя разметка не совпадает 1:1 с
* `.tile`/`.tile-preview` из макета. Здесь мы (1) переопределяем CSS-переменные
* темы LiveKit (`--lk-*`) значениями токенов VidConf, чтобы цвета/радиусы
* совпадали с дизайн-системой, и (2) стилизуем только внешние контейнеры,
* которые нам доступны через className. Точная пиксельная подгонка внутренней
* разметку ParticipantTile (позиция имени/иконки микрофона 1:1 как в
* макете) вынесена в отдельную доводку.
*/
[data-theme='room'] { height: 100%; }
.room-shell { display: flex; flex-direction: column; height: 100vh; background: var(--color-room-bg); }
.room-topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-3) var(--space-6);
background: var(--color-room-surface);
border-bottom: 1px solid var(--color-room-tile-border);
flex-shrink: 0;
}
.room-title-block h1 { font: var(--text-h3); margin: 0 0 2px; color: var(--color-room-text-primary); }
.room-title-block p { font: var(--text-body); margin: 0; color: var(--color-room-text-secondary); display: flex; align-items: center; gap: 8px; }
.rec-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--color-room-mic-on); box-shadow: 0 0 0 3px rgba(70, 217, 122, 0.25); }
/* Инвайт-чип (номер конференции + копирование ссылки) — см. RoomTopbar.tsx. */
.room-invite-chip {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: var(--radius-full);
border: 1px solid var(--color-room-tile-border);
background: var(--color-room-tile);
color: var(--color-room-text-primary);
font: var(--text-body);
font-weight: 600;
}
.room-invite-chip:hover { background: var(--color-room-tile-hover); }
.room-invite-chip span { font: var(--text-mono-sm); color: var(--color-room-text-secondary); }
.room-main { display: flex; flex: 1; min-height: 0; }
.stage { flex: 1; display: flex; flex-direction: column; padding: var(--space-5) var(--space-6); gap: var(--space-4); min-width: 0; }
/*
* Демонстрация экрана: `object-fit: cover` (дефолт VideoTrack для
* камеры) обрезает края — для текста/таблиц на экране это критично, поэтому
* для screenshare-видео явно переключаем на `contain` (letterbox вместо
* обрезки); фон — чёрный, как у самого видео-элемента библиотеки.
* `data-lk-source` — штатный атрибут @livekit/components-react на <video>.
*/
video[data-lk-source='screen_share'] { object-fit: contain; background: #000; }
/*
* `.stage-tiles` — общий className и для FocusLayoutContainer (несколько
* участников), и для GridLayout (один/два участника, см. RoomStage.tsx) —
* тут только то, что нужно ОБОИМ (заполнить оставшуюся высоту `.stage`).
* Раскладку колонок (`grid-template-columns`) переопределяем НИЖЕ отдельно,
* только для варианта с FocusLayoutContainer (`.lk-focus-layout`) — раньше
* это правило висело прямо на `.stage-tiles` и наружу задевало GridLayout:
* при одном участнике его плитка сжималась в узкую 22%-колонку слева, а
* остальная сцена оставалась пустой (баг, поймали на скриншоте).
*/
.stage-tiles {
flex: 1;
min-height: 0;
}
/*
* FocusLayoutContainer рендерит `.lk-focus-layout` (grid, первая колонка —
* узкая, под превью; вторая — широкая, под активного спикера) — сужаем
* первую колонку под наш вкус (дефолтный `1fr` слишком гуляет на широких
* мониторах). Селектор нарочно составной (`.stage-tiles.lk-focus-layout`),
* чтобы НЕ задеть `.lk-grid-layout` (тот же className `.stage-tiles` также
* висит на GridLayout при 12 участниках, см. выше). `!important` не нужен:
* `room.css` импортируется в RoomPage.tsx ПОСЛЕ `@livekit/components-styles`,
* поэтому при равной специфичности наши значения и так побеждают по порядку
* в исходном CSS.
*/
.stage-tiles.lk-focus-layout {
grid-template-columns: minmax(200px, 22%) 1fr;
}
/*
* Колонка миниатюр остальных участников — CarouselLayout без явного
* `orientation` (см. комментарий в RoomStage.tsx): при узкой и высокой форме
* контейнера (десктоп, боковая колонка) сам определяет `vertical` и берёт
* встроенные стили `.lk-carousel[data-lk-orientation="vertical"]` из
* @livekit/components-styles (flex-direction: column, скролл по вертикали,
* тайлы делятся по высоте на --lk-max-visible-tiles). Здесь только
* тонкая настройка скроллбара под тёмную тему комнаты — сами
* flex-direction/overflow не трогаем, чтобы не сломать переключение
* ориентации на мобильной ширине (см. media-запрос ниже).
*/
.lk-carousel::-webkit-scrollbar { width: 6px; height: 6px; }
.lk-carousel::-webkit-scrollbar-thumb { background: var(--color-room-tile-border); border-radius: 3px; }
/*
* Мобильная ширина: @livekit/components-styles сам переключает
* `.lk-focus-layout` на `grid-template-rows: 5fr 1fr` (спикер сверху,
* колонка миниатюр становится узкой горизонтальной полосой снизу,
* `.lk-carousel{order:1}`) — колонка шириной 22% там физически не
* поместится, поэтому здесь возвращаем `grid-template-columns` обратно в
* `1fr` (наше правило выше иначе продолжало бы действовать поверх их
* media-запроса — у обоих селекторов равная специфичность, и то, что
* стоит позже в исходнике, побеждает, а мы просто идём в файле позже).
*/
@media (max-width: 600px) {
.stage-tiles.lk-focus-layout { grid-template-columns: 1fr; }
.stage { padding: var(--space-3) var(--space-4); gap: var(--space-3); }
}
/*
* `.room-single-tile` — сцена мини-плеера (Document PiP, `RoomStage` с
* `variant="pip"`): единственная плитка активного окна на весь контейнер, без
* карусели/грида. Портал рендерит
* `RoomStage` НАПРЯМУЮ в `pipWindow.document.body` — своей `.room-shell`/
* `.room-main` цепочки высот там нет, поэтому `.lk-participant-tile`
* растягиваем явным `flex: 1` (а не полагаемся на %-высоту от родителя, как
* `.stage-tiles` в основном окне) плюс подстраховочный `min-height`, чтобы
* плитка не схлопывалась, если видео ещё не выдало реальные размеры.
*/
.room-single-tile { flex: 1; min-height: 0; display: flex; }
.room-single-tile .lk-participant-tile { flex: 1; min-height: 220px; width: 100%; }
/* Нижний тулбар: свои кнопки на хуках LiveKit (TrackToggle/DisconnectButton) */
.room-toolbar {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
background: var(--color-room-surface);
border-top: 1px solid var(--color-room-tile-border);
padding: var(--space-3) var(--space-6);
flex-shrink: 0;
}
.tb-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
background: transparent;
border: none;
padding: 8px 18px;
border-radius: var(--radius-md);
color: var(--color-room-text-primary);
min-width: 76px;
cursor: pointer;
}
.tb-btn .icon-shell {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 1.5px solid var(--color-room-tile-border);
background: var(--color-room-tile);
color: var(--color-room-text-primary);
}
.tb-btn span.label { font: var(--text-caption); text-transform: none; letter-spacing: normal; color: var(--color-room-text-secondary); font-weight: 500; }
.tb-btn:hover .icon-shell { background: var(--color-room-tile-hover); }
.tb-btn.is-off .icon-shell { background: var(--color-room-mic-off); border-color: var(--color-room-mic-off); color: #3a0f16; }
.tb-btn.is-off span.label { color: var(--color-room-mic-off); font-weight: 700; }
/* Кнопка «Демонстрация» — активное состояние подсвечено зелёным,
как и другие «позитивные» индикаторы комнаты (ср. .rec-dot/--color-room-mic-on). */
.tb-btn.is-sharing .icon-shell { background: var(--color-room-mic-on); border-color: var(--color-room-mic-on); color: #10331f; }
.tb-btn.is-sharing span.label { color: var(--color-room-mic-on); font-weight: 700; }
.tb-btn:disabled { opacity: 0.6; cursor: default; }
.tb-btn.danger .icon-shell { border-color: var(--color-room-danger); color: var(--color-room-danger); background: transparent; }
.tb-btn.danger:hover .icon-shell { background: var(--color-room-danger-bg); color: #fff; border-color: var(--color-room-danger-bg); }
.tb-btn.danger span.label { color: var(--color-room-danger); font-weight: 700; }
/* Кнопка «Чат» — подсветка при открытой панели + бейдж непрочитанных. */
.tb-btn .icon-shell { position: relative; }
.tb-btn.is-panel-open .icon-shell { background: var(--color-room-surface-raised); border-color: var(--color-room-speaker-ring); color: var(--color-room-speaker-ring); }
.badge-count {
position: absolute;
top: -2px;
right: -2px;
background: var(--color-room-mic-on);
color: #0e3320;
font-size: 10px;
font-weight: 700;
width: 16px;
height: 16px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
/* ---------- Панель чата (см. design/mockups/room.html, .chat-panel) ---------- */
.chat-panel {
width: 320px;
flex-shrink: 0;
background: var(--color-room-surface-raised);
border-left: 1px solid var(--color-room-tile-border);
display: flex;
flex-direction: column;
box-shadow: var(--shadow-room-panel);
}
.chat-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-4) var(--space-5);
border-bottom: 1px solid var(--color-room-tile-border);
flex-shrink: 0;
}
.chat-head h2 { font: var(--text-h3); margin: 0; color: var(--color-room-text-primary); }
.chat-head button { background: none; border: none; color: var(--color-room-text-secondary); cursor: pointer; display: flex; }
.chat-head button:hover { color: var(--color-room-text-primary); }
.chat-status-banner {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
padding: var(--space-3) var(--space-5);
background: var(--color-room-danger-bg);
color: #fff;
font: var(--text-body);
flex-shrink: 0;
}
.chat-empty { font: var(--text-body); color: var(--color-room-text-tertiary); margin: auto; text-align: center; }
.chat-messages {
flex: 1;
overflow-y: auto;
padding: var(--space-5);
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.chat-messages::-webkit-scrollbar { width: 6px; }
.chat-messages::-webkit-scrollbar-thumb { background: var(--color-room-tile-border); border-radius: 3px; }
.msg .msg-meta { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }
.msg .msg-author { font: var(--text-body); font-weight: 700; color: var(--color-room-mic-on); }
.msg .msg-guest-badge {
font: var(--text-caption);
color: var(--color-room-text-tertiary);
border: 1px solid var(--color-room-tile-border);
border-radius: var(--radius-sm);
padding: 0 6px;
text-transform: none;
letter-spacing: normal;
}
.msg .msg-time { font: 12px var(--font-body); color: var(--color-room-text-tertiary); }
.msg .msg-bubble {
background: var(--color-room-tile);
border-radius: var(--radius-md);
padding: 10px 14px;
font: var(--text-body);
color: var(--color-room-text-primary);
white-space: pre-wrap;
word-break: break-word;
}
.chat-input-row {
padding: var(--space-4);
border-top: 1px solid var(--color-room-tile-border);
display: flex;
align-items: flex-end;
gap: 8px;
flex-shrink: 0;
}
.chat-input-row textarea {
flex: 1;
resize: none;
background: var(--color-room-tile);
border: 1px solid var(--color-room-tile-border);
border-radius: var(--radius-lg);
padding: 10px 16px;
color: var(--color-room-text-primary);
font: var(--text-body-lg);
max-height: 96px;
}
.chat-input-row textarea::placeholder { color: var(--color-room-text-tertiary); }
.chat-input-row textarea:disabled { opacity: 0.6; }
.chat-input-row button {
width: 42px;
height: 42px;
border-radius: 50%;
background: var(--color-room-mic-on);
border: none;
color: #10331f;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
cursor: pointer;
}
.chat-input-row button:disabled { opacity: 0.5; cursor: default; }
@media (max-width: 900px) {
.chat-panel {
position: fixed;
inset: 0;
top: 64px;
bottom: 96px;
width: auto;
z-index: 20;
}
}
/*
* ---------- Диалог «Настройки устройств» ----------
* Нет отдельного макета для диалога в design/mockups/room.html — собран из
* уже утверждённых токенов тёмной темы комнаты (`--color-room-*`), тот же
* структурный паттерн (overlay/panel/head/close/field), что и модалки
* оболочки (`shell.css` .modal-*), но перекрашенный под комнату: `.modal-*`
* оболочки используют `--color-ink-*`, которые не переопределяются внутри
* `[data-theme="room"]`, — переиспользовать их напрямую нельзя, получилась
* бы светлая панель на тёмном фоне комнаты.
*/
.room-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.55);
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-6);
z-index: 200;
}
.room-modal-panel {
background: var(--color-room-surface-raised);
border: 1px solid var(--color-room-tile-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-room-panel);
padding: var(--space-6);
width: 100%;
max-width: 420px;
max-height: calc(100vh - var(--space-12));
overflow-y: auto;
}
.room-modal-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-4); margin-bottom: var(--space-5); }
.room-modal-head h2 { font: var(--text-h3); color: var(--color-room-text-primary); margin: 0; }
.room-modal-close {
width: 32px;
height: 32px;
border-radius: 50%;
border: 1px solid var(--color-room-tile-border);
background: transparent;
color: var(--color-room-text-secondary);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
cursor: pointer;
}
.room-modal-close:hover { color: var(--color-room-text-primary); border-color: var(--color-room-text-secondary); }
.room-field { margin-bottom: var(--space-5); }
.room-field:last-child { margin-bottom: 0; }
.room-field label { display: block; font: var(--text-body); font-weight: 600; color: var(--color-room-text-primary); margin-bottom: var(--space-2); }
.room-field select {
width: 100%;
font: var(--text-body-lg);
color: var(--color-room-text-primary);
padding: 10px 14px;
border-radius: var(--radius-md);
border: 1px solid var(--color-room-tile-border);
background: var(--color-room-tile);
font-family: var(--font-body);
}
.room-field select:focus { outline: none; border-color: var(--color-room-focus-ring); }
/*
* ---------- Чип «Вы демонстрируете экран» ----------
* Рендерится внутри `RoomParticipantTile` (см. её `TileBody`) — т.е. внутри
* `.lk-participant-tile`, который у @livekit/components-react уже
* `position: relative` (см. её базовые стили), поэтому оверлей позиционируем
* абсолютно прямо относительно плитки, без правок родительских контейнеров.
*/
.stage-sharing-chip {
position: absolute;
top: 12px;
left: 12px;
z-index: 5;
display: flex;
align-items: center;
gap: 10px;
padding: 8px 14px;
border-radius: var(--radius-full);
background: rgba(30, 30, 30, 0.85);
border: 1px solid var(--color-room-mic-on);
color: var(--color-room-text-primary);
font: var(--text-body);
box-shadow: var(--shadow-md);
}
.stage-sharing-chip svg { width: 18px; height: 18px; color: var(--color-room-mic-on); flex-shrink: 0; }
.stage-sharing-chip button {
border: none;
border-radius: var(--radius-full);
background: var(--color-room-mic-on);
color: #10331f;
font: var(--text-body);
font-weight: 700;
padding: 4px 12px;
cursor: pointer;
flex-shrink: 0;
}
.stage-sharing-chip button:hover { filter: brightness(0.95); }
/* ---------- Заглушка «конференция в мини-окне» (Document PiP) ---------- */
.room-pip-placeholder {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-4);
color: var(--color-room-text-secondary);
font: var(--text-body-lg);
text-align: center;
}
.room-pip-placeholder svg { width: 48px; height: 48px; color: var(--color-room-text-tertiary); }
/* ---------- Аватар вместо иконки-заглушки при выключенной камере ---------- */
.room-tile-avatar {
width: 40%;
height: 40%;
min-width: 32px;
min-height: 32px;
max-width: 96px;
max-height: 96px;
font-size: clamp(12px, 3vw, 28px);
font-weight: 700;
}
.room-loader, .room-error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-3);
height: 100vh;
color: var(--color-room-text-secondary);
font: var(--text-body-lg);
background: var(--color-room-bg);
}
.room-error .btn-primary { margin-top: var(--space-2); }
/* ---------- Переопределение темы @livekit/components-react токенами VidConf ---------- */
[data-lk-theme='default'] {
--lk-bg: var(--color-room-bg);
--lk-bg2: var(--color-room-surface);
--lk-bg3: var(--color-room-tile);
--lk-bg4: var(--color-room-tile-hover);
--lk-bg5: var(--color-room-tile-hover);
--lk-fg: var(--color-room-text-primary);
--lk-fg2: var(--color-room-text-primary);
--lk-fg3: var(--color-room-text-secondary);
--lk-fg4: var(--color-room-text-secondary);
--lk-border-color: var(--color-room-tile-border);
--lk-accent-fg: #10331f;
--lk-accent-bg: var(--color-room-speaker-ring);
--lk-danger-fg: #fff;
--lk-danger: var(--color-room-danger-bg);
--lk-success: var(--color-room-mic-on);
--lk-control-fg: var(--color-room-text-primary);
--lk-control-bg: var(--color-room-tile);
--lk-control-hover-bg: var(--color-room-tile-hover);
--lk-font-family: var(--font-body);
--lk-border-radius: var(--radius-lg);
--lk-grid-gap: var(--space-4);
--lk-control-bar-height: 0px;
}

View File

@@ -0,0 +1,126 @@
/*
* VidConf — кастомный селект (DESIGN_SYSTEM.md §4.16).
* Триггер повторяет геометрию инпута (§4.4), дроплист — паттерн `.menu`
* (design/mockups/lobby.html). Все правила на var()-токенах — тёмная тема
* переключается сама через каскад tokens.css, отдельных патчей не требуется
* (см. §4.16 «Обе темы»).
*/
.vc-select {
position: relative;
}
.vc-select-trigger {
width: 100%;
height: 44px;
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
font: var(--text-body-lg);
color: var(--color-ink-900);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
cursor: pointer;
font-family: var(--font-body);
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
.vc-select-trigger:hover:not(:disabled) {
border-color: var(--color-border-strong);
}
.vc-select-trigger:focus-visible {
outline: none;
border-color: var(--color-focus-ring);
box-shadow: 0 0 0 2px var(--color-focus-ring);
}
.vc-select-trigger[aria-expanded='true'] {
border-color: var(--color-focus-ring);
box-shadow: 0 0 0 2px var(--color-focus-ring);
}
.vc-select-trigger:disabled {
background: var(--color-bg-alt);
color: var(--color-ink-300);
cursor: not-allowed;
}
.vc-select-trigger.has-error {
border-width: 2px;
border-color: var(--color-danger);
}
.vc-select-value,
.vc-select-placeholder {
flex: 1;
min-width: 0;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.vc-select-placeholder {
color: var(--color-ink-300);
}
.vc-select-error-icon {
color: var(--color-danger);
flex-shrink: 0;
}
.vc-select-chevron {
color: var(--color-ink-500);
flex-shrink: 0;
transition: transform var(--transition-fast);
}
.vc-select-chevron.is-open {
transform: rotate(180deg);
}
.vc-select-trigger:disabled .vc-select-chevron {
color: var(--color-ink-300);
}
.vc-select-panel {
position: absolute;
top: calc(100% + 6px);
left: 0;
width: 100%;
max-height: 280px;
overflow-y: auto;
margin: 0;
padding: var(--space-2);
list-style: none;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
z-index: 30;
}
.vc-select-option {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 10px 14px;
border-radius: var(--radius-sm);
font: var(--text-body-lg);
font-weight: 400;
color: var(--color-ink-900);
cursor: pointer;
}
.vc-select-option.is-active {
background: var(--color-bg-alt);
color: var(--color-ink-700);
}
.vc-select-option.is-selected {
font-weight: 600;
}
.vc-select-option.is-selected svg {
color: var(--color-ink-700);
flex-shrink: 0;
}
.vc-select-option.is-disabled {
color: var(--color-ink-300);
cursor: not-allowed;
}
.vc-select-option.is-disabled.is-active {
background: transparent;
color: var(--color-ink-300);
}

View File

@@ -0,0 +1,455 @@
/*
* VidConf — общие стили светлой оболочки (auth/lobby/calendar/admin).
* 1:1 воспроизводит классы из design/mockups/{auth,lobby}.html, чтобы
* JSX-разметка страниц могла использовать те же имена классов, что и
* утверждённые макеты. Токены — из tokens.css.
*/
/* ---------- Топбар (лобби/календарь/админка) ---------- */
.topbar {
position: sticky;
top: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-4) var(--space-8);
background: var(--color-surface);
border-bottom: 1px solid var(--color-border);
}
.brand-mark {
display: inline-flex;
align-items: center;
gap: 8px;
font: var(--text-h3);
color: var(--color-ink-700);
text-decoration: none;
}
.brand-mark .logo-mark {
width: 22px;
height: 22px;
flex-shrink: 0;
}
.topbar-right {
display: flex;
align-items: center;
gap: var(--space-4);
}
/* ---------- Переключатель темы «солнце/луна» (см. design/mockups/dark/*.html) ---------- */
.theme-toggle {
display: inline-flex;
gap: 2px;
background: var(--color-bg-alt);
padding: 4px;
border-radius: var(--radius-full);
border: 1px solid var(--color-border);
}
.theme-toggle-btn {
width: 32px;
height: 32px;
border-radius: 50%;
border: none;
background: transparent;
color: var(--color-ink-500);
display: flex;
align-items: center;
justify-content: center;
}
.theme-toggle-btn .icon {
width: 16px;
height: 16px;
}
.theme-toggle-btn:hover {
color: var(--color-ink-900);
}
.theme-toggle-btn.is-active {
background: var(--color-surface);
color: var(--color-ink-700);
box-shadow: var(--shadow-sm);
}
/* ---------- Пилюля пользователя (декоративная, см. lobby.html) ---------- */
.user-pill {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 6px 16px 6px 6px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-full);
font: var(--text-body);
font-weight: 600;
color: var(--color-ink-900);
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
.user-pill:hover {
border-color: var(--color-border-strong);
box-shadow: var(--shadow-sm);
}
.avatar {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--color-ink-700);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font: var(--text-caption);
text-transform: none;
letter-spacing: 0;
flex-shrink: 0;
overflow: hidden;
}
/* Фото аватара (Avatar.tsx) — покрывает заглушку с инициалами целиком. */
.avatar img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
}
.user-pill .avatar {
width: 30px;
height: 30px;
}
/* ---------- Бургер и скрывающаяся панель навигации (см. lobby.html) ---------- */
.menu-wrap {
position: relative;
}
.burger {
width: 44px;
height: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
color: var(--color-ink-700);
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
.burger:hover {
border-color: var(--color-border-strong);
box-shadow: var(--shadow-sm);
}
.burger[aria-expanded='true'] {
border-color: var(--color-ink-700);
}
.menu {
position: absolute;
top: calc(100% + 10px);
right: 0;
min-width: 236px;
padding: var(--space-2);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
display: none;
z-index: 30;
}
.menu.is-open {
display: block;
}
.menu a,
.menu button {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 10px 14px;
border: none;
background: transparent;
border-radius: var(--radius-sm);
font: var(--text-body-lg);
font-weight: 600;
color: var(--color-ink-900);
text-decoration: none;
text-align: left;
cursor: pointer;
}
.menu a:hover,
.menu button:hover {
background: var(--color-bg-alt);
color: var(--color-ink-700);
}
.menu a.is-active {
background: var(--color-bg-alt);
color: var(--color-ink-700);
}
.menu a .icon,
.menu button .icon {
color: var(--color-ink-400);
}
.menu-divider {
height: 1px;
background: var(--color-border);
margin: var(--space-2) 6px;
}
.menu a.danger,
.menu button.danger {
color: var(--color-danger);
}
.menu a.danger .icon,
.menu button.danger .icon {
color: var(--color-danger);
}
/* ---------- Топбар на узких экранах ---------- */
@media (max-width: 640px) {
.topbar {
padding: var(--space-3) var(--space-4);
}
.topbar-right {
gap: var(--space-2);
}
.user-pill {
padding: 6px;
gap: 0;
}
.user-pill-name {
display: none;
}
}
/* ---------- Кнопки (общие для оболочки) ---------- */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
border: none;
cursor: pointer;
font-family: var(--font-body);
white-space: nowrap;
transition: background var(--transition-fast), box-shadow var(--transition-fast);
}
.btn-primary {
background: var(--color-accent);
color: var(--color-accent-text);
font: var(--text-body);
font-weight: 700;
padding: 11px 22px;
border-radius: var(--radius-full);
}
.btn-primary:hover:not(:disabled) {
background: var(--color-accent-hover);
box-shadow: var(--shadow-md);
}
.btn-primary:active:not(:disabled) {
background: var(--color-accent-active);
}
.btn-primary:disabled {
background: rgba(166, 174, 203, 0.35);
color: var(--color-surface);
cursor: not-allowed;
}
.btn-secondary {
background: transparent;
color: var(--color-ink-700);
border: 1.5px solid var(--color-border-pill);
font: var(--text-body);
font-weight: 600;
padding: 9px 18px;
border-radius: var(--radius-full);
}
.btn-secondary:hover:not(:disabled) {
background: var(--color-bg-alt);
}
.btn-secondary:disabled {
border-color: var(--color-border);
color: var(--color-ink-300);
cursor: not-allowed;
}
/* ---------- Бейджи статуса ---------- */
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
border-radius: var(--radius-full);
font: var(--text-caption);
white-space: nowrap;
}
.badge-free { background: var(--color-success-bg); color: var(--color-success); }
.badge-busy { background: var(--color-danger-bg); color: var(--color-danger); }
.badge-booked { background: var(--color-warning-bg); color: var(--color-warning); }
.badge-locked { background: var(--color-bg-alt); color: var(--color-ink-700); }
/* ---------- Индикатор загрузки (bootstrap-сессия, join и т.п.) ---------- */
.app-loader {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
color: var(--color-ink-500);
font: var(--text-body-lg);
background: var(--color-bg);
}
.spin {
animation: vidconf-spin 0.9s linear infinite;
}
@keyframes vidconf-spin {
to { transform: rotate(360deg); }
}
/* ---------- Тосты ---------- */
.toast-stack {
position: fixed;
right: var(--space-6);
bottom: var(--space-6);
display: flex;
flex-direction: column;
gap: var(--space-2);
z-index: 100;
}
.toast {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 18px;
border-radius: var(--radius-md);
font: var(--text-body);
font-weight: 600;
box-shadow: var(--shadow-lg);
max-width: 360px;
}
.toast-error { background: var(--color-danger-bg); color: var(--color-danger); }
.toast-success { background: var(--color-success-bg); color: var(--color-success); }
.toast-info { background: var(--color-surface); color: var(--color-ink-900); border: 1px solid var(--color-border); }
/* ---------- Ховер-карточка конференции (ConferenceHoverCard) ----------
* Новый элемент без готового макета в design/mockups/ — собран из уже
* утверждённых паттернов (карточка-поповер `--shadow-lg`, бейдж закреплённости
* с `Repeat` из §4.6). Рендерится порталом в document.body с `position: fixed`
* (см. ConferenceHoverCard.tsx) — `pointer-events: none`, чтобы не перекрывать
* клики по элементу под ней.
*/
.conf-hover-card {
pointer-events: none;
z-index: 250;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
padding: var(--space-4);
font: var(--text-body);
color: var(--color-ink-900);
}
.conf-hover-card-hint { margin: 0; color: var(--color-ink-500); }
.conf-hover-card-title { margin: 0 0 var(--space-2); font: var(--text-h3); color: var(--color-ink-900); }
.conf-hover-card-row { display: flex; align-items: center; gap: 6px; margin: 0 0 6px; color: var(--color-ink-700); }
.conf-hover-card-row:last-of-type { margin-bottom: 0; }
.conf-hover-card-label { display: flex; align-items: center; gap: 5px; font: var(--text-caption); color: var(--color-ink-400); margin-right: 4px; }
.conf-hover-card-participants { margin-top: var(--space-3); padding-top: var(--space-3); border-top: 1px solid var(--color-border); }
.conf-hover-card-participants ul { list-style: none; margin: 6px 0 0; padding: 0; display: flex; flex-direction: column; gap: 6px; max-height: 140px; overflow-y: auto; }
.conf-hover-card-participants li { display: flex; align-items: center; gap: 8px; font: var(--text-body); color: var(--color-ink-900); }
.conf-hover-card-tag { font: var(--text-caption); color: var(--color-ink-500); }
/* ---------- Модальные диалоги (светлая оболочка) ---------- */
/* Общая обёртка модалок оболочки (диалог быстрого просмотра вхождения
конференции, редактирование в «Моих конференциях» и т.п.) — центрированная
модалка (--shadow-lg) поверх затемнённого фона. */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(46, 52, 84, 0.4);
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-6);
z-index: 200;
}
.modal-panel {
background: var(--color-surface);
border-radius: var(--radius-2xl);
box-shadow: var(--shadow-lg);
padding: var(--space-6);
width: 100%;
max-width: 420px;
max-height: calc(100vh - var(--space-12));
overflow-y: auto;
}
.modal-panel.is-wide { max-width: 480px; }
.modal-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-4); margin-bottom: var(--space-5); }
.modal-head h2 { font: var(--text-h2); color: var(--color-ink-900); margin: 0; }
.modal-close {
width: 32px;
height: 32px;
border-radius: 50%;
border: 1px solid var(--color-border);
background: var(--color-surface);
color: var(--color-ink-500);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.modal-close:hover { border-color: var(--color-border-strong); color: var(--color-ink-900); }
.modal-actions { display: flex; gap: var(--space-3); margin-top: var(--space-5); }
.modal-actions .btn { flex: 1; justify-content: center; }
/* ---------- Поля форм в модалках (общие для всех диалогов оболочки) ---------- */
.field { margin-bottom: var(--space-5); }
.field:last-child { margin-bottom: 0; }
.field label { display: flex; align-items: center; gap: 6px; font: var(--text-body); font-weight: 600; color: var(--color-ink-900); margin-bottom: var(--space-2); }
.field input, .field textarea, .field select {
width: 100%;
font: var(--text-body-lg);
color: var(--color-ink-900);
padding: 10px 14px;
border-radius: var(--radius-md);
border: 1px solid var(--color-border);
background: var(--color-surface);
font-family: var(--font-body);
}
.field input:focus, .field textarea:focus, .field select:focus {
outline: none;
border-color: var(--color-focus-ring);
box-shadow: 0 0 0 3px rgba(59, 77, 149, 0.12);
}
.field-hint { font: var(--text-body); color: var(--color-ink-500); margin: 4px 0 0; }
.error-banner {
display: flex;
gap: 10px;
align-items: flex-start;
background: var(--color-danger-bg);
border: 1px solid rgba(199, 54, 79, 0.3);
color: var(--color-danger);
border-radius: var(--radius-md);
padding: var(--space-4);
font: var(--text-body);
margin-bottom: var(--space-5);
}
.error-banner strong { display: block; margin-bottom: 2px; }
.error-banner svg { flex-shrink: 0; margin-top: 1px; }
/* =====================================================================
* Тёмная тема: точечные патчи (design/mockups/dark/README.md, «Точечные
* правки поверх скопированного CSS»). Все переменные --color-* / --shadow-*
* уже перекрашиваются сами через каскад tokens.css — здесь только правила,
* где использован литеральный hex/rgba (не var()) и поэтому не могли
* перекраситься автоматически. Оба блока (авто по ОС + явный выбор)
* продублированы намеренно, как в самом tokens.css.
* ===================================================================== */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) .avatar { color: var(--color-accent-text); }
:root:not([data-theme]) .btn-primary:disabled { background: rgba(122, 122, 122, 0.3); color: var(--color-ink-300); }
:root:not([data-theme]) .modal-overlay { background: rgba(0, 0, 0, 0.6); }
:root:not([data-theme]) .field input:focus,
:root:not([data-theme]) .field textarea:focus,
:root:not([data-theme]) .field select:focus { box-shadow: 0 0 0 3px rgba(214, 168, 61, 0.18); }
:root:not([data-theme]) .error-banner { border-color: rgba(235, 147, 161, 0.3); }
}
:root[data-theme='dark'] .avatar { color: var(--color-accent-text); }
:root[data-theme='dark'] .btn-primary:disabled { background: rgba(122, 122, 122, 0.3); color: var(--color-ink-300); }
:root[data-theme='dark'] .modal-overlay { background: rgba(0, 0, 0, 0.6); }
:root[data-theme='dark'] .field input:focus,
:root[data-theme='dark'] .field textarea:focus,
:root[data-theme='dark'] .field select:focus { box-shadow: 0 0 0 3px rgba(214, 168, 61, 0.18); }
:root[data-theme='dark'] .error-banner { border-color: rgba(235, 147, 161, 0.3); }

View File

@@ -0,0 +1,315 @@
/*
* VidConf — design tokens
* ВНИМАНИЕ: это копия design/tokens.css (источник истины — design/DESIGN_SYSTEM.md).
* Файл продублирован во frontend/, т.к. Vite не обслуживает статику вне корня
* проекта. При изменении design/tokens.css копию нужно обновить вручную.
* Значения синхронизированы 1:1.
*
* Три темы, независимые механизмы переключения:
* - Оболочка, светлая (дефолт) — :root.
* - Оболочка, тёмная — :root[data-theme="dark"] (переключатель
* пользователя, см. useTheme/ThemeToggle) ИЛИ автоматически по
* @media (prefers-color-scheme: dark), если data-theme не проставлен
* явно (см. блок ниже; design/mockups/dark/README.md)
* — палитра 1:1 из утверждённых тёмных
* макетов оболочки.
* - Комната конференции — [data-theme="room"] (скоуп на контейнер
* экрана конференции; отдельный неймспейс --color-room-*, НЕ переключается
* системной темой и НЕ смешивается с dark-оболочкой — комната всегда
* тёмная независимо от выбора пользователя).
*
* Шрифты самохостятся через npm-пакеты @fontsource (без внешних CDN —
* инвариант приватности), с кириллическим subset для русского интерфейса.
* @font-face объявляют сами пакеты; их CSS импортируется в
* frontend/src/main.tsx ДО ./index.css. Здесь — только имена семейств в
* переменных ниже (--font-display/--font-body/--font-mono), которые ДОЛЖНЫ
* совпадать с family из @fontsource: 'Unbounded' (@fontsource/unbounded),
* 'Inter' (@fontsource/inter — статический, не variable, у которого было бы
* 'Inter Variable'), 'JetBrains Mono' (@fontsource/jetbrains-mono).
*/
:root {
color-scheme: light;
/* ---------- Типографика ---------- */
--font-display: 'Unbounded', 'Arial Rounded MT Bold', -apple-system, 'Segoe UI', sans-serif;
--font-body: 'Inter', -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
--font-mono: 'JetBrains Mono', 'SFMono-Regular', Menlo, Consolas, monospace;
--text-display-2xl: 700 64px/1.05 var(--font-display);
--text-display-xl: 700 48px/1.1 var(--font-display);
--text-display-lg: 600 34px/1.2 var(--font-display);
--text-h1: 700 28px/1.25 var(--font-body);
--text-h2: 600 22px/1.3 var(--font-body);
--text-h3: 600 18px/1.35 var(--font-body);
--text-body-lg: 400 16px/1.5 var(--font-body);
--text-body: 400 14px/1.5 var(--font-body);
--text-caption: 600 12px/1.4 var(--font-body);
--text-mono-sm: 500 13px/1.4 var(--font-mono);
/* ---------- Spacing (8pt grid, база 4px) ---------- */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
--space-16: 64px;
--space-20: 80px;
--space-24: 96px;
/* ---------- Радиусы ---------- */
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 20px;
--radius-2xl: 24px;
--radius-3xl: 32px;
--radius-full: 999px;
/* ---------- Границы ---------- */
--border-thin: 1px;
--border-md: 1.5px;
--border-thick: 2px;
--border-speaker: 3px;
/* ---------- Breakpoints (справочно, desktop-first) ---------- */
--bp-desktop: 1440px;
--bp-min: 1280px;
/* ---------- Motion ---------- */
--transition-fast: 120ms ease;
--transition-base: 200ms ease;
/* ---------- Цвета: светлая тема (оболочка) ---------- */
--color-bg: #F1F1F1;
--color-bg-alt: #F7F7F8;
--color-surface: #FFFFFF;
--color-surface-glass: rgba(255, 255, 255, 0.55);
--color-border: #E1E3E9;
--color-border-strong: #C7CCDC;
--color-border-pill: #4A5A9E;
--color-ink-900: #2E3454;
--color-ink-700: #3B4D95;
--color-ink-500: #535F94;
--color-ink-400: #6976AC;
--color-ink-300: #A6AECB;
/* Пастельная мята — нейтральный акцент оболочки (акцент не обязан
быть зелёным). Mapping см. design/DESIGN_SYSTEM.md §1.1/§1.3. */
--color-accent: #D4F2E3;
--color-accent-hover: #C7EBD9;
--color-accent-active: #BAE4CE;
--color-accent-text: #2E3454;
--color-success: #0A7A4C;
--color-success-bg: #E4F7ED;
--color-success-dot: #46916B;
--color-danger: #C7364F;
--color-danger-bg: #FCE7EB;
--color-danger-dot: #C65366;
--color-warning: #7F560D;
--color-warning-bg: #FBF0DC;
--color-warning-dot: #B58A26;
--color-focus-ring: #3B4D95;
/* ---------- Тени (светлая тема) ---------- */
--shadow-sm: 0 1px 2px rgba(46, 52, 84, 0.06);
--shadow-md: 0 4px 16px rgba(46, 52, 84, 0.08);
--shadow-lg: 0 12px 32px rgba(46, 52, 84, 0.12);
--shadow-glass: 0 8px 24px rgba(59, 77, 149, 0.15);
}
/* =====================================================================
* Тёмная тема: оболочка (auth/лобби/календарь/join/«мои конференции»/админка)
* Утверждено 2026-07-18. Палитра — 1:1 копия блока :root из
* design/mockups/dark/*.html (все 6 файлов идентичны), происхождение каждого
* значения задокументировано в design/mockups/dark/README.md и продублировано
* в design/DESIGN_SYSTEM.md §1.2b. Принцип: тёмная оболочка не изобретает
* новых цветов, а переиспользует уже утверждённые --color-room-* в новых
* ролях (см. таблицу происхождения), поэтому тёмное лобби и тёмная комната
* читаются как одна система.
*
* Механизм активации (два независимых триггера, одни и те же переопределения
* --color-* / --shadow-* — те же имена, что в :root светлой темы выше):
* 1) явный выбор пользователя — атрибут data-theme="dark" на <html>/:root;
* 2) дефолт по системной теме — @media (prefers-color-scheme: dark),
* только пока data-theme НЕ проставлен явно (:root:not([data-theme])).
* Явный data-theme="light" отключает автодетект и форсирует светлую
* тему (базовый :root выше) даже при тёмной ОС.
* Оба блока продублированы намеренно (plain CSS, без препроцессора) — при
* правке палитры менять оба синхронно.
* ===================================================================== */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) {
color-scheme: dark;
--color-bg: #1E1E1E;
--color-bg-alt: #242424;
--color-surface: #2A2A2A;
--color-surface-glass: rgba(51, 51, 51, 0.55);
--color-border: #3F3F3F;
--color-border-strong: #585858;
--color-border-pill: #7FDDA8;
--color-ink-900: #E8E8E8;
--color-ink-700: #7FDDA8;
--color-ink-500: #B0B0B0;
--color-ink-400: #5D9A78;
--color-ink-300: #7A7A7A;
--color-accent: #7FDDA8;
--color-accent-hover: #75CA9A;
--color-accent-active: #6CB78C;
--color-accent-text: #10331F;
--color-success: #7FDDA8;
--color-success-bg: rgba(127, 221, 168, 0.14);
--color-success-dot: #7FDDA8;
--color-danger: #EB93A1;
--color-danger-bg: rgba(235, 147, 161, 0.14);
--color-danger-dot: #EB93A1;
--color-danger-solid: #B85468;
--color-danger-solid-hover: #B9495F;
--color-warning: #D6A83D;
--color-warning-bg: rgba(214, 168, 61, 0.16);
--color-warning-dot: #D6A83D;
--color-focus-ring: #D6A83D;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.5);
--shadow-glass: 0 8px 24px rgba(0, 0, 0, 0.45);
}
}
:root[data-theme='dark'] {
color-scheme: dark;
--color-bg: #1E1E1E;
--color-bg-alt: #242424;
--color-surface: #2A2A2A;
--color-surface-glass: rgba(51, 51, 51, 0.55);
--color-border: #3F3F3F;
--color-border-strong: #585858;
--color-border-pill: #7FDDA8;
--color-ink-900: #E8E8E8;
--color-ink-700: #7FDDA8;
--color-ink-500: #B0B0B0;
--color-ink-400: #5D9A78;
--color-ink-300: #7A7A7A;
--color-accent: #7FDDA8;
--color-accent-hover: #75CA9A;
--color-accent-active: #6CB78C;
--color-accent-text: #10331F;
--color-success: #7FDDA8;
--color-success-bg: rgba(127, 221, 168, 0.14);
--color-success-dot: #7FDDA8;
--color-danger: #EB93A1;
--color-danger-bg: rgba(235, 147, 161, 0.14);
--color-danger-dot: #EB93A1;
--color-danger-solid: #B85468;
--color-danger-solid-hover: #B9495F;
--color-warning: #D6A83D;
--color-warning-bg: rgba(214, 168, 61, 0.16);
--color-warning-dot: #D6A83D;
--color-focus-ring: #D6A83D;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.5);
--shadow-glass: 0 8px 24px rgba(0, 0, 0, 0.45);
}
/* ---------- Тёмная тема: комната конференции ---------- */
[data-theme='room'] {
color-scheme: dark;
--color-room-bg: #1E1E1E;
--color-room-surface: #2A2A2A;
--color-room-surface-raised: #333333;
--color-room-tile: #363636;
--color-room-tile-border: #3F3F3F;
--color-room-tile-hover: #3D3D3D;
--color-room-text-primary: #E8E8E8;
--color-room-text-secondary: #B0B0B0;
--color-room-text-tertiary: #7A7A7A;
--color-room-mic-on: #7FDDA8;
--color-room-mic-off: #EB93A1;
--color-room-speaker-ring: #D6A83D;
--color-room-danger: #EB93A1;
--color-room-danger-bg: #B85468;
--color-room-danger-bg-hover: #B9495F;
--color-room-focus-ring: #D6A83D;
--shadow-speaker-glow: 0 0 0 3px var(--color-room-speaker-ring), 0 0 24px rgba(214, 168, 61, 0.35);
--shadow-room-panel: 0 8px 24px rgba(0, 0, 0, 0.4);
}
/* ---------- Базовые сбросы, общие для макетов ---------- */
*, *::before, *::after { box-sizing: border-box; }
body {
margin: 0;
font: var(--text-body-lg);
color: var(--color-ink-900);
background: var(--color-bg);
-webkit-font-smoothing: antialiased;
}
[data-theme='room'] body,
[data-theme='room'] {
background: var(--color-room-bg);
color: var(--color-room-text-primary);
}
a { color: inherit; }
button {
font: inherit;
cursor: pointer;
}
:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}
[data-theme='room'] :focus-visible {
outline-color: var(--color-room-focus-ring);
}
svg.icon {
width: 20px;
height: 20px;
stroke: currentColor;
stroke-width: 2;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
flex-shrink: 0;
}