commit f55735a59eb0f5b033510b8ffd93d6ce2f0219bf Author: greyjoy Date: Sun Aug 2 22:54:13 2026 +0400 first commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..cba354f --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +SECRET_KEY=replace-with-a-long-random-string +ADMIN_PASSWORD=replace-with-a-private-admin-password diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a7a2373 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.venv/ +__pycache__/ +*.pyc +.env +consultations.db diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/anastasia_viktorovna.iml b/.idea/anastasia_viktorovna.iml new file mode 100644 index 0000000..174cdbe --- /dev/null +++ b/.idea/anastasia_viktorovna.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..8cdb88f --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5f177ba --- /dev/null +++ b/README.md @@ -0,0 +1,162 @@ +# Анастасия Панкратенкова + +> Персональный сайт преподавателя математики и физики с записью на консультацию по подготовке к ОГЭ и ЕГЭ. + +![Статус проекта](https://img.shields.io/badge/status-MVP-e77862?style=flat-square) +![Python](https://img.shields.io/badge/Python-3.10%2B-10211d?style=flat-square&logo=python&logoColor=white) +![Flask](https://img.shields.io/badge/Flask-3.x-10211d?style=flat-square&logo=flask&logoColor=white) +![SQLite](https://img.shields.io/badge/SQLite-local%20database-bdd3ba?style=flat-square&logo=sqlite&logoColor=10211d) + +## О проекте + +Сайт-визитка для Анастасии Викторовны Панкратенковой — преподавателя математики и физики. + +На сайте можно: + +- познакомиться с преподавателем, образованием и опытом; +- узнать о подготовке по математике и физике; +- понять, как проходят занятия; +- оставить заявку на консультацию; +- выбрать предмет, экзамен и формат занятий. + +Заявки сохраняются в локальную базу данных SQLite. Подключение уведомлений в Telegram или по электронной почте можно добавить следующим этапом. + +## Технологии + +| Технология | Назначение | +| --- | --- | +| Python 3.10+ | Основной язык приложения | +| Flask 3.x | Веб-сервер и маршрутизация | +| Jinja2 | Рендеринг HTML-шаблонов | +| SQLite | Хранение заявок на консультацию | +| HTML5 | Структура страниц | +| CSS3 | Адаптивный визуальный стиль | +| JavaScript | Небольшие интерактивные эффекты | + +## Структура проекта + +```text +anastasia_viktorovna/ +├── app.py # Flask-приложение и обработка заявок +├── requirements.txt # Зависимости проекта +├── .env.example # Пример переменных окружения +├── templates/ +│ ├── index.html # Главная страница +│ ├── success.html # Страница успешной отправки +│ ├── admin_login.html # Вход в админ-панель +│ └── admin.html # Список заявок +├── static/ +│ ├── css/styles.css # Стили и адаптивная вёрстка +│ ├── js/main.js # Клиентские эффекты +│ └── images/teacher.jpg # Фотография преподавателя +└── consultations.db # Создаётся автоматически при запуске +``` + +## Запуск в PyCharm + +### 1. Открыть проект + +Откройте в PyCharm папку проекта: + +```text +C:\Users\greyjoy\PycharmProjects\anastasia_viktorovna +``` + +### 2. Выбрать интерпретатор + +В PyCharm выберите интерпретатор виртуального окружения: + +```text +.venv\Scripts\python.exe +``` + +Если виртуальное окружение ещё не создано, выполните в терминале PyCharm: + +```powershell +py -3.10 -m venv .venv +.venv\Scripts\Activate.ps1 +``` + +### 3. Установить зависимости + +```powershell +python -m pip install -r requirements.txt +``` + +### 4. Запустить сайт + +```powershell +python app.py +``` + +После запуска откройте в браузере: + + + +Остановить сервер можно сочетанием `Ctrl+C` в терминале. + +## Как работает форма + +1. Посетитель заполняет имя, контакт, предмет, экзамен и формат занятий. +2. Flask проверяет обязательные поля. +3. Заявка записывается в таблицу `consultation_requests` файла `consultations.db`. +4. Пользователь видит страницу успешной отправки. + +При обновлении существующего проекта недостающие поля базы добавляются автоматически. Старые заявки получают статус «Новая» и значение времени «Гибко». + +В текущей версии заявки не отправляются автоматически преподавателю. Для рабочего сайта нужно подключить канал уведомлений и указать контактные данные Анастасии Викторовны. + +## Переменные окружения + +Для запуска в интернете рекомендуется заменить тестовый секретный ключ Flask: + +```powershell +$env:SECRET_KEY = "ваш-длинный-случайный-ключ" +python app.py +``` + +Файл `.env.example` содержит пример необходимой переменной. + +## Админ-панель + +Панель доступна по адресу: + + + +По умолчанию для локальной разработки используется пароль `admin123`. Перед публикацией сайта обязательно задайте собственный пароль: + +```powershell +$env:ADMIN_PASSWORD = "ваш-приватный-пароль" +python app.py +``` + +После входа в панели отображаются: + +- имя ученика или родителя; +- контакт для обратной связи; +- выбранный предмет; +- экзамен — ОГЭ или ЕГЭ; +- формат занятий; +- желаемое время занятий; +- комментарий из формы; +- дата и номер заявки. + +Также в панели доступны: + +- сводная статистика по всем заявкам; +- поиск по имени, контакту, комментарию и внутренней заметке; +- фильтрация по статусу и предмету; +- четыре статуса: «Новая», «Связались», «Запланирована», «Завершена»; +- внутренние заметки преподавателя по каждой заявке; +- удаление заявки с подтверждением; +- экспорт текущего списка в CSV для Excel или Google Таблиц. + +Ссылка на админ-панель не показывается посетителям сайта. Доступ защищён сессионной авторизацией Flask. + +## Дальнейшее развитие + +- уведомления о новых заявках в Telegram; +- закрытая страница для просмотра заявок; +- реальные контакты и расписание; +- отзывы учеников и результаты подготовки; +- публикация сайта на хостинге с Python. diff --git a/app.py b/app.py new file mode 100644 index 0000000..cc7f0ee --- /dev/null +++ b/app.py @@ -0,0 +1,352 @@ +import csv +import io +import os +import sqlite3 +from datetime import datetime +from functools import wraps +from hmac import compare_digest +from pathlib import Path + +from flask import Flask, flash, make_response, redirect, render_template, request, session, url_for + + +BASE_DIR = Path(__file__).resolve().parent +DATABASE_PATH = BASE_DIR / "consultations.db" +SUBJECTS = {"math": "Математика", "physics": "Физика"} +EXAMS = {"oge": "ОГЭ", "ege": "ЕГЭ", "other": "Пока не определились"} +FORMATS = {"online": "Онлайн", "offline": "Очно", "other": "Пока не определились"} +PREFERRED_TIMES = { + "flexible": "Гибко", + "morning": "Утро", + "day": "День", + "evening": "Вечер", +} +REQUEST_STATUSES = { + "new": "Новая", + "contacted": "Связались", + "scheduled": "Запланирована", + "closed": "Завершена", +} + +app = Flask(__name__) +app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "change-this-secret-key") +app.config["SESSION_COOKIE_HTTPONLY"] = True +app.config["SESSION_COOKIE_SAMESITE"] = "Lax" + + +def get_connection(): + connection = sqlite3.connect(DATABASE_PATH) + connection.row_factory = sqlite3.Row + return connection + + +def init_database(): + with get_connection() as connection: + connection.execute( + """ + CREATE TABLE IF NOT EXISTS consultation_requests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + contact TEXT NOT NULL, + subject TEXT NOT NULL, + exam TEXT NOT NULL, + format TEXT NOT NULL, + preferred_time TEXT NOT NULL DEFAULT 'flexible', + message TEXT, + status TEXT NOT NULL DEFAULT 'new', + admin_note TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL, + updated_at TEXT + ) + """ + ) + existing_columns = { + row["name"] + for row in connection.execute("PRAGMA table_info(consultation_requests)").fetchall() + } + migrations = { + "preferred_time": "TEXT NOT NULL DEFAULT 'flexible'", + "status": "TEXT NOT NULL DEFAULT 'new'", + "admin_note": "TEXT NOT NULL DEFAULT ''", + "updated_at": "TEXT", + } + for column, definition in migrations.items(): + if column not in existing_columns: + connection.execute( + f"ALTER TABLE consultation_requests ADD COLUMN {column} {definition}" + ) + + +@app.context_processor +def inject_site_data(): + return {"current_year": datetime.now().year} + + +@app.get("/") +def index(): + return render_template("index.html") + + +@app.post("/submit") +def submit_request(): + name = request.form.get("name", "").strip() + contact = request.form.get("contact", "").strip() + subject_key = request.form.get("subject", "").strip() + exam_key = request.form.get("exam", "").strip() + format_key = request.form.get("format", "").strip() + preferred_time_key = request.form.get("preferred_time", "").strip() + message = request.form.get("message", "").strip()[:2000] + + if ( + not name + or not contact + or subject_key not in SUBJECTS + or exam_key not in EXAMS + or format_key not in FORMATS + or preferred_time_key not in PREFERRED_TIMES + ): + flash("Пожалуйста, заполните обязательные поля формы.", "error") + return redirect(url_for("index") + "#contact") + + created_at = datetime.now().isoformat(timespec="minutes") + with get_connection() as connection: + connection.execute( + """ + INSERT INTO consultation_requests + (name, contact, subject, exam, format, preferred_time, message, status, admin_note, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + name[:120], + contact[:160], + SUBJECTS[subject_key], + EXAMS[exam_key], + FORMATS[format_key], + PREFERRED_TIMES[preferred_time_key], + message, + "new", + "", + created_at, + created_at, + ), + ) + + return redirect(url_for("success", name=name)) + + +@app.get("/success") +def success(): + return render_template("success.html", name=request.args.get("name", "")) + + +def admin_required(view): + @wraps(view) + def wrapped_view(*args, **kwargs): + if not session.get("is_admin"): + return redirect(url_for("admin_login")) + return view(*args, **kwargs) + + return wrapped_view + + +def get_request_filters(): + status = request.args.get("status", "").strip() + subject = request.args.get("subject", "").strip() + search = request.args.get("q", "").strip() + + return { + "status": status if status in REQUEST_STATUSES else "", + "subject": subject if subject in SUBJECTS.values() else "", + "q": search[:100], + } + + +def build_request_query(filters): + conditions = [] + parameters = [] + + if filters["status"]: + conditions.append("status = ?") + parameters.append(filters["status"]) + if filters["subject"]: + conditions.append("subject = ?") + parameters.append(filters["subject"]) + if filters["q"]: + conditions.append("(name LIKE ? OR contact LIKE ? OR message LIKE ? OR admin_note LIKE ?)") + search_pattern = f"%{filters['q']}%" + parameters.extend([search_pattern] * 4) + + where_clause = f"WHERE {' AND '.join(conditions)}" if conditions else "" + return where_clause, parameters + + +def fetch_requests(connection, filters): + where_clause, parameters = build_request_query(filters) + return connection.execute( + f""" + SELECT id, name, contact, subject, exam, format, preferred_time, + message, status, admin_note, created_at, updated_at + FROM consultation_requests + {where_clause} + ORDER BY id DESC + """, + parameters, + ).fetchall() + + +def fetch_stats(connection): + stats = connection.execute( + """ + SELECT + COUNT(*) AS total, + COALESCE(SUM(CASE WHEN status = 'new' THEN 1 ELSE 0 END), 0) AS new, + COALESCE(SUM(CASE WHEN status = 'contacted' THEN 1 ELSE 0 END), 0) AS contacted, + COALESCE(SUM(CASE WHEN status = 'scheduled' THEN 1 ELSE 0 END), 0) AS scheduled, + COALESCE(SUM(CASE WHEN status = 'closed' THEN 1 ELSE 0 END), 0) AS closed + FROM consultation_requests + """ + ).fetchone() + return dict(stats) + + +@app.route("/admin/login", methods=["GET", "POST"]) +def admin_login(): + if session.get("is_admin"): + return redirect(url_for("admin_dashboard")) + + if request.method == "POST": + password = request.form.get("password", "") + admin_password = os.getenv("ADMIN_PASSWORD", "admin123") + + if compare_digest(password, admin_password): + session.clear() + session["is_admin"] = True + return redirect(url_for("admin_dashboard")) + + flash("Неверный пароль.", "error") + + return render_template("admin_login.html") + + +@app.get("/admin") +@admin_required +def admin_dashboard(): + filters = get_request_filters() + with get_connection() as connection: + consultation_requests = fetch_requests(connection, filters) + stats = fetch_stats(connection) + + return render_template( + "admin.html", + consultation_requests=consultation_requests, + filters=filters, + stats=stats, + statuses=REQUEST_STATUSES, + subjects=SUBJECTS.values(), + ) + + +@app.post("/admin/request//status") +@admin_required +def admin_update_status(request_id): + status = request.form.get("status", "").strip() + if status not in REQUEST_STATUSES: + flash("Неизвестный статус заявки.", "error") + return redirect(url_for("admin_dashboard")) + + with get_connection() as connection: + connection.execute( + "UPDATE consultation_requests SET status = ?, updated_at = ? WHERE id = ?", + (status, datetime.now().isoformat(timespec="minutes"), request_id), + ) + + flash("Статус заявки обновлён.", "success") + return redirect(url_for("admin_dashboard")) + + +@app.post("/admin/request//note") +@admin_required +def admin_update_note(request_id): + admin_note = request.form.get("admin_note", "").strip()[:2000] + with get_connection() as connection: + connection.execute( + "UPDATE consultation_requests SET admin_note = ?, updated_at = ? WHERE id = ?", + (admin_note, datetime.now().isoformat(timespec="minutes"), request_id), + ) + + flash("Заметка сохранена.", "success") + return redirect(url_for("admin_dashboard")) + + +@app.post("/admin/request//delete") +@admin_required +def admin_delete_request(request_id): + with get_connection() as connection: + connection.execute("DELETE FROM consultation_requests WHERE id = ?", (request_id,)) + + flash("Заявка удалена.", "success") + return redirect(url_for("admin_dashboard")) + + +@app.get("/admin/export.csv") +@admin_required +def admin_export(): + filters = get_request_filters() + with get_connection() as connection: + consultation_requests = fetch_requests(connection, filters) + + output = io.StringIO() + writer = csv.writer(output) + writer.writerow( + [ + "Номер", + "Имя", + "Контакт", + "Предмет", + "Экзамен", + "Формат", + "Желаемое время", + "Статус", + "Комментарий", + "Заметка преподавателя", + "Создана", + "Обновлена", + ] + ) + for consultation in consultation_requests: + writer.writerow( + [ + consultation["id"], + consultation["name"], + consultation["contact"], + consultation["subject"], + consultation["exam"], + consultation["format"], + consultation["preferred_time"], + REQUEST_STATUSES.get(consultation["status"], consultation["status"]), + consultation["message"] or "", + consultation["admin_note"] or "", + consultation["created_at"], + consultation["updated_at"] or "", + ] + ) + + response = make_response("\ufeff" + output.getvalue()) + response.headers["Content-Type"] = "text/csv; charset=utf-8" + response.headers["Content-Disposition"] = ( + f"attachment; filename=consultations-{datetime.now().strftime('%Y-%m-%d')}.csv" + ) + return response + + +@app.get("/admin/logout") +def admin_logout(): + session.clear() + return redirect(url_for("admin_login")) + + +init_database() + + +if __name__ == "__main__": + app.run(debug=True) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..840d434 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +Flask>=3.0,<4.0 diff --git a/static/css/styles.css b/static/css/styles.css new file mode 100644 index 0000000..bb203f8 --- /dev/null +++ b/static/css/styles.css @@ -0,0 +1,1978 @@ +@import url("https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Manrope:wght@400;500;600;700;800&family=Playfair+Display:ital,wght@0,500;0,600;1,500;1,600&display=swap"); + +:root { + --ink: #10211d; + --muted: #697773; + --cream: #f5f3ed; + --paper: #fbfaf6; + --line: #dfe4dc; + --sage: #d9e8d8; + --sage-deep: #bdd3ba; + --mint: #cfe8dd; + --coral: #e77862; + --coral-soft: #f3aa91; + --white: #ffffff; + --serif: "Playfair Display", Georgia, serif; + --sans: "Manrope", "Segoe UI", sans-serif; + --mono: "DM Mono", Consolas, monospace; +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + color: var(--ink); + background: var(--cream); + font-family: var(--sans); + -webkit-font-smoothing: antialiased; +} + +a { + color: inherit; + text-decoration: none; +} + +button, +input, +select, +textarea { + font: inherit; +} + +.page-shell { + overflow: hidden; +} + +.section-wrap { + width: min(1160px, calc(100% - 64px)); + margin: 0 auto; +} + +.site-header { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 10; + display: flex; + align-items: center; + justify-content: space-between; + width: min(1240px, calc(100% - 48px)); + margin: 18px auto 0; + padding: 14px 18px 14px 10px; + border: 1px solid transparent; + transition: background 180ms ease, border-color 180ms ease, box-shadow 180ms ease; +} + +.site-header.is-scrolled { + background: rgba(251, 250, 246, 0.9); + border-color: var(--line); + box-shadow: 0 10px 28px rgba(16, 33, 29, 0.06); + backdrop-filter: blur(14px); +} + +.brand { + display: inline-flex; + align-items: center; + gap: 10px; +} + +.brand-mark { + display: grid; + width: 36px; + height: 36px; + place-items: center; + color: var(--white); + background: var(--ink); + border-radius: 50%; + font: 500 12px/1 var(--mono); + letter-spacing: -0.1em; +} + +.brand-name { + font-size: 11px; + font-weight: 800; + line-height: 1.15; + letter-spacing: -0.03em; + text-transform: uppercase; +} + +.main-nav { + display: flex; + gap: 32px; + margin-left: 60px; + color: var(--muted); + font-size: 12px; + font-weight: 700; +} + +.main-nav a, +.header-link, +.text-link, +.card-link { + transition: color 160ms ease; +} + +.main-nav a:hover, +.header-link:hover, +.text-link:hover, +.card-link:hover { + color: var(--coral); +} + +.header-link { + font-size: 12px; + font-weight: 800; +} + +.header-link span, +.text-link span, +.button span, +.card-link span { + display: inline-block; + margin-left: 6px; + font-size: 16px; + transition: transform 160ms ease; +} + +.header-link:hover span, +.text-link:hover span, +.button:hover span, +.card-link:hover span { + transform: translate(2px, -2px); +} + +.hero { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(360px, 0.84fr); + gap: 60px; + align-items: center; + min-height: 760px; + padding-top: 132px; + padding-bottom: 76px; +} + +.hero-copy { + position: relative; + z-index: 1; +} + +.eyebrow, +.section-label, +.card-kicker, +.optional, +.form-note, +.education-years, +.red-diploma { + font-family: var(--mono); + text-transform: uppercase; +} + +.eyebrow { + display: flex; + align-items: center; + gap: 10px; + margin: 0 0 28px; + color: var(--coral); + font-size: 10px; + font-weight: 500; + letter-spacing: 0.12em; +} + +.eyebrow-dot { + width: 7px; + height: 7px; + background: var(--coral); + border-radius: 50%; +} + +h1, +h2, +h3, +p { + margin-top: 0; +} + +h1, +h2 { + margin-bottom: 0; + font-weight: 600; + letter-spacing: -0.065em; +} + +h1, +h2, +h3 { + text-wrap: balance; +} + +h1 { + max-width: 690px; + font-size: clamp(52px, 6.3vw, 88px); + line-height: 0.99; +} + +h2 { + font-size: clamp(42px, 5vw, 68px); + line-height: 1.02; +} + +h1 em, +h2 em { + color: var(--coral); + font-family: var(--serif); + font-weight: 500; + letter-spacing: -0.06em; +} + +.hero-lead { + max-width: 480px; + margin: 30px 0 34px; + color: var(--muted); + font-size: 16px; + line-height: 1.75; +} + +.hero-actions { + display: flex; + align-items: center; + gap: 28px; +} + +.button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 52px; + padding: 0 21px; + border: 0; + cursor: pointer; + font-size: 11px; + font-weight: 800; + letter-spacing: -0.01em; + transition: background 160ms ease, color 160ms ease, transform 160ms ease; +} + +.button:hover { + transform: translateY(-2px); +} + +.button-primary { + color: var(--white); + background: var(--ink); +} + +.button-primary:hover { + background: var(--coral); +} + +.text-link { + color: var(--ink); + font-size: 12px; + font-weight: 800; +} + +.hero-proof { + display: flex; + align-items: center; + gap: 14px; + margin-top: 70px; +} + +.proof-avatars { + display: flex; +} + +.proof-avatars span { + display: grid; + width: 30px; + height: 30px; + margin-left: -7px; + place-items: center; + border: 2px solid var(--cream); + border-radius: 50%; + color: var(--ink); + background: var(--mint); + font: 500 10px var(--mono); +} + +.proof-avatars span:first-child { + margin-left: 0; + background: var(--coral-soft); +} + +.proof-avatars span:nth-child(2) { + background: var(--sage-deep); +} + +.hero-proof p { + margin: 0; + color: var(--muted); + font-size: 11px; + line-height: 1.4; +} + +.hero-proof strong { + color: var(--ink); + font-weight: 800; +} + +.hero-visual { + position: relative; + min-height: 570px; +} + +.hero-photo-frame { + position: absolute; + top: 32px; + right: 12%; + width: min(360px, 78%); + height: 505px; + overflow: hidden; + border-radius: 190px 190px 12px 12px; + box-shadow: 20px 28px 0 rgba(189, 211, 186, 0.6); + transform: rotate(3deg); +} + +.hero-photo-frame::after { + position: absolute; + inset: 0; + background: linear-gradient(180deg, rgba(16, 33, 29, 0.02), rgba(16, 33, 29, 0.16)); + content: ""; +} + +.hero-photo-frame img { + width: 100%; + height: 100%; + object-fit: cover; + object-position: center 18%; +} + +.hero-orbit { + position: absolute; + border: 1px solid var(--coral-soft); + border-radius: 50%; + transform: rotate(-35deg); +} + +.orbit-one { + top: 12px; + right: 0; + width: 470px; + height: 580px; +} + +.orbit-two { + top: 88px; + right: 3%; + width: 395px; + height: 505px; + border-color: var(--sage-deep); +} + +.floating-note { + position: absolute; + z-index: 2; + display: flex; + align-items: center; + gap: 10px; + padding: 13px 15px; + color: var(--ink); + background: var(--white); + box-shadow: 0 15px 32px rgba(16, 33, 29, 0.12); + font-size: 10px; + line-height: 1.25; +} + +.note-top { + top: 89px; + left: 2%; + transform: rotate(-5deg); +} + +.note-top span { + display: grid; + width: 28px; + height: 28px; + place-items: center; + color: var(--white); + background: var(--coral); + border-radius: 50%; + font: 16px var(--serif); +} + +.note-bottom { + right: 0; + bottom: 48px; + background: var(--ink); + color: var(--white); + transform: rotate(5deg); +} + +.note-bottom strong { + color: var(--coral-soft); + font: 34px/1 var(--serif); +} + +.math-scribble { + position: absolute; + color: var(--coral); + font: italic 15px var(--serif); +} + +.scribble-one { + top: 12px; + left: 22%; + transform: rotate(-15deg); +} + +.scribble-two { + right: 1%; + bottom: 4px; + transform: rotate(-9deg); +} + +.ticker { + overflow: hidden; + color: var(--white); + background: var(--ink); +} + +.ticker-track { + display: flex; + justify-content: space-around; + width: 1120px; + max-width: none; + margin: 0 auto; + padding: 18px 0; + font: 500 11px var(--mono); + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.ticker-track i { + color: var(--coral-soft); + font-style: normal; +} + +.section-grid, +.subjects, +.process, +.contact { + display: grid; + grid-template-columns: 150px minmax(0, 1fr); + gap: 48px; + padding-top: 140px; +} + +.section-label { + display: flex; + gap: 10px; + align-self: start; + padding-top: 12px; + color: var(--muted); + font-size: 9px; + font-weight: 500; + letter-spacing: 0.1em; +} + +.section-label span:first-child { + color: var(--coral); +} + +.section-heading-row { + display: flex; + justify-content: space-between; + gap: 20px; +} + +.round-arrow { + display: grid; + width: 56px; + height: 56px; + flex: 0 0 auto; + place-items: center; + border: 1px solid var(--line); + border-radius: 50%; + color: var(--coral); + font-size: 24px; +} + +.section-intro { + max-width: 600px; + margin: 34px 0 58px; + color: var(--muted); + font-size: 16px; + line-height: 1.75; +} + +.about-details { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 25px; + border-top: 1px solid var(--line); + padding-top: 18px; +} + +.detail-item { + display: flex; + gap: 13px; +} + +.detail-number, +.card-number, +.process-item > span { + color: var(--coral); + font: 10px var(--mono); +} + +.detail-item p { + margin: 0; + color: var(--muted); + font-size: 11px; + line-height: 1.6; +} + +.detail-item strong { + display: block; + margin-bottom: 5px; + color: var(--ink); + font-size: 12px; +} + +.subjects { + padding-bottom: 10px; +} + +.subjects-heading { + display: flex; + align-items: end; + justify-content: space-between; + gap: 40px; +} + +.subjects-heading p { + max-width: 255px; + margin: 0 0 7px; + color: var(--muted); + font-size: 12px; + line-height: 1.65; +} + +.subject-cards { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 18px; + margin-top: 48px; +} + +.subject-card { + display: flex; + min-height: 390px; + flex-direction: column; + justify-content: space-between; + padding: 26px 28px 24px; +} + +.subject-card-dark { + color: var(--white); + background: var(--ink); +} + +.subject-card-accent { + color: var(--ink); + background: var(--sage); +} + +.card-top { + display: flex; + align-items: start; + justify-content: space-between; +} + +.card-icon { + display: grid; + width: 45px; + height: 45px; + place-items: center; + border: 1px solid currentColor; + border-radius: 50%; + font: 20px var(--serif); +} + +.subject-card .card-number { + color: var(--coral-soft); +} + +.subject-card-accent .card-number { + color: var(--coral); +} + +.card-kicker { + margin-bottom: 10px; + color: var(--coral-soft); + font-size: 9px; + letter-spacing: 0.11em; +} + +.subject-card-accent .card-kicker { + color: var(--coral); +} + +.subject-card h3 { + margin-bottom: 16px; + font: 500 clamp(36px, 4vw, 52px)/1 var(--serif); + letter-spacing: -0.05em; +} + +.card-description { + max-width: 340px; + margin-bottom: 0; + color: rgba(255, 255, 255, 0.63); + font-size: 12px; + line-height: 1.7; +} + +.subject-card-accent .card-description { + color: rgba(16, 33, 29, 0.65); +} + +.card-link { + font-size: 11px; + font-weight: 800; +} + +.process { + padding-bottom: 12px; +} + +.process-content { + display: grid; + grid-template-columns: minmax(270px, 0.75fr) minmax(0, 1.25fr); + gap: 74px; +} + +.process-list { + border-top: 1px solid var(--line); +} + +.process-item { + display: grid; + grid-template-columns: 34px 1fr 20px; + gap: 17px; + align-items: start; + padding: 22px 0; + border-bottom: 1px solid var(--line); +} + +.process-item h3 { + margin: -4px 0 8px; + font-size: 15px; + letter-spacing: -0.04em; +} + +.process-item p { + max-width: 360px; + margin: 0; + color: var(--muted); + font-size: 11px; + line-height: 1.6; +} + +.process-item b { + color: var(--coral); + font-size: 18px; + font-weight: 400; +} + +.education { + padding-top: 140px; +} + +.education-card { + position: relative; + display: grid; + grid-template-columns: 0.7fr 1fr 0.8fr; + gap: 30px; + min-height: 310px; + padding: 40px 46px; + overflow: hidden; + color: var(--ink); + background: var(--mint); +} + +.education-mark { + position: absolute; + top: -30px; + right: 20px; + color: rgba(255, 255, 255, 0.55); + font-size: 220px; + line-height: 1; +} + +.education-card h2 { + position: relative; + z-index: 1; + margin-top: 15px; + font-size: clamp(38px, 4.3vw, 60px); +} + +.education-card .card-kicker { + position: relative; + z-index: 1; + margin: 0; + color: var(--coral); +} + +.education-years { + display: flex; + align-items: center; + align-self: end; + gap: 10px; + padding-bottom: 6px; + font-size: 11px; +} + +.education-years i { + display: block; + width: 75px; + height: 1px; + background: var(--ink); +} + +.education-copy { + align-self: end; + padding-bottom: 3px; + font-size: 11px; + line-height: 1.6; +} + +.education-copy p { + margin: 0 0 12px; +} + +.red-diploma { + position: absolute; + right: 46px; + bottom: 34px; + z-index: 1; + color: var(--coral); + font-size: 9px; + letter-spacing: 0.09em; +} + +.red-diploma span { + display: inline-grid; + width: 24px; + height: 24px; + margin-left: 8px; + place-items: center; + border: 1px solid var(--coral); + border-radius: 50%; + font-size: 8px; +} + +.contact { + padding-top: 140px; + padding-bottom: 140px; +} + +.contact-intro h2 { + margin-top: 65px; +} + +.contact-intro > p { + max-width: 280px; + margin: 28px 0 70px; + color: var(--muted); + font-size: 13px; + line-height: 1.7; +} + +.contact-sign { + color: var(--coral); + font: italic 24px var(--serif); + transform: rotate(-5deg); +} + +.contact-sign span { + display: block; + margin-left: 48px; +} + +.form-card { + padding: 37px 42px 34px; + background: var(--white); + box-shadow: 10px 16px 0 var(--sage-deep); +} + +.form-row { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20px; +} + +label, +legend { + color: var(--ink); + font-size: 11px; + font-weight: 800; +} + +form > label, +.form-row, +.format-field { + margin-bottom: 22px; +} + +label > span, +legend span { + color: var(--coral); +} + +.optional { + margin-left: 6px; + color: var(--muted); + font-size: 8px; + font-weight: 400; +} + +input, +select, +textarea { + display: block; + width: 100%; + margin-top: 9px; + padding: 13px 0; + border: 0; + border-bottom: 1px solid var(--line); + outline: 0; + color: var(--ink); + background: transparent; + font-size: 12px; + transition: border-color 160ms ease; +} + +input:focus, +select:focus, +textarea:focus { + border-color: var(--coral); +} + +input::placeholder, +textarea::placeholder { + color: #a6afaa; +} + +select { + cursor: pointer; +} + +textarea { + min-height: 78px; + resize: vertical; +} + +.format-field { + padding: 0; + border: 0; +} + +.format-field legend { + margin-bottom: 12px; + padding: 0; +} + +.radio-options { + display: flex; + gap: 9px; +} + +.radio-options label { + position: relative; + flex: 1; + cursor: pointer; + font-size: 10px; + font-weight: 600; +} + +.radio-options input { + position: absolute; + width: 1px; + height: 1px; + opacity: 0; +} + +.radio-options label span { + display: block; + padding: 11px 8px; + border: 1px solid var(--line); + color: var(--muted); + text-align: center; + transition: background 160ms ease, border-color 160ms ease, color 160ms ease; +} + +.radio-options input:checked + span { + border-color: var(--ink); + color: var(--white); + background: var(--ink); +} + +.form-button { + width: 100%; + margin-top: 5px; +} + +.form-note { + margin: 14px 0 0; + color: #a6afaa; + font-size: 8px; + line-height: 1.5; + text-transform: none; +} + +.flash-message { + margin-bottom: 20px; + padding: 12px 14px; + font-size: 11px; +} + +.flash-message.error { + color: #a14c3b; + background: #fce8e2; +} + +.site-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 22px; + padding-bottom: 30px; + border-top: 1px solid var(--line); + color: var(--muted); + font-size: 10px; +} + +.site-footer p { + margin: 0; + line-height: 1.5; + text-align: center; +} + +.success-page { + display: grid; + min-height: 100vh; + padding: 24px; + place-items: center; + background: var(--cream); +} + +.success-card { + width: min(510px, 100%); + padding: 58px 52px; + text-align: center; + background: var(--white); + box-shadow: 12px 14px 0 var(--sage-deep); +} + +.success-symbol { + display: grid; + width: 62px; + height: 62px; + margin: 0 auto 30px; + place-items: center; + color: var(--white); + background: var(--coral); + border-radius: 50%; + font-size: 28px; +} + +.success-card .eyebrow { + justify-content: center; +} + +.success-card h1 { + margin-top: 20px; + font-size: 50px; +} + +.success-card > p:not(.eyebrow) { + margin: 24px 0 30px; + color: var(--muted); + font-size: 13px; + line-height: 1.7; +} + +.admin-page { + min-height: 100vh; + background: var(--cream); +} + +.admin-login-card { + width: min(510px, calc(100% - 36px)); + margin: 0 auto; + padding: 34px 46px 38px; + background: var(--white); + box-shadow: 12px 14px 0 var(--sage-deep); +} + +.admin-brand { + margin-bottom: 87px; +} + +.admin-login-heading h1 { + margin-top: 22px; + font-size: clamp(45px, 6vw, 67px); +} + +.admin-login-heading > p:not(.eyebrow) { + max-width: 310px; + margin: 24px 0 30px; + color: var(--muted); + font-size: 12px; + line-height: 1.7; +} + +.admin-login-form label { + display: block; + margin-bottom: 9px; +} + +.admin-login-form input { + margin-top: 0; + margin-bottom: 24px; +} + +.admin-back-link { + display: block; + margin-top: 24px; + color: var(--muted); + font-size: 11px; + font-weight: 700; + text-align: center; +} + +.admin-back-link:hover, +.admin-logout:hover { + color: var(--coral); +} + +.admin-dashboard-page { + background: var(--ink); +} + +.admin-shell { + min-height: 100vh; + background: var(--cream); +} + +.admin-header { + display: flex; + align-items: center; + justify-content: space-between; + width: min(1160px, calc(100% - 64px)); + margin: 0 auto; + padding: 28px 0; + border-bottom: 1px solid var(--line); +} + +.admin-header-actions { + display: flex; + align-items: center; + gap: 25px; +} + +.admin-status { + color: var(--muted); + font: 9px var(--mono); + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.admin-status i { + display: inline-block; + width: 6px; + height: 6px; + margin-right: 6px; + background: #71af71; + border-radius: 50%; +} + +.admin-logout { + color: var(--ink); + font-size: 11px; + font-weight: 800; +} + +.admin-main { + width: min(1160px, calc(100% - 64px)); + margin: 0 auto; + padding: 96px 0 110px; +} + +.admin-title-row { + display: flex; + align-items: end; + justify-content: space-between; + gap: 20px; + margin-bottom: 54px; +} + +.admin-title-row h1 { + margin-top: 20px; + font-size: clamp(52px, 7vw, 86px); +} + +.requests-count { + display: flex; + align-items: center; + gap: 12px; + padding-bottom: 10px; + color: var(--muted); + font: 9px/1.45 var(--mono); + text-transform: uppercase; +} + +.requests-count strong { + color: var(--coral); + font: 54px/1 var(--serif); +} + +.requests-list { + display: grid; + gap: 14px; +} + +.request-card { + padding: 25px 30px 23px; + background: var(--white); + border-left: 4px solid var(--coral-soft); +} + +.request-card-head, +.request-card-main { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; +} + +.request-card-head { + padding-bottom: 18px; + border-bottom: 1px solid var(--line); + color: var(--muted); + font: 9px var(--mono); + text-transform: uppercase; +} + +.request-id { + color: var(--coral); +} + +.request-card-main { + align-items: start; + padding: 22px 0 7px; +} + +.request-person h2 { + margin: 0 0 6px; + font-size: 22px; + letter-spacing: -0.05em; +} + +.request-person a, +.request-person span { + color: var(--coral); + font-size: 12px; + font-weight: 700; +} + +.request-meta { + display: flex; + gap: 28px; + margin: 0; +} + +.request-meta div { + min-width: 90px; +} + +.request-meta dt, +.request-message span { + margin-bottom: 6px; + color: var(--muted); + font: 8px var(--mono); + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.request-meta dd { + margin: 0; + font-size: 12px; + font-weight: 800; +} + +.request-message { + display: flex; + gap: 18px; + margin: 12px 0 0; + padding: 14px 0 0; + border-top: 1px solid var(--line); + color: var(--muted); + font-size: 12px; + line-height: 1.55; +} + +.request-message span { + flex: 0 0 74px; + margin-top: 3px; +} + +.empty-requests { + padding: 75px 30px; + text-align: center; + background: var(--white); +} + +.empty-requests > span { + color: var(--coral); + font-size: 30px; +} + +.empty-requests h2 { + margin: 18px 0 12px; + font-size: 42px; +} + +.empty-requests p { + margin-bottom: 27px; + color: var(--muted); + font-size: 12px; +} + +@media (max-width: 620px) { + .admin-login-card { + padding: 27px 23px 30px; + } + + .admin-brand { + margin-bottom: 64px; + } + + .admin-header, + .admin-main { + width: calc(100% - 36px); + } + + .admin-header { + align-items: flex-start; + gap: 20px; + } + + .admin-header-actions { + display: block; + text-align: right; + } + + .admin-status { + display: none; + } + + .admin-main { + padding-top: 70px; + padding-bottom: 70px; + } + + .admin-title-row { + align-items: start; + flex-direction: column; + margin-bottom: 35px; + } + + .requests-count { + padding-bottom: 0; + } + + .request-card { + padding: 20px 18px 19px; + } + + .request-card-head, + .request-card-main { + align-items: flex-start; + flex-direction: column; + gap: 10px; + } + + .request-card-main { + padding-top: 18px; + } + + .request-meta { + flex-wrap: wrap; + gap: 15px 25px; + } + + .request-message { + display: block; + } + + .request-message span { + display: block; + } +} + +@media (max-width: 880px) { + .main-nav { + gap: 16px; + margin-left: 0; + } + + .hero { + grid-template-columns: 1fr; + gap: 30px; + min-height: auto; + padding-top: 150px; + } + + .hero-visual { + width: min(600px, 100%); + margin: 0 auto; + } + + .section-grid, + .subjects, + .process, + .contact { + grid-template-columns: 1fr; + gap: 25px; + padding-top: 100px; + } + + .section-label { + padding-top: 0; + } + + .process-content { + gap: 45px; + } + + .education { + padding-top: 100px; + } + + .contact { + padding-bottom: 100px; + } + + .contact-intro h2 { + margin-top: 0; + } + + .contact-intro > p { + margin-bottom: 28px; + } +} + +@media (max-width: 620px) { + .section-wrap { + width: min(100% - 36px, 500px); + } + + .site-header { + width: calc(100% - 24px); + margin-top: 10px; + padding: 10px 10px 10px 7px; + } + + .main-nav, + .header-link { + display: none; + } + + .hero { + padding-top: 120px; + padding-bottom: 50px; + } + + h1 { + font-size: clamp(46px, 14vw, 68px); + } + + .hero-lead { + margin-top: 24px; + font-size: 14px; + } + + .hero-actions { + align-items: flex-start; + flex-direction: column; + gap: 20px; + } + + .hero-proof { + margin-top: 44px; + } + + .hero-visual { + min-height: 450px; + } + + .hero-photo-frame { + right: 7%; + width: 72%; + height: 395px; + } + + .orbit-one { + right: -13%; + width: 380px; + height: 470px; + } + + .orbit-two { + right: -4%; + width: 330px; + height: 415px; + } + + .note-top { + top: 48px; + left: 0; + } + + .note-bottom { + right: -2px; + bottom: 34px; + } + + .math-scribble { + display: none; + } + + .ticker-track { + width: 850px; + justify-content: flex-start; + gap: 24px; + padding-left: 18px; + } + + h2 { + font-size: clamp(40px, 12vw, 58px); + } + + .section-grid, + .subjects, + .process, + .contact { + padding-top: 82px; + } + + .about-details, + .subject-cards, + .form-row { + grid-template-columns: 1fr; + } + + .about-details { + gap: 17px; + } + + .subjects-heading, + .process-content { + display: block; + } + + .subjects-heading p { + margin-top: 25px; + } + + .subject-cards { + margin-top: 32px; + } + + .subject-card { + min-height: 330px; + } + + .process-list { + margin-top: 38px; + } + + .education { + padding-top: 82px; + } + + .education-card { + display: block; + min-height: 490px; + padding: 32px 27px; + } + + .education-years { + margin-top: 55px; + } + + .education-copy { + margin-top: 25px; + } + + .red-diploma { + right: 27px; + bottom: 28px; + } + + .contact { + padding-bottom: 82px; + } + + .form-card { + padding: 27px 22px 25px; + box-shadow: 7px 9px 0 var(--sage-deep); + } + + .radio-options { + flex-wrap: wrap; + } + + .radio-options label { + min-width: 29%; + } + + .site-footer { + align-items: flex-start; + flex-wrap: wrap; + gap: 20px; + } + + .site-footer p { + order: 3; + width: 100%; + text-align: left; + } + + .success-card { + padding: 42px 25px; + } + + .success-card h1 { + font-size: 43px; + } +} + +.form-row-wide { + grid-template-columns: 1fr; +} + +.admin-flashes { + display: grid; + gap: 8px; + margin-bottom: 20px; +} + +.flash-message.success { + color: #2f7253; + background: #e4f3e8; +} + +.admin-stats { + display: grid; + grid-template-columns: 1.35fr repeat(4, 1fr); + gap: 10px; + margin-bottom: 22px; +} + +.stat-card { + display: flex; + min-height: 128px; + flex-direction: column; + justify-content: space-between; + padding: 18px 19px 17px; + background: var(--white); +} + +.stat-card span, +.stat-card small { + color: var(--muted); + font: 9px var(--mono); + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.stat-card strong { + color: var(--ink); + font: 44px/0.9 var(--serif); +} + +.stat-card small { + color: #a3ada7; + font-size: 8px; + text-transform: none; +} + +.stat-card-total { + color: var(--white); + background: var(--ink); +} + +.stat-card-total span, +.stat-card-total small, +.stat-card-total strong { + color: inherit; +} + +.stat-card-total strong { + color: var(--coral-soft); +} + +.admin-toolbar { + display: grid; + gap: 18px; + margin-bottom: 25px; + padding: 20px 22px; + background: var(--white); +} + +.admin-filters { + display: grid; + grid-template-columns: minmax(220px, 1.8fr) repeat(2, minmax(140px, 0.75fr)) auto; + gap: 12px; + align-items: end; +} + +.admin-filters label > span { + display: block; + color: var(--muted); + font: 8px var(--mono); + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.admin-filters input, +.admin-filters select { + margin-top: 7px; + padding: 11px 10px; + border: 1px solid var(--line); + background: var(--paper); + font-size: 11px; +} + +.admin-filters input:focus, +.admin-filters select:focus { + border-color: var(--coral); +} + +.filter-button { + min-height: 42px; + padding: 0 16px; +} + +.toolbar-links { + display: flex; + justify-content: space-between; + gap: 18px; + align-items: center; +} + +.reset-filters, +.export-link { + color: var(--muted); + font-size: 10px; + font-weight: 800; +} + +.reset-filters:hover, +.export-link:hover { + color: var(--coral); +} + +.export-link { + color: var(--ink); +} + +.export-link span { + margin-left: 5px; + color: var(--coral); + font-size: 16px; +} + +.results-heading { + display: flex; + justify-content: space-between; + gap: 18px; + margin: 0 0 12px; + color: var(--muted); + font: 9px var(--mono); + text-transform: uppercase; +} + +.results-heading strong { + color: var(--ink); +} + +.request-card { + border-left-width: 5px; +} + +.status-border-new { + border-left-color: var(--coral); +} + +.status-border-contacted { + border-left-color: #d9b55e; +} + +.status-border-scheduled { + border-left-color: #6c9db1; +} + +.status-border-closed { + border-left-color: #91a79a; +} + +.request-card-head-left { + display: flex; + align-items: center; + gap: 12px; +} + +.status-badge { + display: inline-flex; + align-items: center; + min-height: 23px; + padding: 0 9px; + border-radius: 20px; + font: 8px var(--mono); + letter-spacing: 0.03em; + text-transform: uppercase; +} + +.status-new { + color: #a34e3c; + background: #fbe2da; +} + +.status-contacted { + color: #806321; + background: #f5edcc; +} + +.status-scheduled { + color: #3f7184; + background: #deeff2; +} + +.status-closed { + color: #4f6c5b; + background: #e1eee5; +} + +.request-actions { + display: grid; + grid-template-columns: minmax(265px, 0.85fr) minmax(240px, 1.1fr) auto; + gap: 18px; + align-items: start; + margin-top: 18px; + padding-top: 17px; + border-top: 1px solid var(--line); +} + +.request-status-form { + display: grid; + grid-template-columns: auto minmax(120px, 1fr) auto; + gap: 8px; + align-items: center; +} + +.request-status-form label { + color: var(--muted); + font: 8px var(--mono); + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.request-status-form select { + min-width: 0; + padding: 9px 8px; + border: 1px solid var(--line); + color: var(--ink); + background: var(--paper); + font-size: 10px; +} + +.mini-button { + min-height: 35px; + padding: 0 10px; + border: 1px solid var(--ink); + cursor: pointer; + color: var(--white); + background: var(--ink); + font-size: 9px; + font-weight: 800; + transition: color 160ms ease, background 160ms ease; +} + +.mini-button:hover { + color: var(--ink); + background: var(--white); +} + +.request-note { + min-width: 0; +} + +.request-note summary { + display: flex; + min-height: 35px; + align-items: center; + justify-content: space-between; + padding: 0 10px; + border: 1px solid var(--line); + cursor: pointer; + color: var(--muted); + font-size: 10px; + font-weight: 800; + list-style: none; +} + +.request-note summary::-webkit-details-marker { + display: none; +} + +.request-note summary span { + color: var(--coral); + font-size: 17px; + font-weight: 400; +} + +.request-note form { + padding-top: 8px; +} + +.request-note textarea { + min-height: 58px; + margin: 0 0 7px; + padding: 8px; + border: 1px solid var(--line); + background: var(--paper); + font-size: 10px; +} + +.request-note .mini-button { + width: 100%; +} + +.request-delete-form { + align-self: start; + justify-self: end; +} + +.delete-button { + min-height: 35px; + padding: 0 10px; + border: 1px solid #efc8bf; + cursor: pointer; + color: #a96557; + background: transparent; + font-size: 9px; + font-weight: 800; + transition: color 160ms ease, background 160ms ease; +} + +.delete-button:hover { + color: var(--white); + background: var(--coral); +} + +@media (max-width: 980px) { + .admin-stats { + grid-template-columns: repeat(3, 1fr); + } + + .stat-card-total { + grid-column: span 3; + min-height: 105px; + } + + .admin-filters { + grid-template-columns: minmax(0, 1fr) repeat(2, 1fr); + } + + .filter-button { + grid-column: span 3; + } + + .request-actions { + grid-template-columns: 1fr 1fr; + } + + .request-delete-form { + justify-self: start; + } +} + +@media (max-width: 620px) { + .admin-stats { + grid-template-columns: repeat(2, 1fr); + } + + .stat-card-total { + grid-column: span 2; + } + + .admin-toolbar { + padding: 17px 15px; + } + + .admin-filters { + grid-template-columns: 1fr; + } + + .filter-button { + grid-column: auto; + } + + .toolbar-links { + align-items: flex-start; + flex-direction: column; + gap: 9px; + } + + .results-heading { + align-items: flex-start; + flex-direction: column; + gap: 7px; + } + + .request-card-head-left { + align-items: flex-start; + flex-direction: column; + gap: 7px; + } + + .request-actions { + grid-template-columns: 1fr; + } + + .request-status-form { + grid-template-columns: auto 1fr; + } + + .request-status-form .mini-button { + grid-column: span 2; + } + + .request-delete-form { + justify-self: stretch; + } + + .delete-button { + width: 100%; + } +} diff --git a/static/images/teacher.jpg b/static/images/teacher.jpg new file mode 100644 index 0000000..37f4560 Binary files /dev/null and b/static/images/teacher.jpg differ diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..b3cbf7a --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,20 @@ +document.addEventListener("DOMContentLoaded", () => { + const header = document.querySelector(".site-header"); + + if (header) { + const updateHeader = () => { + header.classList.toggle("is-scrolled", window.scrollY > 24); + }; + + updateHeader(); + window.addEventListener("scroll", updateHeader, { passive: true }); + } + + document.querySelectorAll("form[data-confirm]").forEach((form) => { + form.addEventListener("submit", (event) => { + if (!window.confirm(form.dataset.confirm)) { + event.preventDefault(); + } + }); + }); +}); diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..e9f005f --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,114 @@ + + + + + + + Заявки — Анастасия Панкратенкова + + + +
+
+ + АП + Анастасия
Панкратенкова
+
+
+ Вы вошли как администратор + Выйти +
+
+ +
+
+
+

