add page project and update styles
This commit is contained in:
parent
c6d8e54d89
commit
fcea711ddf
44 changed files with 3890 additions and 112 deletions
BIN
public/fonts/Montserrat/Montserrat-Italic-VariableFont_wght.ttf
Normal file
BIN
public/fonts/Montserrat/Montserrat-Italic-VariableFont_wght.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Montserrat/Montserrat-VariableFont_wght.ttf
Normal file
BIN
public/fonts/Montserrat/Montserrat-VariableFont_wght.ttf
Normal file
Binary file not shown.
|
|
@ -1,5 +1,8 @@
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
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";
|
import "@/static/globals.css";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
|
|
@ -23,7 +26,12 @@ const RootLayout = ({ children }: LayoutProps<"/">) => {
|
||||||
lang="en"
|
lang="en"
|
||||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||||
>
|
>
|
||||||
<body className="min-h-full flex flex-col">{children}</body>
|
<body className="min-h-full flex flex-col">
|
||||||
|
<Preloader />
|
||||||
|
<Cursor />
|
||||||
|
<ScrollCounter />
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
12
src/app/projects/page.tsx
Normal file
12
src/app/projects/page.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { ProjectsPage } from "@/views/projects";
|
||||||
|
|
||||||
|
const Projects = () => {
|
||||||
|
return <ProjectsPage />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "Проекты",
|
||||||
|
description: "Все проекты Egorov Nikita",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Projects;
|
||||||
109
src/components/about/about.tsx
Normal file
109
src/components/about/about.tsx
Normal file
|
|
@ -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<HTMLDivElement>(0.2);
|
||||||
|
const [skillsRef, skillsInView] = useInView<HTMLDivElement>(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 (
|
||||||
|
<section id="about" data-scroll-section className={styles.about}>
|
||||||
|
<span className={styles.glowA} aria-hidden="true" />
|
||||||
|
<span className={styles.glowB} aria-hidden="true" />
|
||||||
|
<span className={styles.ghost} aria-hidden="true">
|
||||||
|
02
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div ref={introRef} className="page-container relative z-10">
|
||||||
|
<p className={`${styles.eyebrow} ${reveal(introInView)}`}>
|
||||||
|
<span className={styles.rule} aria-hidden="true" />
|
||||||
|
обо мне
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2
|
||||||
|
className={`${styles.title} ${reveal(introInView)}`}
|
||||||
|
style={{ transitionDelay: "100ms" }}
|
||||||
|
>
|
||||||
|
Разработчик, для которого важны{" "}
|
||||||
|
<span className={styles.titleAccent}>продуманность</span> и детали
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p
|
||||||
|
className={`${styles.text} ${reveal(introInView)}`}
|
||||||
|
style={{ transitionDelay: "200ms" }}
|
||||||
|
>
|
||||||
|
Привет! Я Никита — веб-разработчик. Строю современные, быстрые и
|
||||||
|
аккуратные интерфейсы на React и Next.js. Уделяю внимание
|
||||||
|
производительности, семантике и мелочам, из которых складывается
|
||||||
|
хорошее впечатление от сайта.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<dl className={`${styles.stats} ${reveal(introInView)}`} style={{ transitionDelay: "300ms" }}>
|
||||||
|
<div>
|
||||||
|
<dt className={styles.statLabel}>опыт</dt>
|
||||||
|
<dd className={styles.statValue}>
|
||||||
|
3<span className={styles.statValueAccent}>+</span>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className={styles.statLabel}>проекты</dt>
|
||||||
|
<dd className={styles.statValue}>
|
||||||
|
10<span className={styles.statValueAccent}>+</span>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className={styles.statLabel}>стек</dt>
|
||||||
|
<dd className={styles.statValue}>8</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={skillsRef}
|
||||||
|
className={`${styles.skills} ${reveal(skillsInView)}`}
|
||||||
|
style={{ transitionDelay: "150ms" }}
|
||||||
|
>
|
||||||
|
<h3 className={styles.skillsTitle}>Стек и навыки</h3>
|
||||||
|
|
||||||
|
<ul className={styles.skillList}>
|
||||||
|
{techStack.map((item, index) => (
|
||||||
|
<li key={item.name}>
|
||||||
|
<p className={styles.skillName}>{item.name}</p>
|
||||||
|
<div
|
||||||
|
className={styles.skillBar}
|
||||||
|
style={{ transitionDelay: `${index * 70}ms` }}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={styles.skillFill}
|
||||||
|
style={{
|
||||||
|
width: skillsInView ? `${item.level}%` : "0%",
|
||||||
|
transitionDelay: `${index * 70}ms`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default About;
|
||||||
55
src/components/hero/FlashlightText.tsx
Normal file
55
src/components/hero/FlashlightText.tsx
Normal file
|
|
@ -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<HTMLSpanElement>(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<HTMLSpanElement>) => {
|
||||||
|
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 (
|
||||||
|
<span
|
||||||
|
ref={ref}
|
||||||
|
className={styles.root}
|
||||||
|
onMouseMove={handleMove}
|
||||||
|
onMouseLeave={handleLeave}
|
||||||
|
style={{ "--fx": `${OFFSCREEN}px`, "--fy": `${OFFSCREEN}px` } as CSSProperties}
|
||||||
|
>
|
||||||
|
<span className={styles.base} aria-hidden="true">
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
<span className={styles.highlight}>{text}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FlashlightText;
|
||||||
|
|
@ -1,20 +1,96 @@
|
||||||
import { sociallinks } from "@/shared/config/social-links/links";
|
import { sociallinks } from "@/shared/config/social-links/links";
|
||||||
import styles from "@/static/hero/hero.module.css";
|
import styles from "@/static/hero/hero.module.css";
|
||||||
|
|
||||||
|
import FlashlightText from "./FlashlightText";
|
||||||
|
|
||||||
|
const socialIcon = (label: string) => {
|
||||||
|
switch (label.trim().toLowerCase()) {
|
||||||
|
case "telegram":
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
case "github":
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A5.98 5.98 0 0 0 16 8c0-4.42-3.58-8-8-8z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const Hero = () => {
|
const Hero = () => {
|
||||||
return (
|
return (
|
||||||
<section className={styles.hero}>
|
<section className={styles.hero} data-scroll-section>
|
||||||
<div className="cont-header">
|
<div className={`cont-header ${styles.inner}`}>
|
||||||
<div className={styles.left}>
|
<div className={styles.left}>
|
||||||
<h1>left</h1>
|
<div className={styles.info}>
|
||||||
|
<span className={styles.role}>
|
||||||
|
<span className={styles.dot} />
|
||||||
|
веб-разработчик
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<h1 className={styles.name} data-cursor-hover>
|
||||||
|
<FlashlightText text="Egorov Nikita" />
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className={styles.desc}>
|
||||||
|
Создаю современные, быстрые и продуманные веб-сайты и
|
||||||
|
веб-приложения под ключ.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<a href="#projects" className={styles.button}>
|
||||||
|
Смотреть проекты
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<ul className={styles.socials}>
|
||||||
|
{sociallinks.map((link) => (
|
||||||
|
<li key={link.href}>
|
||||||
|
<a
|
||||||
|
href={link.href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className={styles.social}
|
||||||
|
aria-label={link.label}
|
||||||
|
>
|
||||||
|
{socialIcon(link.label)}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.right}>
|
<div className={styles.right}>
|
||||||
<div className={styles.visual}>
|
<div className={styles.visual}>
|
||||||
<span>01</span>
|
<span className={styles.index} aria-hidden="true">
|
||||||
|
<i>портфолио</i>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.hint} aria-hidden="true">
|
||||||
|
листай
|
||||||
|
<span className={styles.hintLine} />
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
75
src/components/projects/ProjectCard.tsx
Normal file
75
src/components/projects/ProjectCard.tsx
Normal file
|
|
@ -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<HTMLElement>) => {
|
||||||
|
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<HTMLElement>) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
onOpen(project);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
className={styles.card}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
data-cursor-hover
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
aria-label={`${project.title}: подробнее`}
|
||||||
|
onClick={() => onOpen(project)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onMouseMove={handleSpotlight}
|
||||||
|
>
|
||||||
|
<div className={styles.cardIndex}>
|
||||||
|
<span>{project.id}</span>
|
||||||
|
<span>{project.year}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className={styles.cardTitle}>{project.title}</h3>
|
||||||
|
|
||||||
|
<p className={styles.cardDesc}>{project.description}</p>
|
||||||
|
|
||||||
|
<ul className={styles.tags}>
|
||||||
|
{project.tags.map((tag) => (
|
||||||
|
<li key={tag} className={styles.tag}>
|
||||||
|
{tag}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<span className={styles.cardLink}>
|
||||||
|
подробнее
|
||||||
|
<span className={styles.cardLinkArrow} aria-hidden="true">
|
||||||
|
↗
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectCard;
|
||||||
73
src/components/projects/projects.tsx
Normal file
73
src/components/projects/projects.tsx
Normal file
|
|
@ -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<HTMLDivElement>(0.12);
|
||||||
|
const [selected, setSelected] = useState<Project | null>(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 (
|
||||||
|
<section id="projects" className={styles.projects} data-scroll-section>
|
||||||
|
<span className={styles.glowA} aria-hidden="true" />
|
||||||
|
<span className={styles.glowB} aria-hidden="true" />
|
||||||
|
<span className={styles.ghost} aria-hidden="true">
|
||||||
|
03
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="page-container relative z-10">
|
||||||
|
<div className={styles.head}>
|
||||||
|
<div>
|
||||||
|
<p className={styles.eyebrow}>
|
||||||
|
<span className={styles.rule} aria-hidden="true" />
|
||||||
|
работы
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 className={styles.title}>
|
||||||
|
Избранные <span className={styles.titleAccent}>проекты</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link href="/projects" className={styles.viewAll}>
|
||||||
|
все проекты
|
||||||
|
<span className={styles.viewAllArrow} aria-hidden="true">
|
||||||
|
↗
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref={gridRef} className={styles.grid}>
|
||||||
|
{preview.map((project, index) => (
|
||||||
|
<div
|
||||||
|
key={project.id}
|
||||||
|
className={reveal(gridInView)}
|
||||||
|
style={{ transitionDelay: `${index * 80}ms` }}
|
||||||
|
>
|
||||||
|
<ProjectCard project={project} onOpen={setSelected} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ProjectModal project={selected} onClose={() => setSelected(null)} />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectsSection;
|
||||||
43
src/components/sponsors/sponsors.tsx
Normal file
43
src/components/sponsors/sponsors.tsx
Normal file
|
|
@ -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 }) => (
|
||||||
|
<ul className={styles.group} aria-hidden={hidden || undefined}>
|
||||||
|
{sponsors.map((sponsor) => (
|
||||||
|
<li key={sponsor.name} className={styles.item}>
|
||||||
|
<span className={`${styles.word} ${sponsor.className}`}>
|
||||||
|
{sponsor.name}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Sponsors = () => {
|
||||||
|
return (
|
||||||
|
<section className={styles.section} aria-label="Партнёры и бренды">
|
||||||
|
<div className="page-container">
|
||||||
|
<p className={styles.label}>С ними я работал</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.ticker}>
|
||||||
|
<div className={styles.track}>
|
||||||
|
<List />
|
||||||
|
<List hidden />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Sponsors;
|
||||||
93
src/components/testimonials/testimonials.tsx
Normal file
93
src/components/testimonials/testimonials.tsx
Normal file
|
|
@ -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<HTMLDivElement>(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 (
|
||||||
|
<section className={styles.testimonials} data-scroll-section>
|
||||||
|
<span className={styles.dots} aria-hidden="true" />
|
||||||
|
<span className={styles.glow} aria-hidden="true" />
|
||||||
|
<span className={styles.ghost} aria-hidden="true">
|
||||||
|
04
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="page-container relative z-10">
|
||||||
|
<p className={styles.eyebrow}>
|
||||||
|
<span className={styles.rule} aria-hidden="true" />
|
||||||
|
отзывы
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 className={styles.title}>
|
||||||
|
Что обо мне <span className={styles.titleAccent}>говорят</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div ref={gridRef} className={styles.grid}>
|
||||||
|
{testimonials.map((item, index) => (
|
||||||
|
<article
|
||||||
|
key={item.name}
|
||||||
|
className={`${styles.card} ${reveal(gridInView)}`}
|
||||||
|
style={{ transitionDelay: `${index * 90}ms` }}
|
||||||
|
>
|
||||||
|
<p className={styles.quote} aria-hidden="true">
|
||||||
|
“
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className={styles.stars} aria-label="Оценка 5 из 5">
|
||||||
|
★★★★★
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className={styles.text}>{item.quote}</p>
|
||||||
|
|
||||||
|
<footer className={styles.author}>
|
||||||
|
<span className={styles.avatar} aria-hidden="true">
|
||||||
|
{initials(item.name)}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<span className={styles.authorName}>{item.name}</span>
|
||||||
|
<span className={styles.authorRole}>{item.role}</span>
|
||||||
|
</span>
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Testimonials;
|
||||||
|
|
@ -3,10 +3,14 @@ import type { NavigationItem } from "./types";
|
||||||
export const navigation: NavigationItem[] = [
|
export const navigation: NavigationItem[] = [
|
||||||
{
|
{
|
||||||
label: "Обо мне",
|
label: "Обо мне",
|
||||||
href: "#about",
|
href: "/#about",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Проекты",
|
label: "Проекты",
|
||||||
href: "#projects",
|
href: "/projects",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Контакты",
|
||||||
|
href: "/#contact",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
157
src/shared/config/projects/projects.ts
Normal file
157
src/shared/config/projects/projects.ts
Normal file
|
|
@ -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",
|
||||||
|
},
|
||||||
|
];
|
||||||
11
src/shared/config/projects/types.ts
Normal file
11
src/shared/config/projects/types.ts
Normal file
|
|
@ -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;
|
||||||
|
};
|
||||||
27
src/shared/hooks/useInView.ts
Normal file
27
src/shared/hooks/useInView.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
export const useInView = <T extends HTMLElement>(threshold = 0.2) => {
|
||||||
|
const ref = useRef<T>(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;
|
||||||
|
};
|
||||||
230
src/static/about/about.module.css
Normal file
230
src/static/about/about.module.css
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
114
src/static/cursor/cursor.module.css
Normal file
114
src/static/cursor/cursor.module.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
211
src/static/footer/footer.module.css
Normal file
211
src/static/footer/footer.module.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,15 @@
|
||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
FONTS
|
||||||
|
========================= */
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Montserrat";
|
font-family: "Montserrat";
|
||||||
src: url("/fonts/Montserrat/Montserrat-VariableFont_wght.ttf") format("truetype");
|
src: url("/fonts/Montserrat/Montserrat-VariableFont_wght.ttf") format("truetype");
|
||||||
|
|
@ -14,27 +26,136 @@
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
DESIGN TOKENS
|
||||||
|
========================= */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
/* Font */
|
/* Font */
|
||||||
--font-main: "Montserrat", sans-serif;
|
--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,
|
html,
|
||||||
body {
|
body {
|
||||||
font-family: var(--font-main);
|
font-family: var(--font-main);
|
||||||
|
font-size: var(--fs-base);
|
||||||
background-color: var(--color-bg);
|
background-color: var(--color-bg);
|
||||||
color: var(--color-text);
|
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 {
|
.cont-header {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
max-width: var(--container-max);
|
||||||
padding: 0 var(--container-padding, 40px);
|
margin-inline: auto;
|
||||||
|
padding-inline: var(--container-padding);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: var(--container-gap);
|
||||||
}
|
}
|
||||||
|
|
@ -1,16 +1,25 @@
|
||||||
|
/* =========================
|
||||||
|
HEADER
|
||||||
|
========================= */
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
position: relative;
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 90px;
|
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 {
|
.logo {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
color: #fff;
|
color: var(--color-text);
|
||||||
|
|
||||||
font-size: clamp(28px, 2vw, 38px);
|
font-size: var(--fs-logo);
|
||||||
line-height: 1;
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
letter-spacing: -0.06em;
|
letter-spacing: -0.06em;
|
||||||
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
@ -41,7 +50,6 @@
|
||||||
opacity: 0.65;
|
opacity: 0.65;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
RIGHT
|
RIGHT
|
||||||
========================= */
|
========================= */
|
||||||
|
|
@ -49,11 +57,9 @@
|
||||||
.right {
|
.right {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: var(--space-md);
|
||||||
gap: clamp(24px, 3vw, 55px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
DESKTOP NAV
|
DESKTOP NAV
|
||||||
========================= */
|
========================= */
|
||||||
|
|
@ -62,7 +68,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
gap: clamp(24px, 2.8vw, 55px);
|
gap: var(--space-lg);
|
||||||
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
@ -82,9 +88,9 @@
|
||||||
|
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
|
|
||||||
color: #777;
|
color: var(--color-muted);
|
||||||
|
|
||||||
font-size: clamp(15px, 1vw, 20px);
|
font-size: var(--fs-nav);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
|
|
||||||
|
|
@ -103,20 +109,19 @@
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
|
|
||||||
background: #fff;
|
background: var(--color-text);
|
||||||
|
|
||||||
transition: width 0.3s ease;
|
transition: width 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.desktopNav a:hover {
|
.desktopNav a:hover {
|
||||||
color: #fff;
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.desktopNav a:hover::after {
|
.desktopNav a:hover::after {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
HAMBURGER
|
HAMBURGER
|
||||||
========================= */
|
========================= */
|
||||||
|
|
@ -147,7 +152,7 @@
|
||||||
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
|
||||||
background: #fff;
|
background: var(--color-text);
|
||||||
|
|
||||||
transition:
|
transition:
|
||||||
transform 0.3s ease,
|
transform 0.3s ease,
|
||||||
|
|
@ -167,6 +172,18 @@
|
||||||
bottom: 0;
|
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
|
MOBILE MENU
|
||||||
|
|
@ -180,9 +197,9 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100vh - 90px);
|
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%);
|
transform: translateX(100%);
|
||||||
|
|
||||||
|
|
@ -199,7 +216,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
gap: 30px;
|
gap: var(--space-md);
|
||||||
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
@ -215,12 +232,11 @@
|
||||||
.mobileMenu a {
|
.mobileMenu a {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
color: #fff;
|
color: var(--color-text);
|
||||||
|
|
||||||
font-size: clamp(30px, 6vw, 52px);
|
|
||||||
line-height: 1;
|
|
||||||
|
|
||||||
|
font-size: var(--fs-display);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
letter-spacing: -0.04em;
|
letter-spacing: -0.04em;
|
||||||
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
@ -229,10 +245,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobileMenu a:hover {
|
.mobileMenu a:hover {
|
||||||
color: #777;
|
color: var(--color-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
LARGE SCREENS
|
LARGE SCREENS
|
||||||
========================= */
|
========================= */
|
||||||
|
|
@ -241,21 +256,8 @@
|
||||||
.header {
|
.header {
|
||||||
height: 110px;
|
height: 110px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
|
||||||
font-size: 42px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desktopNav a {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desktopNav ul {
|
|
||||||
gap: 50px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
TABLET
|
TABLET
|
||||||
========================= */
|
========================= */
|
||||||
|
|
@ -265,20 +267,11 @@
|
||||||
height: 80px;
|
height: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
|
||||||
font-size: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desktopNav ul {
|
.desktopNav ul {
|
||||||
gap: 24px;
|
gap: var(--space-md);
|
||||||
}
|
|
||||||
|
|
||||||
.desktopNav a {
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
MOBILE
|
MOBILE
|
||||||
========================= */
|
========================= */
|
||||||
|
|
@ -300,13 +293,8 @@
|
||||||
top: 70px;
|
top: 70px;
|
||||||
height: calc(100vh - 70px);
|
height: calc(100vh - 70px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.right {
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
SMALL MOBILE
|
SMALL MOBILE
|
||||||
========================= */
|
========================= */
|
||||||
|
|
@ -329,14 +317,6 @@
|
||||||
top: 64px;
|
top: 64px;
|
||||||
height: calc(100vh - 64px);
|
height: calc(100vh - 64px);
|
||||||
|
|
||||||
padding-top: 45px;
|
padding-top: var(--space-lg);
|
||||||
}
|
|
||||||
|
|
||||||
.mobileMenu ul {
|
|
||||||
gap: 26px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobileMenu a {
|
|
||||||
font-size: 32px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
64
src/static/hero/flashlight.module.css
Normal file
64
src/static/hero/flashlight.module.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,413 @@
|
||||||
|
/* =========================
|
||||||
|
HERO
|
||||||
|
========================= */
|
||||||
|
|
||||||
.hero {
|
.hero {
|
||||||
background-color: aquamarine;
|
position: relative;
|
||||||
height: calc(100vh - 90px);
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
min-height: calc(100vh - 90px);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right {
|
/* subtle grid */
|
||||||
width: 49%;
|
.hero::before {
|
||||||
height: 90vh;
|
content: "";
|
||||||
background-color: red;
|
|
||||||
|
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: 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
117
src/static/preloader/preloader.module.css
Normal file
117
src/static/preloader/preloader.module.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
281
src/static/project-modal/project-modal.module.css
Normal file
281
src/static/project-modal/project-modal.module.css
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
172
src/static/projects/project-card.module.css
Normal file
172
src/static/projects/project-card.module.css
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
134
src/static/projects/projects-page.module.css
Normal file
134
src/static/projects/projects-page.module.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
187
src/static/projects/projects.module.css
Normal file
187
src/static/projects/projects.module.css
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
184
src/static/sponsors/sponsors.module.css
Normal file
184
src/static/sponsors/sponsors.module.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
254
src/static/testimonials/testimonials.module.css
Normal file
254
src/static/testimonials/testimonials.module.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,29 @@
|
||||||
import { Header } from "@/widgets/header";
|
import { Header } from "@/widgets/header";
|
||||||
|
import { Footer } from "@/widgets/footer";
|
||||||
import Hero from "@/components/hero/hero";
|
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 = () => {
|
const HomePage = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
|
<main className="relative flex flex-col">
|
||||||
<Hero />
|
<Hero />
|
||||||
|
|
||||||
|
<About />
|
||||||
|
|
||||||
|
<Sponsors />
|
||||||
|
|
||||||
|
<Testimonials />
|
||||||
|
|
||||||
|
<Projects />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
1
src/views/projects/index.ts
Normal file
1
src/views/projects/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as ProjectsPage } from "./ui/ProjectsPage";
|
||||||
73
src/views/projects/ui/ProjectsPage.tsx
Normal file
73
src/views/projects/ui/ProjectsPage.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { Header } from "@/widgets/header";
|
||||||
|
import { Footer } from "@/widgets/footer";
|
||||||
|
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 ProjectCard from "@/components/projects/ProjectCard";
|
||||||
|
import styles from "@/static/projects/projects-page.module.css";
|
||||||
|
|
||||||
|
const ProjectsPage = () => {
|
||||||
|
const [selected, setSelected] = useState<Project | null>(null);
|
||||||
|
const [headRef, headInView] = useInView<HTMLDivElement>(0.4);
|
||||||
|
const [gridRef, gridInView] = useInView<HTMLDivElement>(0.08);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<main className="relative flex flex-col">
|
||||||
|
<section className={styles.projectsPage} data-scroll-section>
|
||||||
|
<span className={styles.glow} aria-hidden="true" />
|
||||||
|
<span className={styles.ghost} aria-hidden="true">
|
||||||
|
{String(projects.length).padStart(2, "0")}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="page-container relative z-10">
|
||||||
|
<div ref={headRef} className={styles.head}>
|
||||||
|
<p className={`${styles.eyebrow} ${reveal(headInView)}`}>
|
||||||
|
<span className={styles.rule} aria-hidden="true" />
|
||||||
|
проекты
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h1 className={`${styles.title} ${reveal(headInView)}`}>
|
||||||
|
Все <span className={styles.titleAccent}>работы</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className={`${styles.count} ${reveal(headInView)}`}>
|
||||||
|
{String(projects.length).padStart(2, "0")} проектов под ключ
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref={gridRef} className={styles.grid}>
|
||||||
|
{projects.map((project, index) => (
|
||||||
|
<div
|
||||||
|
key={project.id}
|
||||||
|
className={reveal(gridInView)}
|
||||||
|
style={{ transitionDelay: `${index * 60}ms` }}
|
||||||
|
>
|
||||||
|
<ProjectCard project={project} onOpen={setSelected} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ProjectModal project={selected} onClose={() => setSelected(null)} />
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectsPage;
|
||||||
1
src/widgets/cursor/index.ts
Normal file
1
src/widgets/cursor/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as Cursor } from "./ui/Cursor";
|
||||||
97
src/widgets/cursor/ui/Cursor.tsx
Normal file
97
src/widgets/cursor/ui/Cursor.tsx
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import styles from "@/static/cursor/cursor.module.css";
|
||||||
|
|
||||||
|
const Cursor = () => {
|
||||||
|
const dotRef = useRef<HTMLDivElement>(null);
|
||||||
|
const ringRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [hovering, setHovering] = useState(false);
|
||||||
|
const [pressed, setPressed] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const finePointer = window.matchMedia("(pointer: fine)").matches;
|
||||||
|
|
||||||
|
if (!finePointer) return;
|
||||||
|
|
||||||
|
document.documentElement.classList.add("has-cursor");
|
||||||
|
|
||||||
|
let raf = 0;
|
||||||
|
let mouseX = window.innerWidth / 2;
|
||||||
|
let mouseY = window.innerHeight / 2;
|
||||||
|
let ringX = mouseX;
|
||||||
|
let ringY = mouseY;
|
||||||
|
|
||||||
|
const onPointerMove = (e: PointerEvent) => {
|
||||||
|
mouseX = e.clientX;
|
||||||
|
mouseY = e.clientY;
|
||||||
|
|
||||||
|
if (dotRef.current) {
|
||||||
|
dotRef.current.style.transform = `translate3d(${mouseX}px, ${mouseY}px, 0)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
setVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerOver = (e: PointerEvent) => {
|
||||||
|
const target = e.target as HTMLElement | null;
|
||||||
|
|
||||||
|
const interactive = target?.closest(
|
||||||
|
"a, button, [role='button'], input, select, textarea, [data-cursor-hover]"
|
||||||
|
);
|
||||||
|
|
||||||
|
setHovering(Boolean(interactive));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerDown = () => setPressed(true);
|
||||||
|
const onPointerUp = () => setPressed(false);
|
||||||
|
|
||||||
|
const loop = () => {
|
||||||
|
ringX += (mouseX - ringX) * 0.18;
|
||||||
|
ringY += (mouseY - ringY) * 0.18;
|
||||||
|
|
||||||
|
if (ringRef.current) {
|
||||||
|
ringRef.current.style.transform = `translate3d(${ringX}px, ${ringY}px, 0)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
raf = requestAnimationFrame(loop);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("pointermove", onPointerMove, { passive: true });
|
||||||
|
window.addEventListener("pointerover", onPointerOver, { passive: true });
|
||||||
|
window.addEventListener("pointerdown", onPointerDown);
|
||||||
|
window.addEventListener("pointerup", onPointerUp);
|
||||||
|
|
||||||
|
raf = requestAnimationFrame(loop);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(raf);
|
||||||
|
window.removeEventListener("pointermove", onPointerMove);
|
||||||
|
window.removeEventListener("pointerover", onPointerOver);
|
||||||
|
window.removeEventListener("pointerdown", onPointerDown);
|
||||||
|
window.removeEventListener("pointerup", onPointerUp);
|
||||||
|
document.documentElement.classList.remove("has-cursor");
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const className = [
|
||||||
|
styles.cursor,
|
||||||
|
visible && styles.visible,
|
||||||
|
hovering && styles.hovering,
|
||||||
|
pressed && styles.pressed,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className} aria-hidden="true">
|
||||||
|
<div ref={dotRef} className={styles.dot} />
|
||||||
|
<div ref={ringRef} className={styles.ring} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Cursor;
|
||||||
1
src/widgets/footer/index.ts
Normal file
1
src/widgets/footer/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as Footer } from "./ui/Footer";
|
||||||
81
src/widgets/footer/ui/Footer.tsx
Normal file
81
src/widgets/footer/ui/Footer.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { navigation } from "@/shared/config/navigation/navigation";
|
||||||
|
import { sociallinks } from "@/shared/config/social-links/links";
|
||||||
|
import styles from "@/static/footer/footer.module.css";
|
||||||
|
|
||||||
|
const Footer = () => {
|
||||||
|
const telegram = sociallinks.find(
|
||||||
|
(link) => link.label.toLowerCase() === "telegram"
|
||||||
|
);
|
||||||
|
const github = sociallinks.find(
|
||||||
|
(link) => link.label.toLowerCase() === "github"
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer id="contact" className={styles.footer} data-scroll-section>
|
||||||
|
<span className={styles.watermark} aria-hidden="true">
|
||||||
|
Autli
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="page-container">
|
||||||
|
<div className={styles.cta}>
|
||||||
|
<p className={styles.eyebrow}>контакты</p>
|
||||||
|
|
||||||
|
<h2 className={styles.title}>
|
||||||
|
Есть проект?
|
||||||
|
<br />
|
||||||
|
<span className={styles.titleAccent}>Напиши мне</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className={styles.text}>
|
||||||
|
Отвечаю обычно в течение дня. Расскажи, что хочешь сделать, — я
|
||||||
|
помогу с идеей, оценкой и реализацией.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<a
|
||||||
|
href={telegram?.href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className={styles.button}
|
||||||
|
>
|
||||||
|
Написать в Telegram
|
||||||
|
<span aria-hidden="true">↗</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={github?.href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className={styles.buttonGhost}
|
||||||
|
>
|
||||||
|
GitHub
|
||||||
|
<span aria-hidden="true">↗</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.bottom}>
|
||||||
|
<span className={styles.wordmark}>Autli</span>
|
||||||
|
|
||||||
|
<nav className={styles.nav} aria-label="Навигация в подвале">
|
||||||
|
<ul>
|
||||||
|
{navigation.map((item) => (
|
||||||
|
<li key={item.href}>
|
||||||
|
<Link href={item.href}>{item.label}</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<p className={styles.copy}>
|
||||||
|
© {new Date().getFullYear()} Egorov Nikita
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
|
|
@ -1,14 +1,45 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
import { navigation } from "@/shared/config/navigation/navigation";
|
import { navigation } from "@/shared/config/navigation/navigation";
|
||||||
import styles from "@/static/header/header.module.css";
|
import styles from "@/static/header/header.module.css";
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResize = () => {
|
||||||
|
if (window.innerWidth > 768) setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.documentElement.classList.toggle("no-scroll", open);
|
||||||
|
|
||||||
|
window.addEventListener("keydown", onKeyDown);
|
||||||
|
window.addEventListener("resize", onResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.documentElement.classList.remove("no-scroll");
|
||||||
|
window.removeEventListener("keydown", onKeyDown);
|
||||||
|
window.removeEventListener("resize", onResize);
|
||||||
|
};
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const close = () => setOpen(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<div className="cont-header">
|
<div className="cont-header">
|
||||||
<div className={styles.left}>
|
<div className={styles.left}>
|
||||||
<a href="/" className={styles.logo}>
|
<Link href="/" className={styles.logo} onClick={close}>
|
||||||
Autli
|
Autli
|
||||||
</a>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.right}>
|
<div className={styles.right}>
|
||||||
|
|
@ -16,39 +47,19 @@ const Header = () => {
|
||||||
<ul>
|
<ul>
|
||||||
{navigation.map((item) => (
|
{navigation.map((item) => (
|
||||||
<li key={item.href}>
|
<li key={item.href}>
|
||||||
<a href={item.href}>
|
<Link href={item.href}>{item.label}</Link>
|
||||||
{item.label}
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Переключатель языка */}
|
|
||||||
<a
|
|
||||||
href="/en/"
|
|
||||||
className={styles["language-toggle"]}
|
|
||||||
aria-label="Переключить язык"
|
|
||||||
>
|
|
||||||
EN
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{/* Переключатель темы */}
|
|
||||||
<button
|
|
||||||
className={styles["theme-toggle"]}
|
|
||||||
id="theme-toggle"
|
|
||||||
type="button"
|
|
||||||
aria-label="Переключить тему"
|
|
||||||
>
|
|
||||||
<span className={styles["theme-icon"]}>☾</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Гамбургер */}
|
{/* Гамбургер */}
|
||||||
<button
|
<button
|
||||||
className={styles.hamburger}
|
className={`${styles.hamburger} ${open ? styles.active : ""}`}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Открыть меню"
|
aria-label={open ? "Закрыть меню" : "Открыть меню"}
|
||||||
aria-expanded="false"
|
aria-expanded={open}
|
||||||
|
onClick={() => setOpen((value) => !value)}
|
||||||
>
|
>
|
||||||
<span />
|
<span />
|
||||||
<span />
|
<span />
|
||||||
|
|
@ -57,13 +68,16 @@ const Header = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className={styles.mobileMenu} aria-label="Мобильная навигация">
|
<nav
|
||||||
|
className={`${styles.mobileMenu} ${open ? styles.active : ""}`}
|
||||||
|
aria-label="Мобильная навигация"
|
||||||
|
>
|
||||||
<ul>
|
<ul>
|
||||||
{navigation.map((item) => (
|
{navigation.map((item) => (
|
||||||
<li key={item.href}>
|
<li key={item.href}>
|
||||||
<a href={item.href}>
|
<Link href={item.href} onClick={close}>
|
||||||
{item.label}
|
{item.label}
|
||||||
</a>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
1
src/widgets/preloader/index.ts
Normal file
1
src/widgets/preloader/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as Preloader } from "./ui/Preloader";
|
||||||
87
src/widgets/preloader/ui/Preloader.tsx
Normal file
87
src/widgets/preloader/ui/Preloader.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import styles from "@/static/preloader/preloader.module.css";
|
||||||
|
|
||||||
|
const Preloader = () => {
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
const [done, setDone] = useState(false);
|
||||||
|
const [hidden, setHidden] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.classList.add("no-scroll");
|
||||||
|
|
||||||
|
const duration = 1500;
|
||||||
|
const start = performance.now();
|
||||||
|
let raf = 0;
|
||||||
|
|
||||||
|
const tick = (now: number) => {
|
||||||
|
const value = Math.min(100, Math.round(((now - start) / duration) * 100));
|
||||||
|
setProgress(value);
|
||||||
|
|
||||||
|
if (value < 100) {
|
||||||
|
raf = requestAnimationFrame(tick);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
raf = requestAnimationFrame(tick);
|
||||||
|
|
||||||
|
const finish = () => {
|
||||||
|
cancelAnimationFrame(raf);
|
||||||
|
setProgress(100);
|
||||||
|
document.documentElement.classList.remove("no-scroll");
|
||||||
|
window.setTimeout(() => setDone(true), 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (document.readyState === "complete") {
|
||||||
|
window.setTimeout(finish, duration + 200);
|
||||||
|
} else {
|
||||||
|
window.addEventListener("load", finish);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(raf);
|
||||||
|
window.removeEventListener("load", finish);
|
||||||
|
document.documentElement.classList.remove("no-scroll");
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!done) return;
|
||||||
|
|
||||||
|
const timer = window.setTimeout(() => setHidden(true), 700);
|
||||||
|
|
||||||
|
return () => window.clearTimeout(timer);
|
||||||
|
}, [done]);
|
||||||
|
|
||||||
|
if (hidden) return null;
|
||||||
|
|
||||||
|
const className = [styles.preloader, done && styles.done]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className} aria-hidden="true">
|
||||||
|
<div className={styles.content}>
|
||||||
|
<p className={styles.logo}>
|
||||||
|
Autli<span className={styles.accent}>.</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className={styles.bar}>
|
||||||
|
<span
|
||||||
|
className={styles.barFill}
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.meta}>
|
||||||
|
<span className={styles.status}>загрузка</span>
|
||||||
|
<span className={styles.percent}>{progress}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Preloader;
|
||||||
1
src/widgets/project-modal/index.ts
Normal file
1
src/widgets/project-modal/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as ProjectModal } from "./ui/ProjectModal";
|
||||||
114
src/widgets/project-modal/ui/ProjectModal.tsx
Normal file
114
src/widgets/project-modal/ui/ProjectModal.tsx
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import type { Project } from "@/shared/config/projects/types";
|
||||||
|
import styles from "@/static/project-modal/project-modal.module.css";
|
||||||
|
|
||||||
|
type ProjectModalProps = {
|
||||||
|
project: Project | null;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectModal = ({ project, onClose }: ProjectModalProps) => {
|
||||||
|
const panelRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!project) return;
|
||||||
|
|
||||||
|
document.documentElement.classList.add("no-scroll");
|
||||||
|
panelRef.current?.focus();
|
||||||
|
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", onKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.documentElement.classList.remove("no-scroll");
|
||||||
|
window.removeEventListener("keydown", onKeyDown);
|
||||||
|
};
|
||||||
|
}, [project, onClose]);
|
||||||
|
|
||||||
|
if (!project) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.overlay}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label={`Проект ${project.title}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div ref={panelRef} className={styles.panel} tabIndex={-1}>
|
||||||
|
<div className={styles.wrap}>
|
||||||
|
<header className={styles.head}>
|
||||||
|
<span className={styles.meta}>Проект {project.id}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.close}
|
||||||
|
aria-label="Закрыть"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<h3 className={styles.title}>{project.title}</h3>
|
||||||
|
|
||||||
|
<p className={styles.details}>{project.details}</p>
|
||||||
|
|
||||||
|
<ul className={styles.metaList}>
|
||||||
|
<li>
|
||||||
|
<span>Год</span>
|
||||||
|
<strong>{project.year}</strong>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>Стек</span>
|
||||||
|
<strong>{project.tags.join(" · ")}</strong>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4 className={styles.subheading}>Что внутри</h4>
|
||||||
|
|
||||||
|
<ul className={styles.features}>
|
||||||
|
{project.features.map((feature) => (
|
||||||
|
<li key={feature}>{feature}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<footer className={styles.actions}>
|
||||||
|
{project.demo ? (
|
||||||
|
<a
|
||||||
|
href={project.demo}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className={styles.actionButton}
|
||||||
|
>
|
||||||
|
Живая версия
|
||||||
|
<span aria-hidden="true">↗</span>
|
||||||
|
</a>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{project.code ? (
|
||||||
|
<a
|
||||||
|
href={project.code}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className={styles.actionGhost}
|
||||||
|
>
|
||||||
|
Код проекта
|
||||||
|
<span aria-hidden="true">↗</span>
|
||||||
|
</a>
|
||||||
|
) : null}
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectModal;
|
||||||
1
src/widgets/scroll-counter/index.ts
Normal file
1
src/widgets/scroll-counter/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as ScrollCounter } from "./ui/ScrollCounter";
|
||||||
95
src/widgets/scroll-counter/ui/ScrollCounter.tsx
Normal file
95
src/widgets/scroll-counter/ui/ScrollCounter.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
const DIGITS = Array.from({ length: 10 }, (_, i) => 9 - i);
|
||||||
|
|
||||||
|
const DigitWheel = ({ value }: { value: number }) => (
|
||||||
|
<span
|
||||||
|
className="inline-block h-[1em] w-[0.62em] overflow-hidden leading-none tabular-nums"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="flex flex-col transition-transform duration-500 ease-[cubic-bezier(0.65,0,0.35,1)]"
|
||||||
|
style={{ transform: `translateY(${-(9 - value)}em)` }}
|
||||||
|
>
|
||||||
|
{DIGITS.map((digit) => (
|
||||||
|
<span
|
||||||
|
key={digit}
|
||||||
|
className="flex h-[1em] w-[0.62em] items-center justify-center"
|
||||||
|
>
|
||||||
|
{digit}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ScrollCounter = () => {
|
||||||
|
const [index, setIndex] = useState(0);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const totalRef = useRef(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const sections = Array.from(
|
||||||
|
document.querySelectorAll("[data-scroll-section]")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sections.length) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (totalRef.current !== sections.length) {
|
||||||
|
totalRef.current = sections.length;
|
||||||
|
setTotal(sections.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
setIndex(Math.max(0, sections.indexOf(entry.target)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ rootMargin: "-45% 0px -45% 0px", threshold: 0 }
|
||||||
|
);
|
||||||
|
|
||||||
|
sections.forEach((section) => observer.observe(section));
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const current = String(index + 1).padStart(2, "0");
|
||||||
|
const totalText = String(total).padStart(2, "0");
|
||||||
|
const progress = total > 1 ? (index / (total - 1)) * 100 : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="pointer-events-none fixed right-[clamp(16px,2vw,32px)] top-1/2 z-40 hidden -translate-y-1/2 flex-col items-center gap-3 lg:flex"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{/* rolling digits: предыдущая цифра уходит вниз,
|
||||||
|
следующая появляется сверху */}
|
||||||
|
<div className="flex items-baseline text-2xl font-semibold leading-none text-white/80">
|
||||||
|
<DigitWheel value={Number(current[0])} />
|
||||||
|
<DigitWheel value={Number(current[1])} />
|
||||||
|
<span className="ml-1 text-[11px] font-medium text-white/30">
|
||||||
|
/ {totalText}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* vertical progress hairline (позиция зависит от числа секций) */}
|
||||||
|
<span className="relative block h-14 w-px overflow-hidden bg-white/10">
|
||||||
|
<span
|
||||||
|
className="absolute left-0 top-0 block h-1/2 w-full bg-white/70 transition-[top] duration-500 ease-out"
|
||||||
|
style={{ top: `${progress * 0.5}%` }}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="text-[10px] font-medium uppercase tracking-[0.3em] text-white/30">
|
||||||
|
scroll
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScrollCounter;
|
||||||
Loading…
Add table
Reference in a new issue