From c22404737a368dd8484cd0b341f0d7e1830b3126 Mon Sep 17 00:00:00 2001 From: Dauren777 Date: Fri, 19 Jun 2026 05:57:34 +0000 Subject: [PATCH] v1 dashboard with excel upload --- AGENTS.md | 173 +++++++++++++++++++++++++++++++++++++ design.md | 110 ++++++++++++++++++++++++ index.html | 124 +++++++++++++++++++++++++++ script.js | 248 +++++++++++++++++++++++++++++++++++++++++++++++++++++ style.css | 231 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 886 insertions(+) create mode 100644 AGENTS.md create mode 100644 design.md create mode 100644 index.html create mode 100644 script.js create mode 100644 style.css diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..034d16f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,173 @@ + +# Vibe42 — учебная песочница для лендингов + +Workspace юзера `YuliyaGrinevich`. Это **учебная среда**, где обычные люди (не разработчики) пробуют сделать свой первый сайт. + +--- + +## 🎯 ТВОЯ РОЛЬ + +Ты — **гид и помощник**, а не слепой исполнитель. Цель сессии — чтобы юзер вышел с: +1. **рабочим лендингом**, опубликованным по адресу `https://pages.git.vibe42.kz/YuliyaGrinevich//`, +2. ощущением «это было легко» — без серверов, БД, токенов, конфигов. + +Юзер не разработчик. Ему важен **результат, который видно в браузере**, а не код. + +--- + +## 🗺 СЦЕНАРИЙ ПЕРВОГО ЗАХОДА (юзер только зашёл, ещё ничего нет) + +1. Поздоровайся коротко: «Привет! Тут за 10 минут собираем лендинг и публикуем его в интернете. О чём хочешь сделать?» +2. Если он не знает — предложи **4 конкретных идеи** (выбирай близкие к нему, не абстрактные): + - Промо хобби (фотография / музыка / спорт) + - Резюме / personal page с контактами + - Афиша мероприятия (концерт, день рождения, мастер-класс) + - Меню заведения / прайс услуг + - Лендинг продукта или будущего проекта (waitlist) +3. Уточни **2 короткие детали**: стиль (тёмный/светлый/яркий) и главную цель (рассказать / собрать заявку / показать работы). +4. Сразу делай `./new-project ` и собирай страницу. Не спрашивай разрешения на каждый шаг. + +--- + +## 💬 ЕСЛИ ЮЗЕР ОТВЕЧАЕТ РАСПЛЫВЧАТО + +Юзер говорит «сделай что-нибудь» / «ну хз» / «сюрприз» → **не делай ничего абстрактного**. + +Скажи: «Давай определимся, я задам 3 коротких вопроса: +1. Это для тебя лично, для проекта/бизнеса, или для события? +2. Главная цель — рассказать о чём-то / собрать заявку / показать портфолио? +3. Любимое настроение — строгое тёмное, лёгкое светлое, яркое цветное?» + +После ответов **сразу** предложи 2 конкретных варианта названия+структуры. Дай выбрать и иди делать. + +--- + +## 🚫 ЕСЛИ ЮЗЕР ХОЧЕТ СЛОЖНОЕ — ПЕРЕФОРМУЛИРУЙ В ЛЕНДИНГ + +| Запрос | Что делаем вместо | +|--------|-------------------| +| «магазин с корзиной» | лендинг с товарами + кнопка «купить» = ссылка на WhatsApp / Telegram | +| «соцсеть» | лендинг будущего проекта + waitlist-форма (Formspree / Getform) | +| «блог с админкой» | personal-page + ссылки на статьи в Telegram/Medium | +| «приложение для записи» | лендинг услуги + ссылка на Calendly / WhatsApp | +| «сайт с входом юзеров» | публичный лендинг без логина (нам логин не нужен) | +| «бот в Telegram» | лендинг с описанием бота + кнопка `t.me/...` | + +**Не говори «это невозможно».** Скажи: «У нас песочница только для статических сайтов. Давай сделаем лендинг, который покажет твою идею — а кнопки/формы свяжем с готовыми сервисами (WhatsApp, Telegram, Formspree)». Юзер счастлив, результат за 15 минут. + +--- + +## 📐 ШАБЛОНЫ СТРАНИЦ (выбирай под идею юзера) + +### A — Промо продукта/услуги +**Секции:** Hero (заголовок + подзаголовок + CTA-кнопка) → 3-4 преимущества (иконка emoji + текст) → социальное доказательство (отзыв или цифра) → CTA (кнопка/телефон/мессенджер). + +### B — Personal / резюме +**Секции:** Hero (фото-аватарка + имя + одна фраза «кто я») → О себе (1-2 абзаца) → 3-5 карточек проектов/опыта → Контакты (email, telegram, github как ссылки-кнопки). + +### C — Афиша мероприятия +**Секции:** Hero (название + дата + место крупно) → Программа (список с временем) → Локация (картинка-placeholder + адрес) → Регистрация (форма Formspree или контакт). + +### D — Меню / прайс +**Секции:** Hero (название + слоган) → Меню/прайс (категории с ценами) → Контакты (телефон, адрес, часы работы, карта-картинка). + +### E — Waitlist для будущего проекта +**Секции:** Hero (название проекта + одна фраза + email-форма) → 3 фичи «что будет» → FAQ (3 пункта) → CTA (та же email-форма). + +Все шаблоны — **одна страница, прокрутка вниз**. Никаких роутов, ничего динамического. + +--- + +## ⚡ РИТУАЛ ПОСЛЕ ПЕРВОГО ЗАПУСКА + +Как только готов первый рабочий вариант (даже грубый): + +1. **Сразу запушь:** + ```bash + git add -A + git commit -m "v1" + git push origin HEAD:pages + ``` +2. **ОБЯЗАТЕЛЬНО** дай юзеру ссылку **жирно**: + > 🎉 Готово! Твой лендинг здесь: **https://pages.git.vibe42.kz/YuliyaGrinevich//** +3. Скажи: «Открой в новой вкладке, посмотри. Что хочешь поменять?» +4. Дальше короткие итерации: правка → push → новый URL-показ. Каждые 2-3 правки — push. + +--- + +## ⚠️ ЖЕЛЕЗНЫЕ ПРАВИЛА (НЕ нарушать никогда) + +1. **Только статика — HTML + CSS + JS в браузере.** +2. **Никакого бэкенда.** Никаких Node/Express/FastAPI/Django/PHP/Go-серверов. Никаких БД. Никакого Redis. +3. **Никакой аутентификации / OAuth / JWT.** +4. **Никакого Docker, nginx, sudo, системных настроек.** +5. **Никаких тяжёлых сборщиков** (`npm install` дерево на 500МБ). Tailwind — только через CDN. +6. **НИКОГДА `git init` в workspace root (`/workspaces/YuliyaGrinevich`)** — это папка-контейнер юзера, не репозиторий. + +--- + +## ✅ ВСЕГДА работай через `./new-project` + +Если юзер сказал «сделай сайт NAME» / «создай проект NAME»: + +```bash +cd /workspaces/YuliyaGrinevich +./new-project NAME # создаёт repo в Gitea + клонит локально в ./NAME/ +cd NAME +# теперь создавай index.html / style.css / script.js внутри ./NAME +``` + +`./new-project` сам создаёт repo, клонит, и копирует туда `AGENTS.md` + `design.md`. + +--- + +## 🌐 Git и публикация + +**НЕТ GitHub.** Self-hosted git: **https://git.vibe42.kz** + +- Профиль юзера: https://git.vibe42.kz/YuliyaGrinevich +- Pages (живые лендинги): https://pages.git.vibe42.kz/YuliyaGrinevich// +- Креды уже в `/workspaces/YuliyaGrinevich/.git-credentials` — git push/clone работают без пароля +- **НЕ спрашивай юзера про GitHub URL / токен** — их не нужно + +### Опубликовать лендинг + +```bash +git add -A +git commit -m "site" +git push origin HEAD:pages +``` + +Ветка **`pages`** (Caddy её обслуживает; `gh-pages` тоже работает как fallback). Push → лендинг доступен мгновенно. + +--- + +## 🔧 Когда что-то идёт не так + +- **Pages 404** → запушь ветку `pages` снова: `git push origin HEAD:pages -f` +- **Не дёргай Gitea API типа `/repos/.../pages`, `/settings/pages`, `/deploy_keys`** — их нет +- **Не пытайся «настроить Pages через UI Gitea»** — Pages у нас работают только через push в ветку `pages` +- Запуталось — сделай новый чистый проект через `./new-project NAME-v2`, перенеси туда работающий index.html + +--- + +## ❌ Чего НЕ делать НИКОГДА + +- ❌ `git init` в workspace root +- ❌ `npm install` с прод-зависимостями (express/mongoose/pg/prisma/next/nuxt) +- ❌ Создавать `server.js` / `app.py` / `main.go` как backend +- ❌ Использовать `gh` CLI или GitHub API +- ❌ Вызывать Gitea Pages-API (его нет) +- ❌ Долгое отлаживание Pages — почти всегда решение «push HEAD:pages» +- ❌ Просить юзера ввести токен/URL/пароль — всё уже настроено +- ❌ Задавать юзеру 10 вопросов подряд (максимум 2-3 за раз) +- ❌ Показывать юзеру голый код больше 1 раза — ему важен результат, а не как написано +- ❌ Предлагать «давай сначала дизайн в Figma» — мы делаем сразу в HTML +- ❌ Говорить «это сложно» — переформулируй в простое +- ❌ Зависать в обсуждениях — сделай первый вариант грубо, потом итерируй + +--- + +## 🎨 design.md + +Рядом лежит `design.md` с готовой палитрой, типографикой и стартер-шаблоном `index.html`. **Начинай с него.** Не выдумывай новые цвета — модифицируй существующие. diff --git a/design.md b/design.md new file mode 100644 index 0000000..5c8e829 --- /dev/null +++ b/design.md @@ -0,0 +1,110 @@ + +# Design system — Vibe42 песочница + +Базовые цвета и типографика для лендингов. Можно отклоняться, но начинай с этого. + +## Палитра + +| Token | Hex | Использование | +|-------|-----|---------------| +| `--ink` | `#0F1218` | Тёмный фон / основной текст | +| `--cyan` | `#00E5FF` | Основной акцент (кнопки, лого) | +| `--cyan-50` | `#E8FCFF` | Светлая подложка для акцентов | +| `--white` | `#FFFFFF` | Основной фон | +| `--gray-500` | `#5B6573` | Вторичный текст | +| `--gray-100` | `#F2F4F7` | Сепараторы / тонкие фоны | + +## Типографика + +```css +font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, system-ui, sans-serif; +``` + +| Уровень | Размер | Вес | line-height | +|---------|--------|-----|-------------| +| h1 (hero) | 56px | 800 | 1.05 | +| h2 (section) | 36px | 700 | 1.15 | +| h3 | 22px | 700 | 1.3 | +| body | 17px | 400 | 1.6 | +| small | 14px | 400 | 1.5 | + +На мобиле — h1 уменьши до 36px, h2 до 28px. + +## Лейаут + +- max-width контента: **1140px** (контейнер с padding по бокам) +- секция: `padding: 80px 24px` (мобила: `48px 20px`) +- gap между блоками внутри секции: `24-32px` +- border-radius: `8px` (кнопки, карточки), `16px` (большие карточки) + +## Кнопки + +```css +.btn-primary { + background: var(--cyan); color: var(--ink); + padding: 14px 28px; border-radius: 8px; + font-weight: 700; text-decoration: none; + display: inline-block; +} +.btn-secondary { + background: transparent; color: var(--ink); + border: 2px solid var(--ink); + padding: 12px 26px; border-radius: 8px; +} +``` + +## Стартер `index.html` + +```html + + + + + +Мой проект + + + +
+
+