Панель преподавателя

+

Ваши заявки.

+
+
{{ stats["total"] }}обращений
всего
+
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} + {% endwith %} + +
+
Всего{{ stats["total"] }}за всё время
+
Новые{{ stats["new"] }}ждут ответа
+
В работе{{ stats["contacted"] }}связались
+
Запланированы{{ stats["scheduled"] }}занятия
+
Завершены{{ stats["closed"] }}архив
+
+ +
+
+ + + + +
+ +
+ + {% if consultation_requests %} +
Показано заявок: {{ consultation_requests|length }}Новые сверху
+
+ {% for consultation in consultation_requests %} +
+
+
{{ statuses.get(consultation["status"], consultation["status"]) }}Заявка №{{ consultation["id"] }}
+ +
+
+

{{ consultation["name"] }}

{{ consultation["contact"] }}
+
+
Предмет
{{ consultation["subject"] }}
+
Экзамен
{{ consultation["exam"] }}
+
Формат
{{ consultation["format"] }}
+
Время
{{ consultation["preferred_time"] }}
+
+
+ {% if consultation["message"] %}

Комментарий{{ consultation["message"] }}

{% endif %} +
+
+ + + +
+
+ {% if consultation["admin_note"] %}Изменить заметку{% else %}Добавить заметку{% endif %} +
+ + +
+
+
+ +
+
+
+ {% endfor %} +
+ {% else %} +
+ +

