diff --git a/public/fonts/Montserrat/Montserrat-Italic-VariableFont_wght.ttf b/public/fonts/Montserrat/Montserrat-Italic-VariableFont_wght.ttf new file mode 100644 index 0000000..76e3f9d Binary files /dev/null and b/public/fonts/Montserrat/Montserrat-Italic-VariableFont_wght.ttf differ diff --git a/public/fonts/Montserrat/Montserrat-VariableFont_wght.ttf b/public/fonts/Montserrat/Montserrat-VariableFont_wght.ttf new file mode 100644 index 0000000..451e692 Binary files /dev/null and b/public/fonts/Montserrat/Montserrat-VariableFont_wght.ttf differ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 21f4601..4282ec3 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,8 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; +import { Cursor } from "@/widgets/cursor"; +import { Preloader } from "@/widgets/preloader"; +import { ScrollCounter } from "@/widgets/scroll-counter"; import "@/static/globals.css"; const geistSans = Geist({ @@ -23,7 +26,12 @@ const RootLayout = ({ children }: LayoutProps<"/">) => { lang="en" className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`} > - {children} + + + + + {children} + ); }; diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx new file mode 100644 index 0000000..c8cac9b --- /dev/null +++ b/src/app/projects/page.tsx @@ -0,0 +1,12 @@ +import { ProjectsPage } from "@/views/projects"; + +const Projects = () => { + return ; +}; + +export const metadata = { + title: "Проекты", + description: "Все проекты Egorov Nikita", +}; + +export default Projects; \ No newline at end of file diff --git a/src/components/about/about.tsx b/src/components/about/about.tsx new file mode 100644 index 0000000..a9b3a8f --- /dev/null +++ b/src/components/about/about.tsx @@ -0,0 +1,109 @@ +"use client"; + +import { useInView } from "@/shared/hooks/useInView"; +import styles from "@/static/about/about.module.css"; + +const techStack = [ + { name: "TypeScript", level: 80 }, + { name: "React / Next.js", level: 85 }, + { name: "JavaScript", level: 90 }, + { name: "CSS / Tailwind", level: 85 }, + { name: "Node.js", level: 70 }, + { name: "Git / GitHub", level: 75 }, + { name: "REST API", level: 80 }, + { name: "Figma", level: 65 }, +]; + +const About = () => { + const [introRef, introInView] = useInView(0.2); + const [skillsRef, skillsInView] = useInView(0.3); + + const reveal = (visible: boolean) => + `transition-all duration-700 ease-out will-change-transform ${ + visible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-10" + }`; + + return ( +
+
+ ); +}; + +export default About; \ No newline at end of file diff --git a/src/components/hero/FlashlightText.tsx b/src/components/hero/FlashlightText.tsx new file mode 100644 index 0000000..c9c8307 --- /dev/null +++ b/src/components/hero/FlashlightText.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { useRef, type CSSProperties, type MouseEvent } from "react"; + +import styles from "@/static/hero/flashlight.module.css"; + +const OFFSCREEN = -260; + +type FlashlightTextProps = { + text: string; +}; + +const FlashlightText = ({ text }: FlashlightTextProps) => { + const ref = useRef(null); + + const setPosition = (x: number, y: number) => { + const element = ref.current; + if (!element) return; + + element.style.setProperty("--fx", `${x}px`); + element.style.setProperty("--fy", `${y}px`); + }; + + const handleMove = (e: MouseEvent) => { + const element = ref.current; + if (!element) return; + + const rect = element.getBoundingClientRect(); + + setPosition(e.clientX - rect.left, e.clientY - rect.top); + element.classList.remove(styles.idle); + }; + + const handleLeave = () => { + setPosition(OFFSCREEN, OFFSCREEN); + ref.current?.classList.add(styles.idle); + }; + + return ( + + + {text} + + ); +}; + +export default FlashlightText; \ No newline at end of file diff --git a/src/components/hero/hero.tsx b/src/components/hero/hero.tsx index 146d7ad..ff3aad0 100644 --- a/src/components/hero/hero.tsx +++ b/src/components/hero/hero.tsx @@ -1,20 +1,96 @@ import { sociallinks } from "@/shared/config/social-links/links"; import styles from "@/static/hero/hero.module.css"; +import FlashlightText from "./FlashlightText"; + +const socialIcon = (label: string) => { + switch (label.trim().toLowerCase()) { + case "telegram": + return ( + + ); + case "github": + return ( + + ); + default: + return null; + } +}; + const Hero = () => { return ( -
-
+
+
-

left

+
+ + + веб-разработчик + + +

+ +

+ +

+ Создаю современные, быстрые и продуманные веб-сайты и + веб-приложения под ключ. +

+ +
+ + Смотреть проекты + + + +
+
- 01 +
+ +
); }; diff --git a/src/components/projects/ProjectCard.tsx b/src/components/projects/ProjectCard.tsx new file mode 100644 index 0000000..5b279f0 --- /dev/null +++ b/src/components/projects/ProjectCard.tsx @@ -0,0 +1,75 @@ +"use client"; + +import type { KeyboardEvent, MouseEvent } from "react"; + +import type { Project } from "@/shared/config/projects/types"; +import styles from "@/static/projects/project-card.module.css"; + +type ProjectCardProps = { + project: Project; + onOpen: (project: Project) => void; +}; + +const ProjectCard = ({ project, onOpen }: ProjectCardProps) => { + const handleSpotlight = (e: MouseEvent) => { + const element = e.currentTarget; + + const rect = element.getBoundingClientRect(); + + element.style.setProperty( + "--mx", + `${((e.clientX - rect.left) / rect.width) * 100}%` + ); + element.style.setProperty( + "--my", + `${((e.clientY - rect.top) / rect.height) * 100}%` + ); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onOpen(project); + } + }; + + return ( +
onOpen(project)} + onKeyDown={handleKeyDown} + onMouseMove={handleSpotlight} + > +
+ {project.id} + {project.year} +
+ +

{project.title}

+ +

{project.description}

+ +
    + {project.tags.map((tag) => ( +
  • + {tag} +
  • + ))} +
+ + + подробнее + + +
+ ); +}; + +export default ProjectCard; \ No newline at end of file diff --git a/src/components/projects/projects.tsx b/src/components/projects/projects.tsx new file mode 100644 index 0000000..4e24c78 --- /dev/null +++ b/src/components/projects/projects.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { useState } from "react"; + +import Link from "next/link"; + +import { useInView } from "@/shared/hooks/useInView"; +import { projects } from "@/shared/config/projects/projects"; +import type { Project } from "@/shared/config/projects/types"; +import { ProjectModal } from "@/widgets/project-modal"; +import styles from "@/static/projects/projects.module.css"; + +import ProjectCard from "./ProjectCard"; + +const preview = projects.slice(0, 6); + +const ProjectsSection = () => { + const [gridRef, gridInView] = useInView(0.12); + const [selected, setSelected] = useState(null); + + const reveal = (visible: boolean) => + `transition-all duration-700 ease-out will-change-transform ${ + visible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8" + }`; + + return ( +
+
+ ); +}; + +export default ProjectsSection; \ No newline at end of file diff --git a/src/components/sponsors/sponsors.tsx b/src/components/sponsors/sponsors.tsx new file mode 100644 index 0000000..266e61c --- /dev/null +++ b/src/components/sponsors/sponsors.tsx @@ -0,0 +1,43 @@ +import styles from "@/static/sponsors/sponsors.module.css"; + +const sponsors = [ + { name: "Nova", className: styles.nova }, + { name: "LUMINA", className: styles.lumina }, + { name: "Vertex", className: styles.vertex }, + { name: "orbital", className: styles.orbit }, + { name: "MONOLITH", className: styles.monolith }, + { name: "Aster Studio", className: styles.aster }, + { name: "Kite", className: styles.kite }, + { name: "PULSE", className: styles.pulse }, +]; + +const List = ({ hidden = false }: { hidden?: boolean }) => ( +
    + {sponsors.map((sponsor) => ( +
  • + + {sponsor.name} + +
  • + ))} +
+); + +const Sponsors = () => { + return ( +
+
+

С ними я работал

+
+ +
+
+ +
+
+
+ ); +}; + +export default Sponsors; \ No newline at end of file diff --git a/src/components/testimonials/testimonials.tsx b/src/components/testimonials/testimonials.tsx new file mode 100644 index 0000000..9bfaeb5 --- /dev/null +++ b/src/components/testimonials/testimonials.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { useInView } from "@/shared/hooks/useInView"; +import styles from "@/static/testimonials/testimonials.module.css"; + +const testimonials = [ + { + quote: + "Никита сделал лендинг, который реально приносит лиды. Быстро, аккуратно, без лишней воды в коммуникации.", + name: "Алексей Орлов", + role: "Основатель финтех-стартапа", + }, + { + quote: + "Код понятный, страницы летают, а правки после сдачи вносить легко. Работать с ним — одно удовольствие.", + name: "Мария Соколова", + role: "Продакт-менеджер", + }, + { + quote: + "За 2 недели собрал нам интернет-магазин под ключ: каталог, оплата, админка. Всё чётко и в срок.", + name: "Дмитрий Волков", + role: "Владелец бренда одежды", + }, +]; + +const initials = (name: string) => + name + .split(" ") + .map((part) => part[0]) + .join(""); + +const Testimonials = () => { + const [gridRef, gridInView] = useInView(0.15); + + const reveal = (visible: boolean) => + `transition-all duration-700 ease-out will-change-transform ${ + visible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8" + }`; + + return ( +
+
+ ); +}; + +export default Testimonials; \ No newline at end of file diff --git a/src/shared/config/navigation/navigation.ts b/src/shared/config/navigation/navigation.ts index 752dc37..99c4820 100644 --- a/src/shared/config/navigation/navigation.ts +++ b/src/shared/config/navigation/navigation.ts @@ -3,10 +3,14 @@ import type { NavigationItem } from "./types"; export const navigation: NavigationItem[] = [ { label: "Обо мне", - href: "#about", + href: "/#about", }, { label: "Проекты", - href: "#projects", + href: "/projects", + }, + { + label: "Контакты", + href: "/#contact", }, ]; \ No newline at end of file diff --git a/src/shared/config/projects/projects.ts b/src/shared/config/projects/projects.ts new file mode 100644 index 0000000..f8a9fdc --- /dev/null +++ b/src/shared/config/projects/projects.ts @@ -0,0 +1,157 @@ +import type { Project } from "./types"; + +export const projects: Project[] = [ + { + id: "01", + title: "Fintech Landing", + description: + "Быстрый лендинг финтех-стартапа: анимации, формы заявки и стабильная производительность.", + year: "2025", + tags: ["Next.js", "TypeScript", "Tailwind"], + details: + "Одностраничный сайт для финтех-стартапа: продуманная структура, анимации появления, формы заявок и интеграция с метриками. Упор на скорость загрузки и рост конверсии.", + features: [ + "Лендинг со скоростью загрузки < 1 с", + "Анимированные блоки и формы заявки", + "Интеграция с Яндекс.Метрикой и аналитикой", + "Адаптив под все устройства", + ], + code: "https://github.com/autli5", + }, + { + id: "02", + title: "Маркетплейс", + description: + "Сервис для продажи и покупки товаров: каталог, корзина, оплата и личный кабинет.", + year: "2025", + tags: ["Next.js", "PostgreSQL", "Prisma"], + details: + "Маркетплейс с ролями покупателя и продавца: каталог с фильтрами, корзина, онлайн-оплата, личный кабинет и панель продавца для управления товарами.", + features: [ + "Каталог с фильтрами и поиском", + "Корзина и онлайн-оплата", + "Личный кабинет покупателя", + "Панель продавца с загрузкой товаров", + ], + code: "https://github.com/autli5", + }, + { + id: "03", + title: "SaaS Dashboard", + description: + "Аналитика в реальном времени: графики, отчёты и экспорт данных для бизнеса.", + year: "2024", + tags: ["React", "TypeScript", "Recharts"], + details: + "Дашборд для SaaS-продукта: витрина метрик в реальном времени, интерактивные графики, построение отчётов и экспорт в CSV. Тёмная тема и кастомизация виджетов.", + features: [ + "Графики в реальном времени", + "Конструктор отчётов и экспорт в CSV", + "Пользовательские виджеты и тёмная тема", + "Ролевой доступ к данным", + ], + code: "https://github.com/autli5", + }, + { + id: "04", + title: "Корпоративный сайт", + description: + "Сайт компании на headless CMS с мультиязычностью и редакторским контролем.", + year: "2024", + tags: ["Next.js", "CMS", "Editorial"], + details: + "Корпоративный сайт на headless CMS: редакторы сами публикуют новости и страницы, есть мультиязычность, SEO-разметка и политика конфиденциальности.", + features: [ + "Управление контентом через CMS", + "Мультиязычность (RU / EN)", + "SEO-семантика и Open Graph", + "Блог с постраничной навигацией", + ], + code: "https://github.com/autli5", + }, + { + id: "05", + title: "Доставка · PWA", + description: + "Веб-приложение доставки с офлайн-режимом, трекингом заказов и push-уведомлениями.", + year: "2024", + tags: ["React", "Node.js", "PWA"], + details: + "PWA-приложение службы доставки: офлайн-работа, установка на устройство, трекинг заказов в реальном времени и push-уведомления о статусе.", + features: [ + "Офлайн-режим через service worker", + "Push-уведомления и установка на устройство", + "Трекинг заказа в реальном времени", + "Корзина и быстрое оформление", + ], + code: "https://github.com/autli5", + }, + { + id: "06", + title: "Магазин одежды", + description: + "Интернет-магазин с каталогом, умными фильтрами и онлайн-оплатой.", + year: "2023", + tags: ["Next.js", "Tailwind", "Stripe"], + details: + "Интернет-магазин одежды: каталог с фильтрами по размерам и сезонам, быстрый просмотр товара, онлайн-оплата через Stripe и уведомления о заказах.", + features: [ + "Фильтры и сортировка каталога", + "Онлайн-оплата через Stripe", + "Модальные карточки товара", + "Админка для управления остатками", + ], + code: "https://github.com/autli5", + }, + { + id: "07", + title: "Криптовалютный кошелёк", + description: + "Web-кошелёк с отслеживанием баланса, курсов и историей транзакций.", + year: "2023", + tags: ["React", "TypeScript", "Web3"], + details: + "Клиент для криптовалютного кошелька: подключение через Web3, отслеживание баланса и курсов, история транзакций и переводы между адресами.", + features: [ + "Подключение кошелька через Web3", + "Мониторинг баланса и курсов", + "История и статусы транзакций", + "Переводы между адресами", + ], + code: "https://github.com/autli5", + }, + { + id: "08", + title: "Агрегатор новостей", + description: + "Лента новостей из нескольких источников с фильтрами и поиском.", + year: "2023", + tags: ["Next.js", "TypeScript", "RSS"], + details: + "Агрегатор новостей: сбор ленты из RSS-источников, категории, полнотекстовый поиск, сохранение статей и тёмная тема.", + features: [ + "Сбор ленты из нескольких RSS-источников", + "Фильтры по категориям и поиск", + "Сохранение в «избранное»", + "Тёмная тема", + ], + code: "https://github.com/autli5", + }, + { + id: "09", + title: "Лендинг кофейни", + description: + "Атмосферный лендинг кофейни с меню, галереей и бронированием столика.", + year: "2022", + tags: ["Astro", "GSAP", "Jamstack"], + details: + "Лендинг кофейни: галерея с лайтбоксом, меню с фильтрами, форма бронирования столика и плавные scroll-анимации на GSAP. Генерация статики — максимальная скорость.", + features: [ + "Меню с фильтрацией по категориям", + "Галерея с лайтбоксом", + "Форма бронирования столика", + "Scroll-анимации на GSAP", + ], + code: "https://github.com/autli5", + }, +]; \ No newline at end of file diff --git a/src/shared/config/projects/types.ts b/src/shared/config/projects/types.ts new file mode 100644 index 0000000..50ada5a --- /dev/null +++ b/src/shared/config/projects/types.ts @@ -0,0 +1,11 @@ +export type Project = { + id: string; + title: string; + description: string; + year: string; + tags: string[]; + details: string; + features: string[]; + demo?: string; + code?: string; +}; \ No newline at end of file diff --git a/src/shared/hooks/useInView.ts b/src/shared/hooks/useInView.ts new file mode 100644 index 0000000..cd652e5 --- /dev/null +++ b/src/shared/hooks/useInView.ts @@ -0,0 +1,27 @@ +import { useEffect, useRef, useState } from "react"; + +export const useInView = (threshold = 0.2) => { + const ref = useRef(null); + const [inView, setInView] = useState(false); + + useEffect(() => { + const element = ref.current; + if (!element) return; + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setInView(true); + observer.disconnect(); + } + }, + { threshold } + ); + + observer.observe(element); + + return () => observer.disconnect(); + }, [threshold]); + + return [ref, inView] as const; +}; \ No newline at end of file diff --git a/src/static/about/about.module.css b/src/static/about/about.module.css new file mode 100644 index 0000000..7c87a7d --- /dev/null +++ b/src/static/about/about.module.css @@ -0,0 +1,230 @@ +/* ========================= + ABOUT +========================= */ + +.about { + position: relative; + overflow: hidden; + + padding-block: var(--section-y); +} + +/* decorative glows */ +.glowA, +.glowB { + position: absolute; + + border-radius: 50%; + + filter: blur(96px); + + pointer-events: none; +} + +.glowA { + top: 18%; + left: -180px; + + width: 420px; + height: 420px; + + background: rgba(255, 255, 255, 0.05); +} + +.glowB { + right: -220px; + bottom: 8%; + + width: 520px; + height: 520px; + + background: rgba(255, 255, 255, 0.04); +} + +/* big decorative ghost number behind the section */ +.ghost { + position: absolute; + top: 40px; + right: 4vw; + + z-index: 0; + + font-size: clamp(160px, 24vw, 460px); + font-weight: 800; + line-height: 1; + letter-spacing: -0.05em; + + color: transparent; + + -webkit-text-stroke: 1px rgba(255, 255, 255, 0.05); + + pointer-events: none; + + user-select: none; +} + +/* ========================= + CONTENT +========================= */ + +.eyebrow { + display: inline-flex; + align-items: center; + gap: var(--space-xs); + + color: var(--color-muted); + + font-size: var(--fs-overline); + font-weight: 500; + letter-spacing: var(--ls-overline); + text-transform: uppercase; +} + +.rule { + width: var(--space-lg); + height: 1px; + + background: var(--color-muted); +} + +.title { + margin-top: var(--space-md); + + max-width: 24em; + + font-size: var(--fs-display); + font-weight: 700; + line-height: var(--lh-tight); + letter-spacing: -0.02em; +} + +.titleAccent { + color: var(--color-faint); +} + +.text { + margin-top: var(--space-md); + + max-width: var(--text-measure); + + color: var(--color-muted); + + font-size: var(--fs-body); + line-height: var(--lh-body); +} + +/* ========================= + STATS +========================= */ + +.stats { + display: flex; + flex-wrap: wrap; + gap: var(--space-xl); + + margin-block: var(--space-xl) 0; + padding-top: var(--space-md); + + border-top: 1px solid var(--color-line); +} + +.statLabel { + color: var(--color-faint); + + font-size: var(--fs-small); + text-transform: uppercase; + letter-spacing: 0.14em; +} + +.statValue { + margin-top: var(--space-xs); + + font-size: var(--fs-title); + font-weight: 700; + line-height: 1; +} + +.statValueAccent { + color: var(--color-faint); +} + +/* ========================= + SKILLS +========================= */ + +.skills { + margin-top: var(--space-2xl); +} + +.skillsTitle { + color: var(--color-faint); + + font-size: var(--fs-overline); + font-weight: 500; + letter-spacing: var(--ls-overline); + text-transform: uppercase; +} + +.skillList { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: var(--space-lg); + + margin: 0; + padding: var(--space-md) 0 0; + + list-style: none; +} + +.skillName { + color: var(--color-text); + + font-size: var(--fs-body); + font-weight: 600; +} + +.skillBar { + height: 3px; + + margin-top: var(--space-xs); + + border-radius: 100px; + + background: var(--color-line); + + overflow: hidden; +} + +.skillFill { + display: block; + + height: 100%; + + border-radius: 100px; + + background: var(--color-text); + + transition: width 1s cubic-bezier(0.22, 1, 0.36, 1); +} + +/* ========================= + MOBILE +========================= */ + +@media (max-width: 768px) { + .ghost { + top: 24px; + right: -30px; + } +} + +@media (max-width: 640px) { + .skillList { + grid-template-columns: 1fr; + gap: var(--space-md); + } + + .stats { + gap: var(--space-lg); + } +} \ No newline at end of file diff --git a/src/static/cursor/cursor.module.css b/src/static/cursor/cursor.module.css new file mode 100644 index 0000000..7f45db3 --- /dev/null +++ b/src/static/cursor/cursor.module.css @@ -0,0 +1,114 @@ +/* ========================= + CUSTOM CURSOR +========================= */ + +.cursor { + position: fixed; + inset: 0; + z-index: 10000; + + pointer-events: none; + + opacity: 0; + transition: opacity 0.35s ease; +} + +.visible { + opacity: 1; +} + +.dot { + position: absolute; + top: 0; + left: 0; + + width: 8px; + height: 8px; + + margin: -4px 0 0 -4px; + + border-radius: 50%; + + background: #fff; + + transform: translate3d(-100px, -100px, 0); + + transition: + width 0.25s ease, + height 0.25s ease, + margin 0.25s ease, + opacity 0.25s ease, + background 0.25s ease; +} + +.ring { + position: absolute; + top: 0; + left: 0; + + width: 40px; + height: 40px; + + margin: -20px 0 0 -20px; + + border: 1px solid rgba(255, 255, 255, 0.4); + border-radius: 50%; + + transform: translate3d(-100px, -100px, 0); + + transition: + width 0.25s cubic-bezier(0.22, 1, 0.36, 1), + height 0.25s cubic-bezier(0.22, 1, 0.36, 1), + margin 0.25s cubic-bezier(0.22, 1, 0.36, 1), + border-color 0.25s ease, + background 0.25s ease; +} + +/* ========================= + HOVERING INTERACTIVE +========================= */ + +.hovering .ring { + width: 64px; + height: 64px; + + margin: -32px 0 0 -32px; + + border-color: rgba(255, 255, 255, 0.9); + + background: rgba(255, 255, 255, 0.07); +} + +.hovering .dot { + width: 6px; + height: 6px; + + margin: -3px 0 0 -3px; + + opacity: 0; +} + +/* ========================= + PRESSED +========================= */ + +.pressed .ring { + width: 30px; + height: 30px; + + margin: -15px 0 0 -15px; + + border-color: #fff; + + background: #fff; +} + +.pressed .dot { + opacity: 0; +} + +@media (prefers-reduced-motion: reduce) { + .ring { + transition: none; + } +} \ No newline at end of file diff --git a/src/static/footer/footer.module.css b/src/static/footer/footer.module.css new file mode 100644 index 0000000..da80ba6 --- /dev/null +++ b/src/static/footer/footer.module.css @@ -0,0 +1,211 @@ +/* ========================= + FOOTER +========================= */ + +.footer { + position: relative; + + overflow: hidden; + + border-top: 1px solid var(--color-line); + + padding-block: var(--space-2xl) var(--space-lg); +} + +.watermark { + position: absolute; + left: 50%; + bottom: -0.34em; + + z-index: 0; + + transform: translateX(-50%); + + font-size: clamp(120px, 22vw, 360px); + font-weight: 800; + line-height: 1; + letter-spacing: -0.06em; + + color: transparent; + + -webkit-text-stroke: 1px rgba(255, 255, 255, 0.06); + + white-space: nowrap; + + pointer-events: none; + + user-select: none; +} + +/* ========================= + CTA +========================= */ + +.cta { + position: relative; + z-index: 1; + + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + + gap: var(--space-md); +} + +.eyebrow { + color: var(--color-muted); + + font-size: var(--fs-overline); + font-weight: 500; + letter-spacing: var(--ls-overline); + text-transform: uppercase; +} + +.title { + max-width: 18em; + + font-size: var(--fs-display); + font-weight: 800; + line-height: var(--lh-tight); + letter-spacing: -0.03em; +} + +.titleAccent { + color: var(--color-faint); +} + +.text { + max-width: var(--text-measure); + + color: var(--color-muted); + + font-size: var(--fs-body); + line-height: var(--lh-body); +} + +.actions { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + + gap: var(--space-md); + + margin-top: var(--space-xs); +} + +.button, +.buttonGhost { + display: inline-flex; + align-items: center; + + gap: var(--space-xs); + + padding: var(--space-sm) var(--space-lg); + + border-radius: 100px; + + font-size: var(--fs-body); + font-weight: 600; + text-decoration: none; + + transition: + transform 0.3s ease, + box-shadow 0.3s ease, + background 0.3s ease, + color 0.3s ease, + border-color 0.3s ease; +} + +.button { + background: #fff; + color: #000; +} + +.button:hover { + transform: translateY(-2px); + + box-shadow: 0 18px 40px rgba(255, 255, 255, 0.16); +} + +.buttonGhost { + border: 1px solid rgba(255, 255, 255, 0.2); + + background: transparent; + color: var(--color-text); +} + +.buttonGhost:hover { + border-color: rgba(255, 255, 255, 0.5); + + background: rgba(255, 255, 255, 0.06); + + transform: translateY(-2px); +} + +/* ========================= + BOTTOM ROW +========================= */ + +.bottom { + position: relative; + z-index: 1; + + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + + gap: var(--space-md); + + margin-top: var(--space-2xl); + padding-top: var(--space-md); + + border-top: 1px solid var(--color-line); +} + +.wordmark { + font-size: var(--fs-logo); + font-weight: 800; + line-height: 1; + letter-spacing: -0.06em; +} + +.nav ul { + display: flex; + align-items: center; + + gap: var(--space-md); + + margin: 0; + padding: 0; + + list-style: none; +} + +.nav a { + color: var(--color-muted); + + font-size: var(--fs-small); + text-decoration: none; + + transition: color 0.25s ease; +} + +.nav a:hover { + color: var(--color-text); +} + +.copy { + color: var(--color-faint); + + font-size: var(--fs-caption); +} + +@media (max-width: 640px) { + .bottom { + justify-content: center; + text-align: center; + } +} \ No newline at end of file diff --git a/src/static/globals.css b/src/static/globals.css index ee77360..9696bf2 100644 --- a/src/static/globals.css +++ b/src/static/globals.css @@ -1,3 +1,15 @@ +@import "tailwindcss"; + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +/* ========================= + FONTS +========================= */ + @font-face { font-family: "Montserrat"; src: url("/fonts/Montserrat/Montserrat-VariableFont_wght.ttf") format("truetype"); @@ -14,27 +26,136 @@ font-display: swap; } +/* ========================= + DESIGN TOKENS +========================= */ + :root { /* Font */ --font-main: "Montserrat", sans-serif; + + /* Color */ + --color-bg: #000; + --color-text: #fff; + --color-muted: rgba(255, 255, 255, 0.55); + --color-faint: rgba(255, 255, 255, 0.38); + --color-line: rgba(255, 255, 255, 0.1); + + /* Layout container */ + --container-max: 1280px; + --container-padding: clamp(20px, 4vw, 64px); + --container-gap: clamp(24px, 3vw, 64px); + + /* Spacing scale */ + --space-xs: 8px; + --space-sm: 16px; + --space-md: 24px; + --space-lg: 40px; + --space-xl: 64px; + --space-2xl: 96px; + --section-y: clamp(72px, 9vw, 128px); + + /* Text measure */ + --text-measure: 62ch; + + /* Type scale */ + --fs-base: 16px; + --fs-hero: clamp(44px, 7vw, 108px); + --fs-display: clamp(28px, 3.6vw, 46px); + --fs-title: clamp(18px, 1.6vw, 24px); + --fs-logo: clamp(26px, 2vw, 38px); + --fs-body: clamp(15px, 1.05vw, 17px); + --fs-small: 14px; + --fs-caption: 12px; + --fs-overline: 13px; + --fs-nav: clamp(15px, 1vw, 19px); + + /* Line heights & letter-spacing */ + --lh-tight: 1.05; + --lh-body: 1.6; + --ls-overline: 0.28em; +} + +/* ========================= + BASE +========================= */ + +html { + scroll-behavior: smooth; } html, body { font-family: var(--font-main); + font-size: var(--fs-base); background-color: var(--color-bg); color: var(--color-text); - font-size: var(--fz-normal); + line-height: var(--lh-body); } -h1, a { - color: #fff; + +h1, +h2, +h3, +h4, +h5, +h6 { + font-weight: 700; + color: var(--color-text); +} + +a { + color: var(--color-text); +} + +/* Custom cursor: hide native cursor on fine-pointer devices */ +@media (hover: hover) and (pointer: fine) { + html.has-cursor, + html.has-cursor * { + cursor: none !important; + } +} + +/* Lock scroll while the preloader is visible */ +html.no-scroll, +html.no-scroll body { + overflow: hidden; +} + +/* ========================= + HIDE NATIVE SCROLLBAR + (прокрутка остаётся — wheel/трекпад) +========================= */ + +html { + scrollbar-width: none; + -ms-overflow-style: none; +} + +html::-webkit-scrollbar, +body::-webkit-scrollbar { + display: none; +} + +/* ========================= + LAYOUT GRID — единственный + source of truth для ширины + и горизонтальных отступов +========================= */ + +.page-container { + width: 100%; + max-width: var(--container-max); + margin-inline: auto; + padding-inline: var(--container-padding); } .cont-header { width: 100%; - margin: 0 auto; - padding: 0 var(--container-padding, 40px); + max-width: var(--container-max); + margin-inline: auto; + padding-inline: var(--container-padding); display: flex; justify-content: space-between; align-items: center; + gap: var(--container-gap); } \ No newline at end of file diff --git a/src/static/header/header.module.css b/src/static/header/header.module.css index d61d81d..4aaed57 100644 --- a/src/static/header/header.module.css +++ b/src/static/header/header.module.css @@ -1,16 +1,25 @@ +/* ========================= + HEADER +========================= */ + .header { - position: relative; + position: sticky; + top: 0; display: flex; + justify-content: center; z-index: 1000; width: 100%; height: 90px; - color: #fff; + color: var(--color-text); - padding: var(--container-padding); + background: rgba(0, 0, 0, 0.85); - border-bottom: 1px solid #181818; + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + + border-bottom: 1px solid var(--color-line); } /* ========================= @@ -25,11 +34,11 @@ .logo { display: inline-block; - color: #fff; + color: var(--color-text); - font-size: clamp(28px, 2vw, 38px); - line-height: 1; + font-size: var(--fs-logo); font-weight: 700; + line-height: 1; letter-spacing: -0.06em; text-decoration: none; @@ -41,7 +50,6 @@ opacity: 0.65; } - /* ========================= RIGHT ========================= */ @@ -49,11 +57,9 @@ .right { display: flex; align-items: center; - - gap: clamp(24px, 3vw, 55px); + gap: var(--space-md); } - /* ========================= DESKTOP NAV ========================= */ @@ -62,7 +68,7 @@ display: flex; align-items: center; - gap: clamp(24px, 2.8vw, 55px); + gap: var(--space-lg); margin: 0; padding: 0; @@ -82,9 +88,9 @@ padding: 8px 0; - color: #777; + color: var(--color-muted); - font-size: clamp(15px, 1vw, 20px); + font-size: var(--fs-nav); font-weight: 500; letter-spacing: -0.01em; @@ -103,20 +109,19 @@ width: 0; height: 1px; - background: #fff; + background: var(--color-text); transition: width 0.3s ease; } .desktopNav a:hover { - color: #fff; + color: var(--color-text); } .desktopNav a:hover::after { width: 100%; } - /* ========================= HAMBURGER ========================= */ @@ -147,7 +152,7 @@ border-radius: 10px; - background: #fff; + background: var(--color-text); transition: transform 0.3s ease, @@ -167,6 +172,18 @@ bottom: 0; } +.hamburger.active span:nth-child(1) { + transform: translateY(11px) rotate(45deg); +} + +.hamburger.active span:nth-child(2) { + opacity: 0; + transform: translateX(10px); +} + +.hamburger.active span:nth-child(3) { + transform: translateY(-11px) rotate(-45deg); +} /* ========================= MOBILE MENU @@ -180,9 +197,9 @@ width: 100%; height: calc(100vh - 90px); - padding: 60px var(--container-padding); + padding: var(--space-2xl) var(--container-padding); - background: #000; + background: var(--color-bg); transform: translateX(100%); @@ -199,7 +216,7 @@ display: flex; flex-direction: column; - gap: 30px; + gap: var(--space-md); margin: 0; padding: 0; @@ -215,12 +232,11 @@ .mobileMenu a { display: inline-block; - color: #fff; - - font-size: clamp(30px, 6vw, 52px); - line-height: 1; + color: var(--color-text); + font-size: var(--fs-display); font-weight: 600; + line-height: 1; letter-spacing: -0.04em; text-decoration: none; @@ -229,10 +245,9 @@ } .mobileMenu a:hover { - color: #777; + color: var(--color-muted); } - /* ========================= LARGE SCREENS ========================= */ @@ -241,21 +256,8 @@ .header { height: 110px; } - - .logo { - font-size: 42px; - } - - .desktopNav a { - font-size: 20px; - } - - .desktopNav ul { - gap: 50px; - } } - /* ========================= TABLET ========================= */ @@ -265,20 +267,11 @@ height: 80px; } - .logo { - font-size: 28px; - } - .desktopNav ul { - gap: 24px; - } - - .desktopNav a { - font-size: 15px; + gap: var(--space-md); } } - /* ========================= MOBILE ========================= */ @@ -300,13 +293,8 @@ top: 70px; height: calc(100vh - 70px); } - - .right { - gap: 0; - } } - /* ========================= SMALL MOBILE ========================= */ @@ -329,14 +317,6 @@ top: 64px; height: calc(100vh - 64px); - padding-top: 45px; - } - - .mobileMenu ul { - gap: 26px; - } - - .mobileMenu a { - font-size: 32px; + padding-top: var(--space-lg); } } \ No newline at end of file diff --git a/src/static/hero/flashlight.module.css b/src/static/hero/flashlight.module.css new file mode 100644 index 0000000..8c96f5a --- /dev/null +++ b/src/static/hero/flashlight.module.css @@ -0,0 +1,64 @@ +/* ========================= + FLASHLIGHT TEXT +========================= */ + +.root { + position: relative; + display: inline-block; +} + +.base { + display: block; + + color: rgba(255, 255, 255, 0.14); +} + +.highlight { + position: absolute; + inset: 0; + + display: block; + + color: #fff; + + pointer-events: none; + + text-shadow: 0 0 26px rgba(255, 255, 255, 0.55); + + transition: opacity 0.35s ease; + + -webkit-mask-image: radial-gradient( + circle 170px at var(--fx, -260px) var(--fy, -260px), + #000 0%, + rgba(0, 0, 0, 0.9) 25%, + rgba(0, 0, 0, 0.35) 55%, + transparent 78% + ); + + mask-image: radial-gradient( + circle 170px at var(--fx, -260px) var(--fy, -260px), + #000 0%, + rgba(0, 0, 0, 0.9) 25%, + rgba(0, 0, 0, 0.35) 55%, + transparent 78% + ); +} + +/* свет гаснет, когда курсор покидает текст */ +.root.idle .highlight { + opacity: 0; +} + +/* ========================= + TOUCH DEVICES +========================= */ + +@media (hover: none), (pointer: coarse) { + .base { + color: rgba(255, 255, 255, 0.92); + } + + .highlight { + display: none; + } +} \ No newline at end of file diff --git a/src/static/hero/hero.module.css b/src/static/hero/hero.module.css index 485c131..e11092a 100644 --- a/src/static/hero/hero.module.css +++ b/src/static/hero/hero.module.css @@ -1,10 +1,413 @@ +/* ========================= + HERO +========================= */ + .hero { - background-color: aquamarine; - height: calc(100vh - 90px); + position: relative; + + display: flex; + align-items: center; + + min-height: calc(100vh - 90px); + overflow: hidden; } +/* subtle grid */ +.hero::before { + content: ""; + + position: absolute; + inset: 0; + + z-index: 0; + + background-image: + linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px); + background-size: 72px 72px; + + -webkit-mask-image: radial-gradient( + ellipse 70% 90% at 30% 55%, + #000 0%, + transparent 70% + ); + + mask-image: radial-gradient( + ellipse 70% 90% at 30% 55%, + #000 0%, + transparent 70% + ); + + pointer-events: none; +} + +/* ambient glow */ +.hero::after { + content: ""; + + position: absolute; + top: -22%; + right: -12%; + + width: 58vw; + height: 58vw; + + border-radius: 50%; + + background: radial-gradient(circle, rgba(255, 255, 255, 0.07) 0%, transparent 60%); + + pointer-events: none; +} + +.inner { + position: relative; + z-index: 1; +} + +/* scroll hint at the bottom */ +.hint { + position: absolute; + left: 50%; + bottom: var(--space-md); + + z-index: 1; + + transform: translateX(-50%); + + display: flex; + flex-direction: column; + align-items: center; + + gap: var(--space-xs); + + color: var(--color-faint); + + font-size: var(--fs-caption); + font-weight: 500; + letter-spacing: 0.3em; + text-transform: uppercase; + + pointer-events: none; +} + +.hintLine { + display: block; + + width: 1px; + height: 36px; + + background: linear-gradient(180deg, rgba(255, 255, 255, 0.6), transparent); + + animation: hintFloat 2.2s ease-in-out infinite; +} + +@keyframes hintFloat { + 0% { + transform: scaleY(0.4); + transform-origin: top; + } + 50% { + transform: scaleY(1); + transform-origin: top; + } + 100% { + transform: scaleY(0.4); + transform-origin: top; + } +} + +.left { + flex: 1; + min-width: 0; +} + +.info { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: var(--space-md); + + padding: var(--space-xl) 0; +} + +/* ========================= + ROLE BADGE +========================= */ + +.role { + display: inline-flex; + align-items: center; + gap: var(--space-xs); + + padding: 10px 18px; + + border: 1px solid var(--color-line); + border-radius: 100px; + + background: rgba(255, 255, 255, 0.03); + + color: var(--color-muted); + + font-size: var(--fs-overline); + font-weight: 500; + letter-spacing: var(--ls-overline); + text-transform: uppercase; +} + +.dot { + width: 7px; + height: 7px; + + border-radius: 50%; + + background: #fff; + + box-shadow: + 0 0 0 4px rgba(255, 255, 255, 0.12), + 0 0 18px rgba(255, 255, 255, 0.9); +} + +/* ========================= + NAME +========================= */ + +.name { + margin: 0; + + font-size: var(--fs-hero); + font-weight: 800; + line-height: var(--lh-tight); + letter-spacing: -0.04em; +} + +/* ========================= + DESCRIPTION +========================= */ + +.desc { + max-width: var(--text-measure); + + color: var(--color-muted); + + font-size: var(--fs-body); + line-height: var(--lh-body); +} + +/* ========================= + ACTIONS +========================= */ + +.actions { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--space-md); + + margin-top: var(--space-xs); +} + +.button { + display: inline-flex; + align-items: center; + justify-content: center; + + padding: var(--space-sm) var(--space-lg); + + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 100px; + + background: transparent; + + color: var(--color-text); + + font-size: var(--fs-body); + font-weight: 600; + letter-spacing: 0.04em; + + text-decoration: none; + + transition: + background 0.3s ease, + color 0.3s ease, + box-shadow 0.3s ease, + transform 0.3s ease; +} + +.button:hover { + background: #fff; + color: #000; + + box-shadow: 0 18px 40px rgba(255, 255, 255, 0.18); + + transform: translateY(-2px); +} + +/* ========================= + SOCIALS +========================= */ + +.socials { + display: flex; + gap: var(--space-xs); + + margin: 0; + padding: 0; + + list-style: none; +} + +.social { + display: inline-flex; + align-items: center; + justify-content: center; + + width: 52px; + height: 52px; + + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 50%; + + color: rgba(255, 255, 255, 0.75); + + text-decoration: none; + + transition: + background 0.3s ease, + color 0.3s ease, + border-color 0.3s ease, + transform 0.3s ease; +} + +.social:hover { + border-color: rgba(255, 255, 255, 0.5); + + background: rgba(255, 255, 255, 0.06); + + color: #fff; + + transform: translateY(-2px); +} + +/* ========================= + RIGHT VISUAL +========================= */ + .right { - width: 49%; - height: 90vh; - background-color: red; + width: min(30vw, 400px); + flex-shrink: 0; +} + +@media (max-width: 1200px) { + .right { + width: min(28vw, 340px); + } +} + +.visual { + position: relative; + + display: flex; + align-items: center; + justify-content: center; + + width: 100%; + aspect-ratio: 1; + + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 50%; + + overflow: hidden; + + background: radial-gradient(circle at 35% 30%, rgba(255, 255, 255, 0.08) 0%, transparent 55%); +} + +.visual::before { + content: ""; + + position: absolute; + inset: 8%; + + border: 1px dashed rgba(255, 255, 255, 0.12); + border-radius: 50%; + + animation: spin 30s linear infinite; +} + +.index { + position: relative; + + display: inline-flex; + + color: var(--color-faint); + + font-size: var(--fs-caption); + font-weight: 500; + letter-spacing: var(--ls-overline); + text-transform: uppercase; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* ========================= + TABLET +========================= */ + +@media (min-width: 1600px) { + .hero { + min-height: calc(100vh - 110px); + } +} + +@media (max-width: 1024px) { + .hero { + min-height: calc(100vh - 80px); + } +} + +@media (max-width: 900px) { + .inner { + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 56px; + } + + .right { + width: min(60vw, 300px); + align-self: center; + } +} + +/* ========================= + MOBILE +========================= */ + +@media (max-width: 768px) { + .hero { + min-height: calc(100vh - 70px); + } + + .hint { + display: none; + } + + .right { + display: none; + } +} + +@media (max-width: 480px) { + .hero { + min-height: calc(100vh - 64px); + } +} + +@media (prefers-reduced-motion: reduce) { + .visual::before { + animation: none; + } } \ No newline at end of file diff --git a/src/static/preloader/preloader.module.css b/src/static/preloader/preloader.module.css new file mode 100644 index 0000000..814d0f8 --- /dev/null +++ b/src/static/preloader/preloader.module.css @@ -0,0 +1,117 @@ +/* ========================= + PRELOADER +========================= */ + +.preloader { + position: fixed; + inset: 0; + + z-index: 9999; + + display: flex; + align-items: center; + justify-content: center; + + background: #000; + + transform: translateY(0); + + transition: + transform 0.7s cubic-bezier(0.76, 0, 0.24, 1), + opacity 0.5s ease; +} + +.done { + transform: translateY(-100%); +} + +.content { + display: flex; + flex-direction: column; + align-items: center; + gap: 28px; + + width: min(420px, calc(100% - 80px)); +} + +.logo { + display: flex; + align-items: baseline; + + color: var(--color-text); + + font-size: var(--fs-display); + font-weight: 800; + letter-spacing: -0.06em; + + line-height: 1; +} + +.accent { + color: transparent; + + -webkit-text-stroke: 1px rgba(255, 255, 255, 0.7); + + margin-left: 2px; +} + +.bar { + position: relative; + + width: 100%; + height: 1px; + + background: rgba(255, 255, 255, 0.15); +} + +.barFill { + position: absolute; + top: 0; + left: 0; + + height: 100%; + + background: #fff; + + box-shadow: 0 0 12px rgba(255, 255, 255, 0.8); + + transition: width 0.15s linear; +} + +.meta { + display: flex; + justify-content: space-between; + + width: 100%; + + color: rgba(255, 255, 255, 0.45); + + font-size: var(--fs-caption); + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.percent { + color: rgba(255, 255, 255, 0.9); +} + +/* ========================= + PULSING HINT +========================= */ + +@media (prefers-reduced-motion: no-preference) { + .logo { + animation: pulse 1.4s ease-in-out infinite; + } +} + +@keyframes pulse { + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.55; + } +} \ No newline at end of file diff --git a/src/static/project-modal/project-modal.module.css b/src/static/project-modal/project-modal.module.css new file mode 100644 index 0000000..84db196 --- /dev/null +++ b/src/static/project-modal/project-modal.module.css @@ -0,0 +1,281 @@ +/* ========================= + PROJECT MODAL +========================= */ + +.overlay { + position: fixed; + inset: 0; + + z-index: 9990; + + display: flex; + align-items: center; + justify-content: center; + + padding: var(--container-padding); + + background: rgba(0, 0, 0, 0.72); + + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); + + animation: backdropIn 0.3s ease; +} + +.panel { + width: min(560px, 100%); + max-height: min(78vh, 720px); + + border: 1px solid var(--color-line); + border-radius: 24px; + + background: #0a0a0a; + + color: var(--color-text); + + overflow: hidden; + + animation: modalIn 0.35s cubic-bezier(0.22, 1, 0.36, 1); +} + +.panel:focus { + outline: none; +} + +@keyframes modalIn { + from { + opacity: 0; + + transform: translateY(14px) scale(0.97); + } + + to { + opacity: 1; + + transform: none; + } +} + +@keyframes backdropIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +/* ========================= + CONTENT +========================= */ + +.wrap { + display: flex; + flex-direction: column; + + gap: var(--space-md); + + max-height: inherit; + + padding: var(--space-lg); + + overflow-y: auto; + + scrollbar-width: none; + -ms-overflow-style: none; +} + +.wrap::-webkit-scrollbar { + display: none; +} + +.head { + display: flex; + align-items: center; + justify-content: space-between; +} + +.meta { + color: var(--color-faint); + + font-size: var(--fs-caption); + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.close { + display: inline-flex; + align-items: center; + justify-content: center; + + width: 36px; + height: 36px; + + flex-shrink: 0; + + border: 1px solid var(--color-line); + border-radius: 50%; + + background: transparent; + + color: var(--color-text); + + font-size: 20px; + line-height: 1; + + cursor: pointer; + + transition: + border-color 0.25s ease, + background 0.25s ease; +} + +.close:hover { + border-color: rgba(255, 255, 255, 0.4); + + background: rgba(255, 255, 255, 0.06); +} + +.title { + margin: 0; + + font-size: var(--fs-title); + font-weight: 700; + letter-spacing: -0.02em; +} + +.details { + color: var(--color-muted); + + font-size: var(--fs-body); + line-height: var(--lh-body); +} + +.metaList { + display: flex; + flex-direction: column; + + gap: var(--space-xs); + + margin: 0; + padding: var(--space-sm) 0 0; + + border-top: 1px solid var(--color-line); + + list-style: none; +} + +.metaList li { + display: flex; + align-items: baseline; + justify-content: space-between; + + gap: var(--space-md); + + font-size: var(--fs-small); +} + +.metaList span { + color: var(--color-faint); +} + +.metaList strong { + font-weight: 600; + text-align: right; +} + +.subheading { + margin: 0; + + color: var(--color-faint); + + font-size: var(--fs-overline); + letter-spacing: var(--ls-overline); + text-transform: uppercase; +} + +.features { + display: flex; + flex-direction: column; + + gap: var(--space-xs); + + margin: 0; + padding: 0; + + color: var(--color-muted); + + font-size: var(--fs-body); + + list-style: none; +} + +.features li { + position: relative; + + padding-left: 18px; +} + +.features li::before { + content: "—"; + + position: absolute; + left: 0; + + color: var(--color-faint); +} + +.actions { + display: flex; + flex-wrap: wrap; + + gap: var(--space-sm); + + padding-top: var(--space-sm); + + border-top: 1px solid var(--color-line); +} + +.actionButton, +.actionGhost { + display: inline-flex; + align-items: center; + + gap: var(--space-xs); + + padding: 10px 18px; + + border-radius: 100px; + + font-size: var(--fs-small); + font-weight: 600; + text-decoration: none; + + transition: + transform 0.25s ease, + background 0.25s ease, + border-color 0.25s ease; +} + +.actionButton { + background: #fff; + + color: #000; +} + +.actionButton:hover { + transform: translateY(-1px); +} + +.actionGhost { + border: 1px solid rgba(255, 255, 255, 0.2); + + color: var(--color-text); +} + +.actionGhost:hover { + border-color: rgba(255, 255, 255, 0.5); + + background: rgba(255, 255, 255, 0.06); +} \ No newline at end of file diff --git a/src/static/projects/project-card.module.css b/src/static/projects/project-card.module.css new file mode 100644 index 0000000..06386f6 --- /dev/null +++ b/src/static/projects/project-card.module.css @@ -0,0 +1,172 @@ +/* ========================= + PROJECT CARD +========================= */ + +.card { + position: relative; + + display: flex; + flex-direction: column; + + gap: var(--space-sm); + + height: 100%; + + padding: var(--space-lg); + + border: 1px solid var(--color-line); + border-radius: 20px; + + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.045), + rgba(255, 255, 255, 0.012) + ); + + overflow: hidden; + + cursor: pointer; + + transition: + border-color 0.35s ease, + background 0.35s ease, + transform 0.35s cubic-bezier(0.22, 1, 0.36, 1); +} + +.card::before { + content: ""; + + position: absolute; + inset: -40%; + + opacity: 0; + + background: radial-gradient( + circle at var(--mx, 50%) var(--my, 0%), + rgba(255, 255, 255, 0.09), + transparent 45% + ); + + pointer-events: none; + + transition: opacity 0.5s ease; +} + +.card:hover, +.card:focus-visible { + border-color: rgba(255, 255, 255, 0.22); + + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.06), + rgba(255, 255, 255, 0.015) + ); + + transform: translateY(-4px); +} + +.card:focus-visible { + outline: 2px solid rgba(255, 255, 255, 0.4); + + outline-offset: 2px; +} + +.card:hover::before { + opacity: 1; +} + +.cardIndex { + display: flex; + justify-content: space-between; + + color: var(--color-faint); + + font-size: var(--fs-caption); + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.cardTitle { + position: relative; + + margin: 0; + margin-top: var(--space-sm); + + font-size: var(--fs-title); + font-weight: 700; + letter-spacing: -0.02em; +} + +.cardDesc { + position: relative; + + margin: 0; + + color: var(--color-muted); + + font-size: var(--fs-body); + line-height: var(--lh-body); +} + +.tags { + position: relative; + + display: flex; + flex-wrap: wrap; + + gap: var(--space-xs); + + margin: 0; + margin-top: auto; + padding: var(--space-md) 0 0; + + list-style: none; +} + +.tag { + padding: 5px 12px; + + border: 1px solid var(--color-line); + border-radius: 100px; + + color: var(--color-muted); + + font-size: var(--fs-caption); +} + +.cardLink { + position: relative; + + display: inline-flex; + align-items: center; + + gap: 6px; + + margin-top: var(--space-sm); + + color: var(--color-text); + + font-size: var(--fs-caption); + letter-spacing: 0.14em; + text-transform: uppercase; + + opacity: 0.7; + + transition: opacity 0.3s ease; +} + +.card:hover .cardLink, +.card:focus-visible .cardLink { + opacity: 1; +} + +.cardLinkArrow { + display: inline-block; + + transition: transform 0.3s ease; +} + +.card:hover .cardLinkArrow, +.card:focus-visible .cardLinkArrow { + transform: translate(3px, -3px); +} \ No newline at end of file diff --git a/src/static/projects/projects-page.module.css b/src/static/projects/projects-page.module.css new file mode 100644 index 0000000..cf5af11 --- /dev/null +++ b/src/static/projects/projects-page.module.css @@ -0,0 +1,134 @@ +/* ========================= + PROJECTS PAGE +========================= */ + +.projectsPage { + position: relative; + overflow: hidden; + + padding-block: var(--section-y); +} + +.glow { + position: absolute; + top: 6%; + right: -180px; + + width: 480px; + height: 480px; + + border-radius: 50%; + + filter: blur(96px); + + background: rgba(255, 255, 255, 0.05); + + pointer-events: none; +} + +.ghost { + position: absolute; + top: 40px; + left: 1vw; + + z-index: 0; + + font-size: clamp(160px, 24vw, 460px); + font-weight: 800; + line-height: 1; + letter-spacing: -0.05em; + + color: transparent; + + -webkit-text-stroke: 1px rgba(255, 255, 255, 0.05); + + pointer-events: none; + + user-select: none; +} + +/* ========================= + HEAD +========================= */ + +.head { + position: relative; + z-index: 1; +} + +.eyebrow { + display: inline-flex; + align-items: center; + gap: var(--space-xs); + + color: var(--color-muted); + + font-size: var(--fs-overline); + font-weight: 500; + letter-spacing: var(--ls-overline); + text-transform: uppercase; +} + +.rule { + width: var(--space-lg); + height: 1px; + + background: var(--color-muted); +} + +.title { + margin-top: var(--space-md); + + font-size: var(--fs-display); + font-weight: 700; + line-height: var(--lh-tight); + letter-spacing: -0.02em; +} + +.titleAccent { + color: var(--color-faint); +} + +.count { + margin-top: var(--space-xs); + + color: var(--color-faint); + + font-size: var(--fs-body); +} + +/* ========================= + GRID +========================= */ + +.grid { + position: relative; + z-index: 1; + + display: grid; + grid-template-columns: repeat(3, 1fr); + + gap: var(--space-md); + + margin-top: var(--space-xl); +} + +/* ========================= + RESPONSIVE +========================= */ + +@media (max-width: 1024px) { + .grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 640px) { + .grid { + grid-template-columns: 1fr; + } + + .ghost { + left: -24px; + } +} \ No newline at end of file diff --git a/src/static/projects/projects.module.css b/src/static/projects/projects.module.css new file mode 100644 index 0000000..2aa9502 --- /dev/null +++ b/src/static/projects/projects.module.css @@ -0,0 +1,187 @@ +/* ========================= + PROJECTS +========================= */ + +.projects { + position: relative; + overflow: hidden; + + padding-block: var(--section-y); +} + +.glowA, +.glowB { + position: absolute; + + border-radius: 50%; + + filter: blur(96px); + + pointer-events: none; +} + +.glowA { + top: 10%; + right: -180px; + + width: 460px; + height: 460px; + + background: rgba(255, 255, 255, 0.05); +} + +.glowB { + bottom: 0; + left: -220px; + + width: 520px; + height: 520px; + + background: rgba(255, 255, 255, 0.04); +} + +.ghost { + position: absolute; + top: 30px; + left: 1vw; + + z-index: 0; + + font-size: clamp(160px, 24vw, 460px); + font-weight: 800; + line-height: 1; + letter-spacing: -0.05em; + + color: transparent; + + -webkit-text-stroke: 1px rgba(255, 255, 255, 0.05); + + pointer-events: none; + + user-select: none; +} + +/* ========================= + HEAD +========================= */ + +.head { + position: relative; + z-index: 1; + + display: flex; + align-items: flex-end; + justify-content: space-between; + + gap: var(--space-md); +} + +.eyebrow { + display: inline-flex; + align-items: center; + gap: var(--space-xs); + + color: var(--color-muted); + + font-size: var(--fs-overline); + font-weight: 500; + letter-spacing: var(--ls-overline); + text-transform: uppercase; +} + +.rule { + width: var(--space-lg); + height: 1px; + + background: var(--color-muted); +} + +.title { + margin-top: var(--space-md); + + max-width: 24em; + + font-size: var(--fs-display); + font-weight: 700; + line-height: var(--lh-tight); + letter-spacing: -0.02em; +} + +.titleAccent { + color: var(--color-faint); +} + +.viewAll { + display: inline-flex; + align-items: center; + gap: var(--space-xs); + + padding-bottom: 4px; + + border-bottom: 1px solid transparent; + + color: var(--color-muted); + + font-size: var(--fs-body); + font-weight: 500; + text-decoration: none; + + transition: + color 0.3s ease, + border-color 0.3s ease; +} + +.viewAll:hover { + color: var(--color-text); + + border-color: currentColor; +} + +.viewAllArrow { + display: inline-block; + + transition: transform 0.3s ease; +} + +.viewAll:hover .viewAllArrow { + transform: translate(3px, -3px); +} + +/* ========================= + GRID +========================= */ + +.grid { + position: relative; + z-index: 1; + + display: grid; + grid-template-columns: repeat(3, 1fr); + + gap: var(--space-md); + + margin-top: var(--space-xl); +} + +/* ========================= + RESPONSIVE +========================= */ + +@media (max-width: 1024px) { + .grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 640px) { + .grid { + grid-template-columns: 1fr; + } + + .head { + flex-direction: column; + align-items: flex-start; + + gap: var(--space-sm); + } +} \ No newline at end of file diff --git a/src/static/sponsors/sponsors.module.css b/src/static/sponsors/sponsors.module.css new file mode 100644 index 0000000..37e3f14 --- /dev/null +++ b/src/static/sponsors/sponsors.module.css @@ -0,0 +1,184 @@ +/* ========================= + SPONSORS / MARQUEE +========================= */ + +.section { + padding-top: var(--space-2xl); +} + +.label { + margin-bottom: var(--space-lg); + + text-align: center; + + color: var(--color-faint); + + font-size: var(--fs-overline); + font-weight: 500; + letter-spacing: var(--ls-overline); + text-transform: uppercase; +} + +.ticker { + position: relative; + + overflow: hidden; + + border-block: 1px solid var(--color-line); + + padding-block: var(--space-md); +} + +/* мягкое затухание по краям */ +.ticker::before, +.ticker::after { + content: ""; + + position: absolute; + top: 0; + bottom: 0; + + z-index: 1; + + width: clamp(24px, 5vw, 80px); + + pointer-events: none; +} + +.ticker::before { + left: 0; + + background: linear-gradient(90deg, #000, transparent); +} + +.ticker::after { + right: 0; + + background: linear-gradient(270deg, #000, transparent); +} + +.track { + display: flex; + + width: max-content; + + animation: marquee 34s linear infinite; +} + +.track:hover { + animation-play-state: paused; +} + +.group { + display: flex; + align-items: center; + + margin: 0; + padding: 0; + + list-style: none; +} + +.item { + display: flex; + align-items: center; + gap: var(--space-md); + + padding-inline: var(--space-lg); +} + +.item::after { + content: "◆"; + + color: rgba(255, 255, 255, 0.28); + + font-size: 10px; +} + +.word { + color: rgba(255, 255, 255, 0.55); + + font-size: clamp(18px, 1.8vw, 26px); + font-weight: 700; + + white-space: nowrap; +} + +/* разные «начертания» брендов */ +.nova { + font-style: italic; + font-weight: 800; + + color: rgba(255, 255, 255, 0.75); +} + +.lumina { + font-weight: 300; + letter-spacing: 0.5em; + text-transform: uppercase; + + color: rgba(255, 255, 255, 0.4); + + margin-right: -0.5em; +} + +.vertex { + font-weight: 900; + letter-spacing: 0.12em; + + color: rgba(255, 255, 255, 0.7); +} + +.orbit { + font-style: italic; + font-weight: 400; + + color: rgba(255, 255, 255, 0.45); +} + +.monolith { + font-weight: 700; + letter-spacing: 0.3em; + text-transform: uppercase; + + color: rgba(255, 255, 255, 0.5); +} + +.aster { + font-weight: 600; + + color: rgba(255, 255, 255, 0.65); +} + +.kite { + font-weight: 800; + text-transform: lowercase; + + color: rgba(255, 255, 255, 0.55); +} + +.pulse { + font-weight: 500; + letter-spacing: 0.22em; + text-transform: uppercase; + + color: #fff; +} + +@keyframes marquee { + to { + transform: translateX(-50%); + } +} + +@media (max-width: 640px) { + .item { + padding-inline: var(--space-md); + } +} + +@media (prefers-reduced-motion: reduce) { + .track { + animation: none; + } +} \ No newline at end of file diff --git a/src/static/testimonials/testimonials.module.css b/src/static/testimonials/testimonials.module.css new file mode 100644 index 0000000..57a41a0 --- /dev/null +++ b/src/static/testimonials/testimonials.module.css @@ -0,0 +1,254 @@ +/* ========================= + TESTIMONIALS +========================= */ + +.testimonials { + position: relative; + overflow: hidden; + + padding-block: var(--section-y); +} + +/* точечная текстура по фоновой сетке */ +.dots { + position: absolute; + inset: 0; + + z-index: 0; + + background-image: radial-gradient( + rgba(255, 255, 255, 0.06) 1px, + transparent 1.6px + ); + background-size: 26px 26px; + + -webkit-mask-image: radial-gradient( + ellipse 62% 72% at 50% 45%, + #000 0%, + transparent 78% + ); + + mask-image: radial-gradient( + ellipse 62% 72% at 50% 45%, + #000 0%, + transparent 78% + ); + + pointer-events: none; +} + +.glow { + position: absolute; + top: 30%; + left: -160px; + + width: 440px; + height: 440px; + + border-radius: 50%; + + filter: blur(96px); + + background: rgba(255, 255, 255, 0.04); + + pointer-events: none; +} + +.ghost { + position: absolute; + top: 24px; + right: 2vw; + + z-index: 0; + + font-size: clamp(160px, 24vw, 460px); + font-weight: 800; + line-height: 1; + letter-spacing: -0.05em; + + color: transparent; + + -webkit-text-stroke: 1px rgba(255, 255, 255, 0.05); + + pointer-events: none; + + user-select: none; +} + +/* ========================= + HEAD +========================= */ + +.eyebrow { + display: inline-flex; + align-items: center; + gap: var(--space-xs); + + color: var(--color-muted); + + font-size: var(--fs-overline); + font-weight: 500; + letter-spacing: var(--ls-overline); + text-transform: uppercase; +} + +.rule { + width: var(--space-lg); + height: 1px; + + background: var(--color-muted); +} + +.title { + margin-top: var(--space-md); + + max-width: 24em; + + font-size: var(--fs-display); + font-weight: 700; + line-height: var(--lh-tight); + letter-spacing: -0.02em; +} + +.titleAccent { + color: var(--color-faint); +} + +/* ========================= + GRID +========================= */ + +.grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + + gap: var(--space-md); + + margin-top: var(--space-xl); +} + +.card { + position: relative; + + display: flex; + flex-direction: column; + + gap: var(--space-sm); + + padding: var(--space-lg); + + border: 1px solid var(--color-line); + border-radius: 20px; + + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.04), + rgba(255, 255, 255, 0.012) + ); + + transition: + border-color 0.3s ease, + transform 0.3s cubic-bezier(0.22, 1, 0.36, 1); +} + +.card:hover { + border-color: rgba(255, 255, 255, 0.22); + + transform: translateY(-4px); +} + +.quote { + color: var(--color-faint); + + font-size: clamp(44px, 4vw, 60px); + font-weight: 800; + line-height: 0.55; +} + +.stars { + margin-top: var(--space-xs); + + color: #fff; + + font-size: var(--fs-small); + letter-spacing: 4px; +} + +.text { + flex: 1; + + color: var(--color-muted); + + font-size: var(--fs-body); + line-height: var(--lh-body); +} + +.author { + display: flex; + align-items: center; + + gap: var(--space-sm); + + margin-top: var(--space-sm); + padding-top: var(--space-sm); + + border-top: 1px solid var(--color-line); +} + +.avatar { + display: flex; + align-items: center; + justify-content: center; + + width: 44px; + height: 44px; + + flex-shrink: 0; + + border: 1px solid var(--color-line); + border-radius: 50%; + + background: rgba(255, 255, 255, 0.06); + + color: var(--color-text); + + font-size: var(--fs-small); + font-weight: 700; +} + +.authorName { + display: block; + + font-size: var(--fs-small); + font-weight: 600; +} + +.authorRole { + display: block; + + margin-top: 2px; + + color: var(--color-faint); + + font-size: var(--fs-caption); +} + +/* ========================= + RESPONSIVE +========================= */ + +@media (max-width: 1024px) { + .grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 640px) { + .grid { + grid-template-columns: 1fr; + } + + .ghost { + right: -30px; + } +} \ No newline at end of file diff --git a/src/views/home/ui/HomePage.tsx b/src/views/home/ui/HomePage.tsx index a8a05b2..a477602 100644 --- a/src/views/home/ui/HomePage.tsx +++ b/src/views/home/ui/HomePage.tsx @@ -1,13 +1,29 @@ import { Header } from "@/widgets/header"; +import { Footer } from "@/widgets/footer"; import Hero from "@/components/hero/hero"; +import About from "@/components/about/about"; +import Sponsors from "@/components/sponsors/sponsors"; +import Testimonials from "@/components/testimonials/testimonials"; +import Projects from "@/components/projects/projects"; const HomePage = () => { return ( <>
- +
+ + + + + + + + +
+ +