diff --git a/api.js b/api.js
new file mode 100644
index 0000000..9ffbf12
--- /dev/null
+++ b/api.js
@@ -0,0 +1,25 @@
+const API = {
+ async request(path, options = {}) {
+ const res = await fetch(path, {
+ credentials: 'include',
+ headers: { 'Content-Type': 'application/json', ...options.headers },
+ ...options,
+ });
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok) {
+ const detail = data.detail;
+ const msg = Array.isArray(detail)
+ ? detail.map((d) => d.msg).join(', ')
+ : detail || 'Ошибка запроса';
+ throw new Error(msg);
+ }
+ return data;
+ },
+ me: () => API.request('/api/auth/me'),
+ logout: () => API.request('/api/auth/logout', { method: 'POST' }),
+ getProfile: () => API.request('/api/profile'),
+ updateProfile: (body) => API.request('/api/profile', { method: 'PUT', body: JSON.stringify(body) }),
+ getRobots: () => API.request('/api/robots'),
+ saveRobots: (body) => API.request('/api/robots', { method: 'PUT', body: JSON.stringify(body) }),
+ getSessions: () => API.request('/api/sessions'),
+};
diff --git a/app.js b/app.js
new file mode 100644
index 0000000..850548c
--- /dev/null
+++ b/app.js
@@ -0,0 +1,374 @@
+const MODE_LABELS = {
+ auto: 'Автономный',
+ manual: 'Ручной',
+ assist: 'Ассистированный',
+ standby: 'Ожидание',
+};
+
+const STATUS_LABELS = {
+ online: 'ONLINE',
+ idle: 'IDLE',
+ offline: 'OFFLINE',
+ emergency: 'E-STOP',
+};
+
+let currentUser = null;
+let ROBOTS = [];
+let selectedRobotId = null;
+let saveTimer = null;
+
+const $ = (sel) => document.querySelector(sel);
+const $$ = (sel) => document.querySelectorAll(sel);
+
+async function init() {
+ try {
+ currentUser = await API.me();
+ } catch {
+ window.location.href = '/login';
+ return;
+ }
+
+ $('#header-user').textContent = currentUser.email;
+ initNavigation();
+ initLogout();
+ await loadRobots();
+ initControls();
+ initProfile();
+ initFooterClock();
+ addLog('Система инициализирована', 'success');
+}
+
+async function loadRobots() {
+ const data = await API.getRobots();
+ ROBOTS = data.robots;
+ selectedRobotId = data.selected_robot_id || ROBOTS[0]?.id;
+ ROBOTS.forEach(applySettingsToRobot);
+ initRobotList();
+ selectRobot(selectedRobotId, { skipSave: true });
+ $('#robot-count').textContent = ROBOTS.length;
+}
+
+function applySettingsToRobot(robot) {
+ if (robot.emergency) {
+ robot.speed = 0;
+ robot.status = 'emergency';
+ robot.temperature = Math.round((robot.baseTemp || 30) + 5);
+ robot.sensorAccuracy = 0;
+ return;
+ }
+ if (robot.mode === 'standby') {
+ robot.speed = 0;
+ robot.status = 'idle';
+ robot.temperature = Math.round(robot.baseTemp || 30);
+ robot.sensorAccuracy = Math.round(robot.sensitivity * 0.5);
+ return;
+ }
+ const maxSpeed = robot.maxSpeed || 2;
+ robot.speed = +(maxSpeed * robot.speedSetting / 100).toFixed(1);
+ robot.sensorAccuracy = robot.sensitivity;
+ robot.temperature = Math.round(
+ (robot.baseTemp || 30) + robot.speedSetting * 0.12 + robot.sensitivity * 0.06
+ );
+ if (robot.battery < 15) {
+ robot.status = 'offline';
+ robot.speed = 0;
+ } else {
+ robot.status = 'online';
+ }
+}
+
+function readControlsToRobot(robot) {
+ robot.mode = $('#ctrl-mode').value;
+ robot.speedSetting = parseInt($('#ctrl-speed').value, 10);
+ robot.sensitivity = parseInt($('#ctrl-sensitivity').value, 10);
+ robot.emergency = $('#ctrl-emergency').checked;
+ robot.nightMode = $('#ctrl-night').checked;
+}
+
+async function persistRobots() {
+ await API.saveRobots({
+ robots: ROBOTS,
+ selected_robot_id: selectedRobotId,
+ });
+}
+
+function scheduleSave() {
+ clearTimeout(saveTimer);
+ saveTimer = setTimeout(async () => {
+ try {
+ await persistRobots();
+ } catch (err) {
+ showToast(err.message);
+ }
+ }, 500);
+}
+
+async function saveCurrentRobotSettings({ silent = false } = {}) {
+ const robot = ROBOTS.find((r) => r.id === selectedRobotId);
+ if (!robot) return;
+ readControlsToRobot(robot);
+ applySettingsToRobot(robot);
+ refreshRobotListItem(robot);
+ syncRobotUI(robot);
+ try {
+ await persistRobots();
+ if (!silent) {
+ addLog(`Настройки применены: ${robot.name}`, 'success');
+ showToast('Настройки сохранены');
+ }
+ } catch (err) {
+ showToast(err.message);
+ }
+}
+
+function initNavigation() {
+ $$('.nav__btn').forEach((btn) => {
+ btn.addEventListener('click', async () => {
+ const page = btn.dataset.page;
+ const activePage = $('.page--active')?.id;
+
+ if (activePage === 'page-dashboard') {
+ await saveCurrentRobotSettings({ silent: true });
+ }
+ if (activePage === 'page-profile') {
+ await loadProfile();
+ }
+
+ $$('.nav__btn').forEach((b) => b.classList.remove('nav__btn--active'));
+ btn.classList.add('nav__btn--active');
+ $$('.page').forEach((p) => p.classList.remove('page--active'));
+ $(`#page-${page}`).classList.add('page--active');
+
+ if (page === 'profile') await loadProfile();
+ if (page === 'dashboard') {
+ const robot = ROBOTS.find((r) => r.id === selectedRobotId);
+ if (robot) syncRobotUI(robot);
+ }
+ });
+ });
+}
+
+function initLogout() {
+ $('#btn-logout').addEventListener('click', async () => {
+ await API.logout();
+ window.location.href = '/login';
+ });
+}
+
+function initRobotList() {
+ const list = $('#robot-list');
+ list.innerHTML = ROBOTS.map((r) => renderRobotListItem(r)).join('');
+ list.querySelectorAll('.robot-item').forEach((item) => {
+ item.addEventListener('click', async () => {
+ if (item.dataset.id !== selectedRobotId) {
+ await saveCurrentRobotSettings({ silent: true });
+ }
+ selectRobot(item.dataset.id, { skipSave: true });
+ });
+ });
+}
+
+function renderRobotListItem(robot) {
+ const statusClass = robot.status === 'emergency' ? 'emergency' : robot.status;
+ return `
+
+
${String(robot.number).padStart(3, '0')}
+
+
${robot.name}
+
${robot.type}
+
${MODE_LABELS[robot.mode]} · ${robot.speed} м/с
+
+
+
`;
+}
+
+function refreshRobotListItem(robot) {
+ const item = $(`.robot-item[data-id="${robot.id}"]`);
+ if (!item) return;
+ const wrapper = document.createElement('div');
+ wrapper.innerHTML = renderRobotListItem(robot);
+ const newItem = wrapper.firstElementChild;
+ item.replaceWith(newItem);
+ newItem.addEventListener('click', async () => {
+ if (newItem.dataset.id !== selectedRobotId) {
+ await saveCurrentRobotSettings({ silent: true });
+ }
+ selectRobot(newItem.dataset.id, { skipSave: true });
+ });
+}
+
+function selectRobot(id, { skipSave = false } = {}) {
+ selectedRobotId = id;
+ $$('.robot-item').forEach((item) => {
+ item.classList.toggle('robot-item--active', item.dataset.id === id);
+ });
+ const robot = ROBOTS.find((r) => r.id === id);
+ if (!robot) return;
+ loadControls(robot);
+ syncRobotUI(robot);
+ if (!skipSave) scheduleSave();
+ addLog(`Выбран робот ${robot.name}`, 'info');
+}
+
+function syncRobotUI(robot) {
+ $('#selected-robot-name').textContent = robot.name;
+ $('#robot-visual-id').textContent = `№ ${String(robot.number).padStart(3, '0')}`;
+ $('#robot-mode-label').textContent = MODE_LABELS[robot.mode];
+
+ const badge = $('#robot-status-badge');
+ badge.textContent = STATUS_LABELS[robot.status] || robot.status.toUpperCase();
+ badge.className = 'badge badge--status';
+ if (['offline', 'emergency', 'idle'].includes(robot.status)) {
+ badge.classList.add(robot.status);
+ }
+
+ const frame = $('#robot-visual-frame');
+ frame.className = 'robot-visual';
+ if (robot.nightMode) frame.classList.add('robot-visual--night');
+ if (robot.emergency) frame.classList.add('robot-visual--emergency');
+ if (robot.mode === 'standby' && !robot.emergency) frame.classList.add('robot-visual--standby');
+
+ renderStats(robot);
+}
+
+function renderStats(robot) {
+ const maxSpeed = robot.maxSpeed || 2;
+ const stats = [
+ { label: 'АКБ', value: `${robot.battery}%`, percent: robot.battery },
+ { label: 'Скорость', value: `${robot.speed} м/с`, percent: robot.speed / maxSpeed * 100 },
+ { label: 'Нагрузка', value: `${robot.payload} кг`, percent: robot.payload / 60 * 100 },
+ { label: 'Температура', value: `${robot.temperature}°C`, percent: robot.temperature / 80 * 100 },
+ { label: 'Датчики', value: `${robot.sensorAccuracy ?? robot.sensitivity}%`, percent: robot.sensorAccuracy ?? robot.sensitivity },
+ { label: 'Режим', value: MODE_LABELS[robot.mode], percent: robot.speedSetting },
+ ];
+ $('#stats-grid').innerHTML = stats.map((s) => `
+
+
${s.label}
+
${s.value}
+
+
`).join('');
+}
+
+function loadControls(robot) {
+ $('#ctrl-mode').value = robot.mode;
+ $('#ctrl-speed').value = robot.speedSetting;
+ $('#speed-value').textContent = robot.speedSetting;
+ $('#ctrl-sensitivity').value = robot.sensitivity;
+ $('#sensitivity-value').textContent = robot.sensitivity;
+ $('#ctrl-emergency').checked = robot.emergency;
+ $('#ctrl-night').checked = robot.nightMode;
+}
+
+function initControls() {
+ const onChange = () => {
+ const robot = ROBOTS.find((r) => r.id === selectedRobotId);
+ if (!robot) return;
+ readControlsToRobot(robot);
+ applySettingsToRobot(robot);
+ syncRobotUI(robot);
+ refreshRobotListItem(robot);
+ scheduleSave();
+ };
+
+ $('#ctrl-mode').addEventListener('change', onChange);
+ $('#ctrl-emergency').addEventListener('change', onChange);
+ $('#ctrl-night').addEventListener('change', onChange);
+ $('#ctrl-speed').addEventListener('input', (e) => {
+ $('#speed-value').textContent = e.target.value;
+ onChange();
+ });
+ $('#ctrl-sensitivity').addEventListener('input', (e) => {
+ $('#sensitivity-value').textContent = e.target.value;
+ onChange();
+ });
+ $('#btn-apply').addEventListener('click', () => saveCurrentRobotSettings());
+ $('#btn-reset').addEventListener('click', async () => {
+ await loadRobots();
+ showToast('Настройки сброшены');
+ });
+}
+
+async function loadProfile() {
+ currentUser = await API.getProfile();
+ $('#field-email').value = currentUser.email;
+ $('#field-phone').value = currentUser.phone || '';
+ $('#field-firstname').value = currentUser.firstname || '';
+ $('#field-lastname').value = currentUser.lastname || '';
+ $('#field-patronymic').value = currentUser.patronymic || '';
+ $('#field-age').value = currentUser.age || '';
+ $('#profile-email').textContent = currentUser.email;
+ $('#profile-rating').textContent = currentUser.rating.toFixed(1);
+ $('#profile-points').textContent = currentUser.points;
+ updateProfileDisplay();
+
+ const { sessions } = await API.getSessions();
+ $('#sessions-list').innerHTML = sessions.length
+ ? sessions.map((s) => `
+
+
Сессия #${s.id}
+
+ Создана: ${formatDate(s.created_at)} · Истекает: ${formatDate(s.expires_at)}
+ IP: ${s.ip_address || '—'} · ${s.user_agent?.slice(0, 60) || '—'}
+
+
`).join('')
+ : 'Нет активных сессий
';
+}
+
+function initProfile() {
+ $('#profile-form').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ try {
+ currentUser = await API.updateProfile({
+ firstname: $('#field-firstname').value,
+ lastname: $('#field-lastname').value,
+ patronymic: $('#field-patronymic').value,
+ age: parseInt($('#field-age').value, 10) || null,
+ phone: $('#field-phone').value,
+ });
+ updateProfileDisplay();
+ $('#profile-rating').textContent = currentUser.rating.toFixed(1);
+ $('#profile-points').textContent = currentUser.points;
+ addLog('Профиль обновлён', 'success');
+ showToast('Профиль сохранён');
+ } catch (err) {
+ showToast(err.message);
+ }
+ });
+
+ $('#btn-cancel-profile').addEventListener('click', () => loadProfile());
+}
+
+function updateProfileDisplay() {
+ const parts = [currentUser.lastname, currentUser.firstname, currentUser.patronymic].filter(Boolean);
+ $('#profile-fullname').textContent = parts.length ? parts.join(' ') : currentUser.email;
+}
+
+function formatDate(iso) {
+ return new Date(iso).toLocaleString('ru-RU');
+}
+
+function addLog(message, type = 'info') {
+ const log = $('#event-log');
+ const time = new Date().toLocaleTimeString('ru-RU');
+ const entry = document.createElement('div');
+ entry.className = `log__entry log__entry--${type}`;
+ entry.innerHTML = `${time}
${message}
`;
+ log.prepend(entry);
+ while (log.children.length > 50) log.removeChild(log.lastChild);
+}
+
+function showToast(message) {
+ const toast = $('#toast');
+ toast.textContent = message;
+ toast.classList.add('toast--visible');
+ clearTimeout(showToast._t);
+ showToast._t = setTimeout(() => toast.classList.remove('toast--visible'), 3000);
+}
+
+function initFooterClock() {
+ const tick = () => { $('#footer-time').textContent = new Date().toLocaleString('ru-RU'); };
+ tick();
+ setInterval(tick, 1000);
+}
+
+document.addEventListener('DOMContentLoaded', init);
diff --git a/auth.js b/auth.js
new file mode 100644
index 0000000..e5a43ed
--- /dev/null
+++ b/auth.js
@@ -0,0 +1,23 @@
+document.addEventListener('DOMContentLoaded', () => {
+ const form = document.getElementById('auth-form');
+ const errorEl = document.getElementById('auth-error');
+ const isRegister = document.body.dataset.page === 'register';
+
+ form.addEventListener('submit', async (e) => {
+ e.preventDefault();
+ errorEl.textContent = '';
+ const email = document.getElementById('email').value.trim();
+ const password = document.getElementById('password').value;
+ const endpoint = isRegister ? '/api/auth/register' : '/api/auth/login';
+
+ try {
+ await API.request(endpoint, {
+ method: 'POST',
+ body: JSON.stringify({ email, password }),
+ });
+ window.location.href = '/';
+ } catch (err) {
+ errorEl.textContent = err.message;
+ }
+ });
+});
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..c551a9d
--- /dev/null
+++ b/index.html
@@ -0,0 +1,171 @@
+
+
+
+
+
+ ROBO-OPS | Панель оператора
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Настройки: —
+ —
+
+
+
+
+
+
+
+
Параметры управления
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
—
+
—
+
+
+ Рейтинг
+ 0
+
+
+ Баллы
+ 0
+
+
+
+
+
+
+
+
+
Активные сессии
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/styles.css b/styles.css
new file mode 100644
index 0000000..c72c75a
--- /dev/null
+++ b/styles.css
@@ -0,0 +1,322 @@
+:root {
+ --bg: #f5f5f5;
+ --surface: #fff;
+ --border: #ddd;
+ --text: #222;
+ --text-muted: #666;
+ --accent: #2563eb;
+ --accent-hover: #1d4ed8;
+ --danger: #dc2626;
+ --success: #16a34a;
+ --warning: #ca8a04;
+ --radius: 6px;
+}
+
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ background: var(--bg);
+ color: var(--text);
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ font-size: 14px;
+ line-height: 1.5;
+}
+
+/* Header */
+.header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 24px;
+ background: var(--surface);
+ border-bottom: 1px solid var(--border);
+}
+
+.logo { font-weight: 700; font-size: 16px; }
+.header__user { margin-left: 16px; color: var(--text-muted); font-size: 13px; }
+
+.nav { display: flex; gap: 8px; align-items: center; }
+
+.nav__btn {
+ padding: 6px 14px;
+ border: 1px solid var(--border);
+ background: var(--surface);
+ border-radius: var(--radius);
+ cursor: pointer;
+ font-size: 13px;
+ color: var(--text);
+}
+
+.nav__btn:hover { border-color: #aaa; }
+.nav__btn--active { border-color: var(--accent); color: var(--accent); background: #eff6ff; }
+
+/* Main */
+.main { flex: 1; padding: 20px 24px; max-width: 1400px; margin: 0 auto; width: 100%; }
+
+.page { display: none; }
+.page--active { display: block; }
+
+.page__header { margin-bottom: 20px; }
+.page__title { font-size: 20px; font-weight: 600; margin-bottom: 4px; }
+.page__subtitle { color: var(--text-muted); }
+
+/* Panel */
+.panel {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ padding: 16px;
+}
+
+.panel__head {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 12px;
+ padding-bottom: 8px;
+ border-bottom: 1px solid var(--border);
+}
+
+.panel__title { font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-muted); }
+
+.badge {
+ font-size: 11px;
+ padding: 2px 8px;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--text-muted);
+}
+
+.badge--status { color: var(--success); border-color: #bbf7d0; background: #f0fdf4; }
+.badge--status.offline { color: var(--danger); border-color: #fecaca; background: #fef2f2; }
+.badge--status.emergency { color: var(--danger); border-color: #fecaca; background: #fef2f2; }
+.badge--status.idle { color: var(--warning); border-color: #fef08a; background: #fefce8; }
+
+/* Dashboard */
+.dashboard-grid {
+ display: grid;
+ grid-template-columns: 240px 1fr 260px;
+ gap: 16px;
+ align-items: start;
+}
+
+.robot-list { display: flex; flex-direction: column; gap: 6px; }
+
+.robot-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px;
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ cursor: pointer;
+}
+
+.robot-item:hover { border-color: #aaa; }
+.robot-item--active { border-color: var(--accent); background: #eff6ff; }
+
+.robot-item__num {
+ font-size: 11px;
+ font-weight: 700;
+ color: var(--accent);
+ min-width: 32px;
+ text-align: center;
+}
+
+.robot-item__name { font-weight: 600; font-size: 13px; }
+.robot-item__type, .robot-item__mode { font-size: 11px; color: var(--text-muted); }
+
+.robot-item__indicator {
+ width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
+}
+.robot-item__indicator--online { background: var(--success); }
+.robot-item__indicator--offline, .robot-item__indicator--emergency { background: var(--danger); }
+.robot-item__indicator--idle { background: var(--warning); }
+
+.robot-visual {
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ padding: 20px;
+ margin-bottom: 16px;
+ text-align: center;
+ min-height: 80px;
+ position: relative;
+ background: #fafafa;
+}
+
+.robot-visual--night { background: #1e1b2e; color: #c4b5fd; border-color: #7c3aed; }
+.robot-visual--emergency { background: #fef2f2; border-color: var(--danger); }
+.robot-visual--standby { background: #fefce8; border-color: var(--warning); }
+
+.robot-visual__id { font-size: 12px; color: var(--text-muted); }
+.robot-visual__mode { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
+
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 8px;
+ margin-bottom: 16px;
+}
+
+.stat-card {
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ padding: 10px;
+ text-align: center;
+}
+
+.stat-card__label { font-size: 10px; color: var(--text-muted); text-transform: uppercase; }
+.stat-card__value { font-size: 16px; font-weight: 600; margin-top: 2px; }
+
+.stat-card__bar {
+ height: 3px;
+ background: #eee;
+ border-radius: 2px;
+ margin-top: 6px;
+ overflow: hidden;
+}
+
+.stat-card__bar-fill {
+ height: 100%;
+ background: var(--accent);
+ transition: width 0.3s;
+}
+
+.controls__title { font-size: 12px; color: var(--text-muted); margin-bottom: 12px; text-transform: uppercase; }
+.control-group { margin-bottom: 12px; }
+.control-group--row { display: flex; gap: 20px; }
+
+.control-label { display: flex; flex-direction: column; gap: 4px; font-size: 13px; }
+.control-select, .form-field input {
+ padding: 8px 10px;
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ font-size: 14px;
+ background: var(--surface);
+}
+
+.control-select:focus, .form-field input:focus {
+ outline: none;
+ border-color: var(--accent);
+}
+
+.control-range { width: 100%; margin-top: 4px; }
+.checkbox-label { font-size: 13px; display: flex; align-items: center; gap: 6px; cursor: pointer; }
+
+.control-actions, .form-actions { display: flex; gap: 8px; margin-top: 12px; }
+
+/* Buttons */
+.btn {
+ padding: 8px 16px;
+ border-radius: var(--radius);
+ font-size: 13px;
+ cursor: pointer;
+ border: 1px solid var(--border);
+ background: var(--surface);
+}
+
+.btn--primary { background: var(--accent); color: #fff; border-color: var(--accent); }
+.btn--primary:hover { background: var(--accent-hover); }
+.btn--secondary:hover { border-color: #aaa; }
+.btn--ghost { background: transparent; }
+.btn--block { width: 100%; }
+
+/* Log */
+.log { max-height: 400px; overflow-y: auto; }
+.log__entry { padding: 6px 8px; border-left: 2px solid var(--border); margin-bottom: 4px; font-size: 12px; }
+.log__entry--success { border-left-color: var(--success); }
+.log__entry--warning { border-left-color: var(--warning); }
+.log__entry--info { border-left-color: var(--accent); }
+.log__time { color: var(--text-muted); font-size: 11px; }
+
+/* Profile */
+.profile-layout {
+ display: grid;
+ grid-template-columns: 280px 1fr;
+ gap: 16px;
+}
+
+.profile-sessions { grid-column: 1 / -1; }
+
+.profile-card__name { font-size: 18px; font-weight: 600; margin-bottom: 4px; }
+.profile-card__email { color: var(--text-muted); margin-bottom: 16px; }
+
+.profile-stats { display: flex; gap: 16px; }
+.profile-stat { flex: 1; border: 1px solid var(--border); border-radius: var(--radius); padding: 12px; text-align: center; }
+.profile-stat__label { font-size: 11px; color: var(--text-muted); text-transform: uppercase; }
+.profile-stat__value { font-size: 24px; font-weight: 700; color: var(--accent); }
+
+.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; }
+.form-field { display: flex; flex-direction: column; gap: 4px; }
+.form-field label { font-size: 12px; color: var(--text-muted); }
+
+.sessions-list { display: flex; flex-direction: column; gap: 6px; }
+.session-item {
+ padding: 10px;
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ font-size: 12px;
+}
+.session-item__meta { color: var(--text-muted); margin-top: 4px; }
+
+/* Auth */
+.auth-page {
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 20px;
+}
+
+.auth-card {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ padding: 32px;
+ width: 100%;
+ max-width: 400px;
+}
+
+.auth-card h1 { font-size: 22px; margin-bottom: 4px; }
+.auth-subtitle { color: var(--text-muted); margin-bottom: 20px; }
+.auth-form { display: flex; flex-direction: column; gap: 12px; }
+.auth-link { margin-top: 16px; text-align: center; font-size: 13px; color: var(--text-muted); }
+.auth-link a { color: var(--accent); }
+.form-error { color: var(--danger); font-size: 13px; min-height: 18px; }
+
+/* Footer */
+.footer {
+ display: flex;
+ justify-content: space-between;
+ padding: 12px 24px;
+ border-top: 1px solid var(--border);
+ font-size: 12px;
+ color: var(--text-muted);
+ background: var(--surface);
+}
+
+/* Toast */
+.toast {
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ padding: 10px 16px;
+ background: var(--text);
+ color: #fff;
+ border-radius: var(--radius);
+ font-size: 13px;
+ opacity: 0;
+ transform: translateY(10px);
+ transition: all 0.3s;
+ pointer-events: none;
+}
+
+.toast--visible { opacity: 1; transform: translateY(0); }
+
+@media (max-width: 1000px) {
+ .dashboard-grid, .profile-layout, .form-grid { grid-template-columns: 1fr; }
+ .header { flex-direction: column; gap: 10px; align-items: flex-start; }
+}