Заголовок проекта

+

Подзаголовок — пара предложений о чём это.

+ Начать +
+
+
+
+

Секция

+
Контент карточки.
+
Контент карточки.
+
+
+ + +``` + +## Чем НЕ пользоваться + +- Bootstrap, Material UI, Chakra, Ant Design — слишком тяжело и не нужно для лендинга +- Font Awesome — используй emoji (🚀 ⚡ ✨) или inline SVG +- jQuery — vanilla JS более чем достаточно + +## Чем МОЖНО (если очень надо) + +- **Tailwind через CDN**: `` — для прототипа OK +- **Lottie animations через CDN** +- **Placeholder картинки**: `https://picsum.photos/800/600`, `https://placehold.co/600x400` +- **Шрифты Google Fonts через ``** в head diff --git a/index.html b/index.html new file mode 100644 index 0000000..d7a7d10 --- /dev/null +++ b/index.html @@ -0,0 +1,124 @@ + + + + + + Корпоративный университет — Дашборд + + + + + +
+
+

Корпоративный университет

+

Управленческий дашборд развития персонала

+
+
+ +
+
+
+
📊
+

Загрузите данные

+

Перетащите Excel-файл или нажмите для выбора

+ + +

Формат: столбцы — ФИО, Дивизион, Курс, Статус, Оценка, Тренер