{% if filters["q"] or filters["status"] or filters["subject"] %}Ничего не найдено.{% else %}Пока заявок нет.{% endif %}

+

{% if filters["q"] or filters["status"] or filters["subject"] %}Попробуйте изменить условия поиска или сбросить фильтры.{% else %}Новые обращения появятся здесь после заполнения формы на сайте.{% endif %}

+ {% if filters["q"] or filters["status"] or filters["subject"] %}Сбросить фильтры {% else %}Посмотреть форму {% endif %} +
+ {% endif %} +
+
+ + + diff --git a/templates/admin_login.html b/templates/admin_login.html new file mode 100644 index 0000000..c09de16 --- /dev/null +++ b/templates/admin_login.html @@ -0,0 +1,35 @@ + + + + + + Вход для преподавателя — Анастасия Панкратенкова + + + +
+ + АП + Анастасия
Панкратенкова
+
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} + + ← Вернуться на сайт +
+ + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..70c652d --- /dev/null +++ b/templates/index.html @@ -0,0 +1,163 @@ + + + + + + + Анастасия Панкратенкова — математика и физика + + + +
+ + +
+
+
+

Математика · физика · подготовка к экзаменам

+

Понимать, решать, быть уверенным.

+

Помогаю школьникам увидеть логику точных наук и спокойно прийти к нужному результату на ОГЭ и ЕГЭ.