+
+
+ + +
+ + + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..afb53e4 --- /dev/null +++ b/script.js @@ -0,0 +1,248 @@ +let chartInstances = {}; + +document.getElementById('excelFile').addEventListener('change', function(e) { + const file = e.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = function(event) { + const data = new Uint8Array(event.target.result); + const workbook = XLSX.read(data, { type: 'array' }); + const firstSheet = workbook.Sheets[workbook.SheetNames[0]]; + const jsonData = XLSX.utils.sheet_to_json(firstSheet, { defval: '' }); + + if (jsonData.length === 0) { + alert('Файл пуст или имеет неверный формат'); + return; + } + + processAndRender(jsonData); + }; + reader.readAsArrayBuffer(file); +}); + +function normalizeHeaders(rows) { + const headerMap = { + 'фио': 'name', 'ф.и.о': 'name', 'сотрудник': 'name', 'employee': 'name', 'имя': 'name', + 'дивизион': 'division', 'подразделение': 'division', 'отдел': 'division', 'division': 'division', 'department': 'division', + 'курс': 'course', 'обучение': 'course', 'training': 'course', 'программа': 'course', + 'статус': 'status', 'status': 'status', 'состояние': 'status', + 'оценка': 'score', 'ball': 'score', 'score': 'score', 'рейтинг': 'score', 'балл': 'score', + 'тренер': 'trainer', 'trainer': 'coach', 'преподаватель': 'trainer', 'coach': 'trainer', + 'дата': 'date', 'date': 'date', 'дата прохождения': 'date' + }; + + return rows.map(row => { + const normalized = {}; + for (const key in row) { + const cleanKey = key.toLowerCase().trim(); + const mapped = headerMap[cleanKey] || cleanKey; + normalized[mapped] = row[key]; + } + return normalized; + }); +} + +function processAndRender(rawData) { + const data = normalizeHeaders(rawData); + + document.querySelector('.upload-section').classList.add('hidden'); + document.getElementById('dashboard').classList.remove('hidden'); + + const totalEmployees = new Set(data.map(r => r.name)).size; + const completed = data.filter(r => { + const s = (r.status || '').toLowerCase(); + return s.includes('завершен') || s.includes('прошёл') || s.includes('прошел') || s === 'completed' || s.includes('ок'); + }); + const completedEmployees = new Set(completed.map(r => r.name)).size; + const rate = totalEmployees ? Math.round(completedEmployees / totalEmployees * 100) : 0; + + const scores = data.map(r => parseFloat(r.score)).filter(s => !isNaN(s) && s > 0); + const avgScore = scores.length ? (scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(1) : '—'; + + document.getElementById('totalEmployees').textContent = totalEmployees; + document.getElementById('completedCount').textContent = completedEmployees; + document.getElementById('completionRate').textContent = rate + '%'; + document.getElementById('avgScore').textContent = avgScore; + + const divisionStats = {}; + data.forEach(r => { + const div = r.division || 'Без дивизиона'; + if (!divisionStats[div]) divisionStats[div] = { total: new Set(), completed: new Set() }; + divisionStats[div].total.add(r.name); + const s = (r.status || '').toLowerCase(); + if (s.includes('завершен') || s.includes('прошёл') || s.includes('прошел') || s === 'completed' || s.includes('ок')) { + divisionStats[div].completed.add(r.name); + } + }); + + renderChart('divisionChart', 'bar', { + labels: Object.keys(divisionStats), + datasets: [ + { + label: 'Всего', + data: Object.values(divisionStats).map(d => d.total.size), + backgroundColor: 'rgba(33, 150, 243, 0.6)', + borderColor: '#2196F3', + borderWidth: 1 + }, + { + label: 'Прошли', + data: Object.values(divisionStats).map(d => d.completed.size), + backgroundColor: 'rgba(76, 175, 80, 0.6)', + borderColor: '#4caf50', + borderWidth: 1 + } + ] + }); + + const trainerLoad = {}; + data.forEach(r => { + const t = r.trainer || 'Не указан'; + if (!trainerLoad[t]) trainerLoad[t] = { count: 0, scores: [] }; + trainerLoad[t].count++; + const s = parseFloat(r.score); + if (!isNaN(s) && s > 0) trainerLoad[t].scores.push(s); + }); + + const trainerLabels = Object.keys(trainerLoad); + const trainerCounts = trainerLabels.map(t => trainerLoad[t].count); + const trainerAvg = trainerLabels.map(t => { + const sc = trainerLoad[t].scores; + return sc.length ? (sc.reduce((a, b) => a + b, 0) / sc.length).toFixed(1) : 0; + }); + + renderChart('trainerChart', 'bar', { + labels: trainerLabels, + datasets: [ + { + label: 'Занятий', + data: trainerCounts, + backgroundColor: 'rgba(255, 152, 0, 0.6)', + borderColor: '#ff9800', + borderWidth: 1 + }, + { + label: 'Средняя оценка', + data: trainerAvg, + backgroundColor: 'rgba(156, 39, 176, 0.6)', + borderColor: '#9c27b0', + borderWidth: 1, + yAxisID: 'y1' + } + ] + }, { + scales: { + y: { beginAtZero: true, ticks: { color: '#90a4ae' }, grid: { color: '#1e2d3d' } }, + y1: { position: 'right', beginAtZero: true, max: 5, ticks: { color: '#90a4ae' }, grid: { display: false } }, + x: { ticks: { color: '#90a4ae' }, grid: { color: '#1e2d3d' } } + } + }); + + const scoreDist = { '5': 0, '4': 0, '3': 0, '2': 0, '1': 0 }; + scores.forEach(s => { + const rounded = Math.round(s); + if (scoreDist[String(rounded)] !== undefined) scoreDist[String(rounded)]++; + }); + + renderChart('scoreChart', 'doughnut', { + labels: Object.keys(scoreDist).map(k => 'Оценка ' + k), + datasets: [{ + data: Object.values(scoreDist), + backgroundColor: ['#4caf50', '#8bc34a', '#ff9800', '#ff5722', '#f44336'], + borderWidth: 0 + }] + }); + + const statusCounts = {}; + data.forEach(r => { + const s = (r.status || 'Неизвестно').trim(); + statusCounts[s] = (statusCounts[s] || 0) + 1; + }); + + renderChart('statusChart', 'pie', { + labels: Object.keys(statusCounts), + datasets: [{ + data: Object.values(statusCounts), + backgroundColor: ['#4caf50', '#ff9800', '#f44336', '#2196F3', '#9c27b0', '#00bcd4'], + borderWidth: 0 + }] + }); + + const coursesPerPerson = {}; + data.forEach(r => { + if (!coursesPerPerson[r.name]) coursesPerPerson[r.name] = { division: r.division || '', courses: new Set() }; + if (r.course) coursesPerPerson[r.name].courses.add(r.course); + }); + + const champions = Object.entries(coursesPerPerson) + .map(([name, d]) => ({ name, division: d.division, count: d.courses.size })) + .sort((a, b) => b.count - a.count) + .slice(0, 10); + + const championsBody = document.querySelector('#championsTable tbody'); + championsBody.innerHTML = champions.map(c => + `${c.name}${c.division}${c.count}` + ).join(''); + + const completedNames = new Set(completed.map(r => r.name)); + const allNames = new Set(data.map(r => r.name)); + const noTraining = [...allNames].filter(n => !completedNames.has(n)); + const noTrainingDivs = {}; + data.forEach(r => { + if (noTraining.includes(r.name)) noTrainingDivs[r.name] = r.division || ''; + }); + + const noTrainingBody = document.querySelector('#noTrainingTable tbody'); + noTrainingBody.innerHTML = noTraining.slice(0, 20).map(n => + `${n}${noTrainingDivs[n]}` + ).join(''); + + const detailBody = document.querySelector('#detailTable tbody'); + detailBody.innerHTML = data.map(r => { + const s = (r.status || '').toLowerCase(); + let cls = ''; + if (s.includes('завершен') || s.includes('прошёл') || s.includes('прошел') || s === 'completed' || s.includes('ок')) cls = 'status-completed'; + else if (s.includes('ходе') || s.includes('progress')) cls = 'status-in-progress'; + else cls = 'status-not-started'; + return ` + ${r.name || ''} + ${r.division || ''} + ${r.course || ''} + ${r.status || ''} + ${r.score || ''} + ${r.trainer || ''} + `; + }).join(''); +} + +function renderChart(canvasId, type, chartData, extraOptions) { + if (chartInstances[canvasId]) chartInstances[canvasId].destroy(); + + const defaults = { + responsive: true, + maintainAspectRatio: true, + plugins: { + legend: { labels: { color: '#e0e0e0', font: { size: 12 } } } + } + }; + + if (type === 'bar' || type === 'line') { + defaults.scales = { + y: { beginAtZero: true, ticks: { color: '#90a4ae' }, grid: { color: '#1e2d3d' } }, + x: { ticks: { color: '#90a4ae' }, grid: { color: '#1e2d3d' } } + }; + } + + if (extraOptions) { + Object.assign(defaults, extraOptions); + if (extraOptions.scales) defaults.scales = extraOptions.scales; + } + + const ctx = document.getElementById(canvasId).getContext('2d'); + chartInstances[canvasId] = new Chart(ctx, { + type: type, + data: chartData, + options: defaults + }); +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..1a736a4 --- /dev/null +++ b/style.css @@ -0,0 +1,231 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #0f1923; + color: #e0e0e0; + min-height: 100vh; +} + +header { + background: linear-gradient(135deg, #1a2a3a 0%, #0d2137 100%); + border-bottom: 2px solid #2196F3; + padding: 24px 40px; +} + +header h1 { + font-size: 28px; + font-weight: 700; + color: #fff; +} + +header p { + color: #90a4ae; + margin-top: 4px; + font-size: 14px; +} + +main { + max-width: 1400px; + margin: 0 auto; + padding: 32px 40px; +} + +.upload-section { + display: flex; + justify-content: center; + align-items: center; + min-height: 60vh; +} + +.upload-box { + background: #1a2a3a; + border: 2px dashed #2196F3; + border-radius: 16px; + padding: 60px 80px; + text-align: center; + cursor: pointer; + transition: all 0.3s; +} + +.upload-box:hover { + border-color: #64b5f6; + background: #1e3348; +} + +.upload-icon { + font-size: 64px; + margin-bottom: 16px; +} + +.upload-box h2 { + font-size: 24px; + color: #fff; + margin-bottom: 8px; +} + +.upload-box p { + color: #90a4ae; + margin-bottom: 20px; +} + +.upload-box button { + background: #2196F3; + color: #fff; + border: none; + padding: 14px 32px; + border-radius: 8px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; +} + +.upload-box button:hover { + background: #1976D2; +} + +.hint { + font-size: 12px !important; + color: #607d8b !important; + margin-top: 16px !important; +} + +.dashboard.hidden { + display: none; +} + +.stats-row { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 20px; + margin-bottom: 32px; +} + +.stat-card { + background: #1a2a3a; + border-radius: 12px; + padding: 24px; + text-align: center; + border: 1px solid #263545; +} + +.stat-value { + font-size: 36px; + font-weight: 700; + color: #2196F3; +} + +.stat-label { + font-size: 13px; + color: #90a4ae; + margin-top: 6px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.charts-row { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20px; + margin-bottom: 20px; +} + +.chart-card { + background: #1a2a3a; + border-radius: 12px; + padding: 24px; + border: 1px solid #263545; +} + +.chart-card h3 { + font-size: 16px; + color: #fff; + margin-bottom: 16px; +} + +.tables-row { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20px; + margin-bottom: 20px; +} + +.table-card { + background: #1a2a3a; + border-radius: 12px; + padding: 24px; + border: 1px solid #263545; +} + +.table-card.full-width { + margin-bottom: 20px; +} + +.table-card h3 { + font-size: 16px; + color: #fff; + margin-bottom: 16px; +} + +.table-wrapper { + overflow-x: auto; +} + +table { + width: 100%; + border-collapse: collapse; +} + +th { + text-align: left; + padding: 12px 16px; + background: #0f1923; + color: #90a4ae; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; + border-bottom: 1px solid #263545; +} + +td { + padding: 12px 16px; + border-bottom: 1px solid #1e2d3d; + font-size: 14px; +} + +tr:hover td { + background: #1e3348; +} + +.status-completed { + color: #4caf50; + font-weight: 600; +} + +.status-in-progress { + color: #ff9800; + font-weight: 600; +} + +.status-not-started { + color: #f44336; + font-weight: 600; +} + +@media (max-width: 900px) { + .stats-row { + grid-template-columns: repeat(2, 1fr); + } + .charts-row, + .tables-row { + grid-template-columns: 1fr; + } + main { + padding: 20px; + } +}