+ +
+ +

Индивидуальный подход
с 2017 года

+
+
+
+
+
+
+ Анастасия Панкратенкова +
+
Разбираем
до ясности
+
2красных
диплома
+
y = kx + b
+
F = ma
+
+
+ +
+
+ МатематикаФизикаОГЭЕГЭМатематикаФизика +
+
+ +
+ +
+
+

Точные науки
могут быть понятными.

+ +
+

Я — Анастасия Викторовна, преподаватель математики и физики. На занятиях мы не заучиваем решения, а разбираемся, как всё устроено — шаг за шагом и в удобном для ученика темпе.

+
+
01

ОбразованиеАГУ, бакалавриат «Математика» и магистратура «Математическое образование».

+
02

РезультатОбе ступени образования окончены с красным дипломом.

+
03

ОпытОфициально веду педагогическую деятельность с 2017 года.

+
+
+
+ +
+ +
+

Выберите свою
точку старта.

+

Первая консультация помогает определить текущий уровень, цели и комфортный план подготовки.

+
+
+
+
01
+

Подготовка

Математика

От уверенной базы до сложных задач второй части. Разбираем теорию, практику и стратегию экзамена.

+ Обсудить подготовку +
+
+
02F
+

Подготовка

Физика

Учимся видеть физику за формулами: решаем задачи, выстраиваем связи и готовимся к нужному баллу.

+ Обсудить подготовку +
+
+
+ +
+ +
+

Спокойная подготовка
с понятным маршрутом.

+
+
01

Знакомимся с задачей

Определяем цель, уровень знаний и точки, которые требуют внимания.

+
02

Собираем систему

Выстраиваем понятную теорию и закрепляем её практикой на реальных заданиях.

+
03

Двигаемся к результату

Отслеживаем прогресс, разбираем ошибки и учимся уверенно работать на экзамене.

+
+
+
+ +
+
+
+

Образование

Два диплома.
Одна цель.

+
20132019
+

АГУ · бакалавриат
«Математика»

АГУ · магистратура
«Математическое образование»

+
Красный диплом × 2
+
+
+ +
+
+ +

Давайте
познакомимся.

+

Оставьте заявку — я свяжусь с вами, отвечу на вопросы и помогу выбрать формат подготовки.

+
Анастасия Панкратенкова
+
+
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} +
+
+ + +
+
+ + +
+
+ +
+
Формат занятий *
+ + +

Нажимая кнопку, вы соглашаетесь на обработку персональных данных.

+
+
+
+
+ + +
+ + + diff --git a/templates/success.html b/templates/success.html new file mode 100644 index 0000000..7607aa3 --- /dev/null +++ b/templates/success.html @@ -0,0 +1,18 @@ + + + + + + Заявка отправлена — Анастасия Панкратенкова + + + +
+ +

Заявка отправлена

+

Спасибо, {{ name or "что написали" }}!

+

Анастасия свяжется с вами в ближайшее время, чтобы обсудить цель, формат и удобное время занятий.

+ Вернуться на сайт +
+ +