feat: dark luxury redesign with black heart branding

Complete visual overhaul: dark-only mode, rose/crimson accent system,
glassmorphism header, animated hero with floating hearts and glow orbs,
photo-backed cards, infinite team carousel with drag support,
redesigned modals with hero images, black heart logo with rose glow silhouette.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 23:30:10 +03:00
parent 1f6e314af6
commit 9cf09b6894
16 changed files with 762 additions and 244 deletions

View File

@@ -16,6 +16,17 @@ html {
scroll-behavior: smooth;
}
body {
overflow-x: hidden;
}
/* ===== Selection ===== */
::selection {
background-color: rgba(225, 29, 72, 0.3);
color: inherit;
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
@@ -32,6 +43,5 @@ html {
/* ===== Focus ===== */
:focus-visible {
@apply outline-2 outline-offset-2 outline-neutral-900;
@apply dark:outline-white;
@apply outline-2 outline-offset-2 outline-rose-500;
}

View File

@@ -26,31 +26,18 @@ export const metadata: Metadata = {
},
};
const themeScript = `
(function() {
var stored = localStorage.getItem('theme');
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (stored === 'dark' || (!stored && prefersDark)) {
document.documentElement.classList.add('dark');
}
})();
`;
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ru" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
</head>
<html lang="ru" className="dark">
<body
className={`${inter.variable} ${oswald.variable} surface-base font-sans antialiased`}
className={`${inter.variable} ${oswald.variable} bg-[#050505] text-neutral-50 font-sans antialiased`}
>
<Header />
<main className="pt-16">{children}</main>
<main>{children}</main>
<Footer />
</body>
</html>

View File

@@ -3,7 +3,7 @@
@keyframes hero-fade-in-up {
from {
opacity: 0;
transform: translateY(24px);
transform: translateY(32px);
}
to {
opacity: 1;
@@ -14,11 +14,59 @@
@keyframes hero-fade-in-scale {
from {
opacity: 0;
transform: scale(0.85);
transform: scale(0.8);
filter: blur(10px);
}
to {
opacity: 1;
transform: scale(1);
filter: blur(0);
}
}
@keyframes gradient-shift {
0%, 100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
}
@keyframes pulse-glow {
0%, 100% {
opacity: 0.4;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.05);
}
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
@keyframes heart-float {
0% {
opacity: 0;
transform: translateY(0) scale(0.5);
}
10% {
opacity: 0.6;
}
90% {
opacity: 0;
}
100% {
opacity: 0;
transform: translateY(-100vh) scale(1);
}
}
@@ -26,22 +74,95 @@
.hero-logo {
opacity: 0;
animation: hero-fade-in-scale 1s ease-out 0.1s forwards;
animation: hero-fade-in-scale 1.2s cubic-bezier(0.16, 1, 0.3, 1) 0.1s forwards;
}
.hero-title {
opacity: 0;
animation: hero-fade-in-up 0.8s ease-out 0.4s forwards;
animation: hero-fade-in-up 1s cubic-bezier(0.16, 1, 0.3, 1) 0.5s forwards;
}
.hero-subtitle {
opacity: 0;
animation: hero-fade-in-up 0.8s ease-out 0.7s forwards;
animation: hero-fade-in-up 1s cubic-bezier(0.16, 1, 0.3, 1) 0.8s forwards;
}
.hero-cta {
opacity: 0;
animation: hero-fade-in-up 0.8s ease-out 1s forwards;
animation: hero-fade-in-up 1s cubic-bezier(0.16, 1, 0.3, 1) 1.1s forwards;
}
/* ===== Hero Background ===== */
.hero-bg-gradient {
background: radial-gradient(ellipse 80% 60% at 50% -20%, rgba(225, 29, 72, 0.15), transparent),
radial-gradient(ellipse 60% 40% at 80% 50%, rgba(225, 29, 72, 0.08), transparent),
radial-gradient(ellipse 60% 40% at 20% 80%, rgba(225, 29, 72, 0.06), transparent);
}
.hero-glow-orb {
position: absolute;
border-radius: 50%;
filter: blur(80px);
animation: pulse-glow 6s ease-in-out infinite;
pointer-events: none;
}
/* ===== Gradient Text ===== */
.gradient-text {
background: linear-gradient(135deg, #fff 0%, #e11d48 50%, #fff 100%);
background-size: 200% 200%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
animation: gradient-shift 6s ease-in-out infinite;
}
/* Light mode gradient text */
.gradient-text-light {
background: linear-gradient(135deg, #171717 0%, #e11d48 50%, #171717 100%);
background-size: 200% 200%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
animation: gradient-shift 6s ease-in-out infinite;
}
/* ===== Animated Border ===== */
.animated-border {
position: relative;
}
.animated-border::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
padding: 1px;
background: linear-gradient(135deg, rgba(225, 29, 72, 0.3), transparent 40%, transparent 60%, rgba(225, 29, 72, 0.15));
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
mask-composite: exclude;
pointer-events: none;
transition: opacity 0.5s ease;
opacity: 0.5;
}
.animated-border:hover::before {
opacity: 1;
background: linear-gradient(135deg, rgba(225, 29, 72, 0.6), transparent 40%, transparent 60%, rgba(225, 29, 72, 0.4));
}
/* ===== Glow Effect ===== */
.glow-hover {
transition: box-shadow 0.5s ease, transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.glow-hover:hover {
box-shadow: 0 0 30px rgba(225, 29, 72, 0.1), 0 0 60px rgba(225, 29, 72, 0.05);
transform: translateY(-4px);
}
/* ===== Scroll Reveal ===== */
@@ -49,7 +170,7 @@
.reveal {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.7s ease-out, transform 0.7s ease-out;
transition: opacity 0.8s cubic-bezier(0.16, 1, 0.3, 1), transform 0.8s cubic-bezier(0.16, 1, 0.3, 1);
}
.reveal.visible {
@@ -62,11 +183,11 @@
@keyframes modal-fade-in {
from {
opacity: 0;
transform: scale(0.95);
transform: scale(0.95) translateY(10px);
}
to {
opacity: 1;
transform: scale(1);
transform: scale(1) translateY(0);
}
}
@@ -80,11 +201,18 @@
}
.modal-overlay {
animation: modal-overlay-in 0.2s ease-out;
animation: modal-overlay-in 0.3s ease-out;
}
.modal-content {
animation: modal-fade-in 0.3s ease-out;
animation: modal-fade-in 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
/* ===== Section Divider ===== */
.section-divider {
height: 1px;
background: linear-gradient(90deg, transparent, rgba(225, 29, 72, 0.3), transparent);
}
/* ===== Reduced Motion ===== */
@@ -96,6 +224,7 @@
.hero-cta {
animation: none !important;
opacity: 1 !important;
filter: none !important;
}
.reveal {
@@ -108,4 +237,17 @@
.modal-content {
animation: none !important;
}
.gradient-text,
.gradient-text-light {
animation: none !important;
}
.hero-glow-orb {
animation: none !important;
}
.glow-hover:hover {
transform: none;
}
}

View File

@@ -1,50 +1,57 @@
/* ===== Navigation ===== */
.nav-link {
@apply text-sm font-medium text-neutral-600 transition-colors duration-200;
@apply text-sm font-medium transition-all duration-300;
@apply text-neutral-500;
@apply hover:text-neutral-900;
@apply dark:text-neutral-400 dark:hover:text-white;
}
.nav-link-active {
@apply text-rose-600;
@apply dark:text-rose-400;
}
.social-icon {
@apply text-neutral-500 transition-colors duration-200;
@apply hover:text-neutral-900;
@apply dark:text-neutral-400 dark:hover:text-white;
@apply text-neutral-400 transition-all duration-300;
@apply hover:text-rose-600;
@apply dark:text-neutral-500 dark:hover:text-rose-400;
}
/* ===== Cards ===== */
.card {
@apply rounded-2xl border p-6 transition-all duration-200 cursor-pointer;
@apply border-neutral-200 bg-neutral-50;
@apply hover:border-neutral-400 hover:shadow-lg;
@apply dark:border-neutral-800 dark:bg-neutral-900;
@apply dark:hover:border-neutral-600;
@apply rounded-2xl border p-6 transition-all duration-500 cursor-pointer;
@apply border-neutral-200 bg-white;
@apply hover:border-rose-200 hover:shadow-lg;
@apply dark:border-white/[0.06] dark:bg-white/[0.02];
@apply dark:hover:border-rose-500/20 dark:hover:bg-white/[0.04];
@apply dark:hover:shadow-[0_0_30px_rgba(225,29,72,0.08)];
}
/* ===== Buttons ===== */
.btn-primary {
@apply inline-flex items-center justify-center font-medium rounded-full transition-colors duration-200 cursor-pointer;
@apply bg-neutral-900 text-white;
@apply hover:bg-neutral-700;
@apply dark:bg-white dark:text-neutral-900;
@apply dark:hover:bg-neutral-200;
@apply inline-flex items-center justify-center font-semibold rounded-full transition-all duration-300 cursor-pointer;
@apply bg-rose-600 text-white;
@apply hover:bg-rose-500 hover:shadow-[0_0_30px_rgba(225,29,72,0.4)];
@apply dark:bg-rose-600 dark:text-white;
@apply dark:hover:bg-rose-500 dark:hover:shadow-[0_0_30px_rgba(225,29,72,0.4)];
}
.btn-outline {
@apply inline-flex items-center justify-center font-medium rounded-full transition-colors duration-200 cursor-pointer;
@apply border border-neutral-900 text-neutral-900;
@apply hover:bg-neutral-900 hover:text-white;
@apply dark:border-white dark:text-white;
@apply dark:hover:bg-white dark:hover:text-neutral-900;
@apply inline-flex items-center justify-center font-semibold rounded-full transition-all duration-300 cursor-pointer;
@apply border border-rose-600 text-rose-600;
@apply hover:bg-rose-600 hover:text-white;
@apply dark:border-rose-500 dark:text-rose-400;
@apply dark:hover:bg-rose-500 dark:hover:text-white;
}
.btn-ghost {
@apply inline-flex items-center justify-center font-medium rounded-full transition-colors duration-200 cursor-pointer;
@apply inline-flex items-center justify-center font-medium rounded-full transition-all duration-300 cursor-pointer;
@apply text-neutral-600;
@apply hover:text-neutral-900;
@apply dark:text-neutral-400 dark:hover:text-white;
@apply hover:text-rose-600;
@apply dark:text-neutral-400 dark:hover:text-rose-400;
}
/* ===== Scrollbar ===== */
@@ -74,6 +81,6 @@
}
.contact-icon {
@apply shrink-0 text-neutral-900;
@apply dark:text-neutral-50;
@apply shrink-0 text-rose-600;
@apply dark:text-rose-400;
}

View File

@@ -1,32 +1,37 @@
/* ===== Surfaces ===== */
.surface-base {
@apply bg-white text-neutral-900;
@apply dark:bg-neutral-950 dark:text-neutral-50;
@apply bg-neutral-50 text-neutral-900;
@apply dark:bg-[#050505] dark:text-neutral-50;
}
.surface-muted {
@apply bg-neutral-100;
@apply dark:bg-neutral-900;
@apply dark:bg-[#0a0a0a];
}
.surface-glass {
@apply bg-white/80 backdrop-blur-md;
@apply dark:bg-neutral-950/80;
@apply bg-white/70 backdrop-blur-xl;
@apply dark:bg-black/40 dark:backdrop-blur-xl;
}
.surface-card {
@apply bg-white/80 backdrop-blur-sm;
@apply dark:bg-white/[0.03] dark:backdrop-blur-sm;
}
/* ===== Borders ===== */
.theme-border {
@apply border-neutral-200;
@apply dark:border-neutral-800;
@apply dark:border-white/[0.06];
}
/* ===== Text ===== */
.heading-text {
@apply text-neutral-900;
@apply dark:text-neutral-50;
@apply dark:text-white;
}
.body-text {
@@ -36,13 +41,18 @@
.muted-text {
@apply text-neutral-500;
@apply dark:text-neutral-400;
@apply dark:text-neutral-500;
}
.accent-text {
@apply text-rose-600;
@apply dark:text-rose-400;
}
/* ===== Layout ===== */
.section-padding {
@apply py-10 sm:py-14;
@apply py-16 sm:py-24;
}
.section-container {

View File

@@ -1,17 +1,20 @@
import { BRAND } from "@/lib/constants";
import { siteContent } from "@/data/content";
import { SocialLinks } from "@/components/ui/SocialLinks";
import { Heart } from "lucide-react";
export function Footer() {
const { contact } = siteContent;
const year = new Date().getFullYear();
return (
<footer className="surface-muted theme-border border-t">
<div className="section-container flex flex-col items-center gap-4 py-8 sm:flex-row sm:justify-between">
<p className="muted-text text-sm">
<footer className="relative border-t border-neutral-200 bg-neutral-100 dark:border-white/[0.06] dark:bg-[#050505]">
<div className="section-divider absolute top-0 left-0 right-0" />
<div className="section-container flex flex-col items-center gap-4 py-10 sm:flex-row sm:justify-between">
<p className="text-sm text-neutral-500">
&copy; {year} {BRAND.name}
</p>
<div className="flex items-center gap-1.5 text-sm text-neutral-500">
<span>Made with</span>
<Heart size={14} className="fill-rose-500 text-rose-500" />
</div>
</div>
</footer>
);

View File

@@ -3,65 +3,97 @@
import Image from "next/image";
import Link from "next/link";
import { Menu, X } from "lucide-react";
import { useState } from "react";
import { useState, useEffect } from "react";
import { BRAND, NAV_LINKS } from "@/lib/constants";
import { ThemeToggle } from "@/components/ui/ThemeToggle";
export function Header() {
const [menuOpen, setMenuOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
function handleScroll() {
setScrolled(window.scrollY > 20);
}
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return (
<header className="surface-glass theme-border fixed top-0 z-50 w-full border-b">
<div className="flex h-16 items-center justify-between px-6 sm:px-8">
<Link href="/" className="flex items-center gap-2">
<Image
src="/images/logo.png"
alt={BRAND.name}
width={32}
height={32}
unoptimized
className="dark:invert"
/>
<span className="font-display text-lg font-bold tracking-tight">
<header
className={`fixed top-0 z-50 w-full transition-all duration-500 ${
scrolled
? "border-b border-white/[0.06] bg-black/40 shadow-none backdrop-blur-xl"
: "bg-transparent"
}`}
>
<div className="mx-auto flex h-16 max-w-6xl items-center justify-between px-6 sm:px-8">
<Link href="/" className="group flex items-center gap-2.5">
<div className="relative flex h-8 w-8 items-center justify-center">
<div
className="absolute inset-0 rounded-full transition-all duration-300 group-hover:scale-125"
style={{
background: "radial-gradient(circle, rgba(225,29,72,0.5) 0%, rgba(225,29,72,0.15) 50%, transparent 70%)",
}}
/>
<Image
src="/images/logo.png"
alt={BRAND.name}
width={24}
height={24}
unoptimized
className="relative transition-transform duration-300 group-hover:scale-110"
style={{
filter: "drop-shadow(0 0 3px rgba(225,29,72,0.5))",
}}
/>
</div>
<span className="font-display text-lg font-bold tracking-tight text-white">
{BRAND.shortName}
</span>
</Link>
<nav className="hidden items-center gap-8 md:flex">
{NAV_LINKS.map((link) => (
<a key={link.href} href={link.href} className="nav-link">
<a
key={link.href}
href={link.href}
className="relative py-1 text-sm font-medium text-neutral-400 transition-all duration-300 after:absolute after:bottom-0 after:left-0 after:h-[2px] after:w-0 after:bg-rose-500 after:transition-all after:duration-300 hover:text-white hover:after:w-full"
>
{link.label}
</a>
))}
<ThemeToggle />
</nav>
<div className="flex items-center gap-2 md:hidden">
<ThemeToggle />
<button
onClick={() => setMenuOpen(!menuOpen)}
aria-label="Меню"
className="body-text rounded-md p-2"
className="rounded-lg p-2 text-neutral-400 transition-colors hover:text-white"
>
{menuOpen ? <X size={24} /> : <Menu size={24} />}
</button>
</div>
</div>
{menuOpen && (
<nav className="surface-base theme-border border-t px-6 py-4 sm:px-8 md:hidden">
{/* Mobile menu */}
<div
className={`overflow-hidden transition-all duration-300 md:hidden ${
menuOpen ? "max-h-80 opacity-100" : "max-h-0 opacity-0"
}`}
>
<nav className="border-t border-white/[0.06] bg-black/40 px-6 py-4 backdrop-blur-xl sm:px-8">
{NAV_LINKS.map((link) => (
<a
key={link.href}
href={link.href}
onClick={() => setMenuOpen(false)}
className="nav-link block py-3"
className="block py-3 text-base text-neutral-400 transition-colors hover:text-white"
>
{link.label}
</a>
))}
</nav>
)}
</div>
</header>
);
}

View File

@@ -1,21 +1,29 @@
import { siteContent } from "@/data/content";
import { SectionHeading } from "@/components/ui/SectionHeading";
import { Reveal } from "@/components/ui/Reveal";
import { Heart } from "lucide-react";
export function About() {
const { about } = siteContent;
return (
<section id="about" className="surface-muted section-padding">
<section id="about" className="relative section-padding bg-neutral-100 dark:bg-[#0a0a0a]">
<div className="section-divider absolute top-0 left-0 right-0" />
<div className="section-container">
<Reveal>
<SectionHeading>{about.title}</SectionHeading>
</Reveal>
<div className="mt-8 max-w-3xl space-y-4">
<div className="mt-10 max-w-3xl space-y-6">
{about.paragraphs.map((text, i) => (
<Reveal key={i}>
<p className="body-text text-lg leading-relaxed">{text}</p>
<div className="flex gap-4">
<Heart
size={20}
className="mt-1 shrink-0 fill-rose-500/20 text-rose-500 dark:fill-rose-500/10 dark:text-rose-400"
/>
<p className="body-text text-lg leading-relaxed">{text}</p>
</div>
</Reveal>
))}
</div>

View File

@@ -1,7 +1,8 @@
"use client";
import { useState } from "react";
import { Flame, Sparkles, Wind, Zap, Star, Monitor } from "lucide-react";
import Image from "next/image";
import { Flame, Sparkles, Wind, Zap, Star, Monitor, ArrowRight } from "lucide-react";
import { siteContent } from "@/data/content";
import { SectionHeading } from "@/components/ui/SectionHeading";
import { Reveal } from "@/components/ui/Reveal";
@@ -9,12 +10,12 @@ import { ClassModal } from "@/components/ui/ClassModal";
import type { ClassItem } from "@/types";
const iconMap: Record<string, React.ReactNode> = {
flame: <Flame size={32} />,
sparkles: <Sparkles size={32} />,
wind: <Wind size={32} />,
zap: <Zap size={32} />,
star: <Star size={32} />,
monitor: <Monitor size={32} />,
flame: <Flame size={20} />,
sparkles: <Sparkles size={20} />,
wind: <Wind size={20} />,
zap: <Zap size={20} />,
star: <Star size={20} />,
monitor: <Monitor size={20} />,
};
export function Classes() {
@@ -22,22 +23,57 @@ export function Classes() {
const [selectedClass, setSelectedClass] = useState<ClassItem | null>(null);
return (
<section id="classes" className="surface-muted section-padding">
<section id="classes" className="relative section-padding bg-neutral-100 dark:bg-[#0a0a0a]">
<div className="section-divider absolute top-0 left-0 right-0" />
<div className="section-container">
<Reveal>
<SectionHeading>{classes.title}</SectionHeading>
</Reveal>
<div className="mt-12 grid gap-6 sm:grid-cols-2">
<div className="mt-14 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{classes.items.map((item) => (
<Reveal key={item.name} className="h-full">
<div
className="card h-full flex flex-col cursor-pointer transition-transform hover:scale-[1.02]"
className="group relative h-full min-h-[280px] cursor-pointer overflow-hidden rounded-2xl"
onClick={() => setSelectedClass(item)}
>
<div className="heading-text">{iconMap[item.icon]}</div>
<h3 className="heading-text mt-4 text-xl font-semibold">{item.name}</h3>
<p className="body-text mt-2">{item.description}</p>
{/* Background image */}
{item.images && item.images[0] && (
<Image
src={item.images[0]}
alt={item.name}
fill
className="object-cover transition-transform duration-700 ease-out group-hover:scale-105"
/>
)}
{/* Dark gradient overlay */}
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-black/10 transition-all duration-500 group-hover:from-black/95 group-hover:via-black/50" />
{/* Rose tint on hover */}
<div className="absolute inset-0 bg-rose-900/0 transition-all duration-500 group-hover:bg-rose-900/10" />
{/* Content */}
<div className="relative flex h-full flex-col justify-end p-6">
{/* Icon badge */}
<div className="mb-3 inline-flex h-9 w-9 items-center justify-center rounded-lg bg-white/10 text-white backdrop-blur-sm transition-all duration-300 group-hover:bg-rose-500/20 group-hover:text-rose-300">
{iconMap[item.icon]}
</div>
<h3 className="text-xl font-semibold text-white">
{item.name}
</h3>
<p className="mt-1.5 text-sm leading-relaxed text-white/60 line-clamp-2">
{item.description}
</p>
{/* Hover arrow */}
<div className="mt-3 flex items-center gap-1.5 text-sm font-medium text-rose-400 opacity-0 translate-y-2 transition-all duration-300 group-hover:opacity-100 group-hover:translate-y-0">
<span>Подробнее</span>
<ArrowRight size={14} />
</div>
</div>
</div>
</Reveal>
))}

View File

@@ -8,51 +8,65 @@ export function Contact() {
const { contact } = siteContent;
return (
<section id="contact" className="surface-base section-padding">
<section id="contact" className="relative section-padding bg-neutral-50 dark:bg-[#050505]">
<div className="section-divider absolute top-0 left-0 right-0" />
<div className="section-container grid items-start gap-12 lg:grid-cols-2">
<Reveal>
<SectionHeading>{contact.title}</SectionHeading>
<div className="mt-12 space-y-6">
<div className="mt-10 space-y-5">
{contact.addresses.map((address, i) => (
<div key={i} className="contact-item">
<MapPin size={20} className="contact-icon" />
<div key={i} className="group flex items-center gap-4">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-rose-50 text-rose-600 transition-colors group-hover:bg-rose-100 dark:bg-rose-500/10 dark:text-rose-400 dark:group-hover:bg-rose-500/15">
<MapPin size={18} />
</div>
<p className="body-text">{address}</p>
</div>
))}
<div className="contact-item">
<Phone size={20} className="contact-icon" />
<a href={`tel:${contact.phone}`} className="nav-link text-base">
<div className="group flex items-center gap-4">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-rose-50 text-rose-600 transition-colors group-hover:bg-rose-100 dark:bg-rose-500/10 dark:text-rose-400 dark:group-hover:bg-rose-500/15">
<Phone size={18} />
</div>
<a
href={`tel:${contact.phone}`}
className="text-neutral-600 transition-colors hover:text-rose-600 dark:text-neutral-400 dark:hover:text-rose-400"
>
{contact.phone}
</a>
</div>
<div className="contact-item">
<Clock size={20} className="contact-icon" />
<div className="group flex items-center gap-4">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-rose-50 text-rose-600 transition-colors group-hover:bg-rose-100 dark:bg-rose-500/10 dark:text-rose-400 dark:group-hover:bg-rose-500/15">
<Clock size={18} />
</div>
<p className="body-text">{contact.workingHours}</p>
</div>
<div className="theme-border contact-item border-t pt-6">
<Instagram size={20} className="contact-icon" />
<a
href={contact.instagram}
target="_blank"
rel="noopener noreferrer"
className="nav-link text-base"
>
{BRAND.instagramHandle}
</a>
<div className="border-t border-neutral-200 pt-5 dark:border-white/[0.06]">
<div className="group flex items-center gap-4">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-rose-50 text-rose-600 transition-colors group-hover:bg-rose-100 dark:bg-rose-500/10 dark:text-rose-400 dark:group-hover:bg-rose-500/15">
<Instagram size={18} />
</div>
<a
href={contact.instagram}
target="_blank"
rel="noopener noreferrer"
className="text-neutral-600 transition-colors hover:text-rose-600 dark:text-neutral-400 dark:hover:text-rose-400"
>
{BRAND.instagramHandle}
</a>
</div>
</div>
</div>
</Reveal>
<Reveal>
<div className="theme-border overflow-hidden rounded-2xl border">
<div className="overflow-hidden rounded-2xl border border-neutral-200 shadow-sm dark:border-white/[0.06] dark:shadow-[0_0_30px_rgba(225,29,72,0.05)]">
<iframe
src={contact.mapEmbedUrl}
width="100%"
height="350"
height="380"
style={{ border: 0 }}
allowFullScreen
loading="lazy"

View File

@@ -1,35 +1,99 @@
"use client";
import Image from "next/image";
import { siteContent } from "@/data/content";
import { BRAND } from "@/lib/constants";
import { Button } from "@/components/ui/Button";
import { FloatingHearts } from "@/components/ui/FloatingHearts";
import { ChevronDown } from "lucide-react";
export function Hero() {
const { hero } = siteContent;
return (
<section className="surface-base section-container flex min-h-svh items-center justify-center">
<div className="text-center">
<Image
src="/images/logo.png"
alt={BRAND.name}
width={280}
height={280}
priority
unoptimized
className="hero-logo mx-auto mb-8 dark:invert"
/>
<section className="relative flex min-h-svh items-center justify-center overflow-hidden bg-[#050505]">
{/* Animated gradient background */}
<div className="hero-bg-gradient absolute inset-0" />
{/* Glow orbs */}
<div
className="hero-glow-orb"
style={{
width: "500px",
height: "500px",
top: "-10%",
left: "50%",
transform: "translateX(-50%)",
background: "radial-gradient(circle, rgba(225, 29, 72, 0.12), transparent 70%)",
}}
/>
<div
className="hero-glow-orb"
style={{
width: "300px",
height: "300px",
bottom: "10%",
right: "10%",
background: "radial-gradient(circle, rgba(225, 29, 72, 0.08), transparent 70%)",
animationDelay: "3s",
}}
/>
{/* Floating hearts */}
<FloatingHearts />
{/* Content */}
<div className="section-container relative z-10 text-center">
<div className="hero-logo relative mx-auto mb-10 h-[220px] w-[220px]">
{/* Outer ambient glow */}
<div className="absolute -inset-16 rounded-full bg-rose-500/8 blur-[60px]" />
{/* Rose disc — makes black heart visible as silhouette */}
<div
className="absolute inset-2 rounded-full"
style={{
background: "radial-gradient(circle, rgba(225,29,72,0.45) 0%, rgba(225,29,72,0.18) 45%, transparent 70%)",
}}
/>
<Image
src="/images/logo.png"
alt={BRAND.name}
width={220}
height={220}
priority
unoptimized
className="relative"
style={{
filter:
"drop-shadow(0 0 6px rgba(225,29,72,0.5)) drop-shadow(0 0 20px rgba(225,29,72,0.25))",
}}
/>
</div>
<h1 className="hero-title font-display text-5xl font-bold tracking-tight sm:text-6xl lg:text-8xl">
{hero.headline}
<span className="gradient-text">{hero.headline}</span>
</h1>
<p className="hero-subtitle body-text mx-auto mt-6 max-w-md text-lg sm:text-xl">
<p className="hero-subtitle mx-auto mt-6 max-w-lg text-lg text-neutral-400 sm:text-xl">
{hero.subheadline}
</p>
<div className="hero-cta mt-10">
<div className="hero-cta mt-12">
<Button href={hero.ctaHref} size="lg">
{hero.ctaText}
</Button>
</div>
</div>
{/* Scroll indicator */}
<div className="hero-cta absolute bottom-8 left-1/2 -translate-x-1/2">
<a
href="#about"
className="flex flex-col items-center gap-1 text-neutral-600 transition-colors hover:text-rose-400"
>
<span className="text-xs uppercase tracking-widest">Scroll</span>
<ChevronDown size={20} className="animate-bounce" />
</a>
</div>
</section>
);
}

View File

@@ -1,8 +1,8 @@
"use client";
import { useState } from "react";
import { useState, useRef, useEffect, useCallback } from "react";
import Image from "next/image";
import { Instagram } from "lucide-react";
import { Instagram, ChevronLeft, ChevronRight } from "lucide-react";
import { siteContent } from "@/data/content";
import { SectionHeading } from "@/components/ui/SectionHeading";
import { Reveal } from "@/components/ui/Reveal";
@@ -12,51 +12,179 @@ import type { TeamMember } from "@/types";
export function Team() {
const { team } = siteContent;
const [selectedMember, setSelectedMember] = useState<TeamMember | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const scrollTimer = useRef<ReturnType<typeof setTimeout>>(null);
const isDragging = useRef(false);
const dragStartX = useRef(0);
const dragScrollLeft = useRef(0);
const dragMoved = useRef(false);
// Render 3 copies: [clone] [original] [clone]
const tripled = [...team.members, ...team.members, ...team.members];
// On mount, jump to the middle set (no animation)
useEffect(() => {
const el = scrollRef.current;
if (!el) return;
requestAnimationFrame(() => {
const cardWidth = el.scrollWidth / 3;
el.scrollLeft = cardWidth;
});
}, []);
// When scroll settles, check if we need to loop
const handleScroll = useCallback(() => {
if (scrollTimer.current) clearTimeout(scrollTimer.current);
scrollTimer.current = setTimeout(() => {
const el = scrollRef.current;
if (!el) return;
const oneSetWidth = el.scrollWidth / 3;
if (el.scrollLeft < oneSetWidth * 0.3) {
el.style.scrollBehavior = "auto";
el.scrollLeft += oneSetWidth;
el.style.scrollBehavior = "";
}
if (el.scrollLeft > oneSetWidth * 1.7) {
el.style.scrollBehavior = "auto";
el.scrollLeft -= oneSetWidth;
el.style.scrollBehavior = "";
}
}, 100);
}, []);
// Mouse drag handlers
function handleMouseDown(e: React.MouseEvent) {
const el = scrollRef.current;
if (!el) return;
isDragging.current = true;
dragMoved.current = false;
dragStartX.current = e.pageX;
dragScrollLeft.current = el.scrollLeft;
el.style.scrollBehavior = "auto";
el.style.scrollSnapType = "none";
el.style.cursor = "grabbing";
}
function handleMouseMove(e: React.MouseEvent) {
if (!isDragging.current || !scrollRef.current) return;
e.preventDefault();
const dx = e.pageX - dragStartX.current;
if (Math.abs(dx) > 3) dragMoved.current = true;
scrollRef.current.scrollLeft = dragScrollLeft.current - dx;
}
function handleMouseUp() {
if (!isDragging.current || !scrollRef.current) return;
isDragging.current = false;
scrollRef.current.style.scrollBehavior = "";
scrollRef.current.style.scrollSnapType = "";
scrollRef.current.style.cursor = "";
}
function handleCardClick(member: TeamMember) {
// Don't open modal if user was dragging
if (dragMoved.current) return;
setSelectedMember(member);
}
function scroll(direction: "left" | "right") {
if (!scrollRef.current) return;
const amount = scrollRef.current.offsetWidth * 0.7;
scrollRef.current.scrollBy({
left: direction === "left" ? -amount : amount,
behavior: "smooth",
});
}
return (
<section id="team" className="surface-base section-padding">
<section id="team" className="relative section-padding bg-neutral-50 dark:bg-[#050505]">
<div className="section-divider absolute top-0 left-0 right-0" />
<div className="section-container">
<Reveal>
<SectionHeading>{team.title}</SectionHeading>
</Reveal>
</div>
<div className="mt-12 grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
{team.members.map((member, i) => (
<Reveal key={i}>
{/* Carousel wrapper */}
<Reveal>
<div className="relative mt-10">
{/* Scroll container */}
<div
ref={scrollRef}
onScroll={handleScroll}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
className="flex cursor-grab gap-4 overflow-x-auto px-6 pb-4 sm:px-8 scroll-smooth snap-x snap-mandatory select-none lg:px-[max(2rem,calc((100vw-72rem)/2+2rem))]"
style={{ scrollbarWidth: "none" }}
>
{tripled.map((member, i) => (
<div
className="card flex h-full cursor-pointer flex-col items-center text-center transition-transform hover:scale-[1.02]"
onClick={() => setSelectedMember(member)}
key={`${i}-${member.name}`}
className="group relative w-[220px] shrink-0 cursor-pointer snap-start overflow-hidden rounded-2xl sm:w-[260px]"
onClick={() => handleCardClick(member)}
>
<div className="mx-auto h-32 w-32 overflow-hidden rounded-full">
{/* Photo */}
<div className="aspect-[3/4] w-full overflow-hidden">
<Image
src={member.image}
alt={member.name}
width={128}
height={128}
className="h-full w-full object-cover"
width={260}
height={347}
className="h-full w-full object-cover transition-transform duration-700 ease-out group-hover:scale-105"
/>
</div>
<h3 className="heading-text mt-4 text-lg font-semibold">{member.name}</h3>
{member.instagram && (
<span
className="nav-link mt-1 inline-flex gap-1.5 text-sm"
onClick={(e) => e.stopPropagation()}
>
<Instagram size={14} className="shrink-0 mt-[3px]" />
<a
href={member.instagram}
target="_blank"
rel="noopener noreferrer"
{/* Gradient overlay */}
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent opacity-80 transition-opacity duration-500 group-hover:opacity-100" />
{/* Rose glow on hover */}
<div className="absolute inset-0 bg-gradient-to-t from-rose-900/20 to-transparent opacity-0 transition-opacity duration-500 group-hover:opacity-100" />
{/* Content */}
<div className="absolute bottom-0 left-0 right-0 p-4 translate-y-1 transition-transform duration-500 group-hover:translate-y-0">
<h3 className="text-base font-semibold text-white sm:text-lg">
{member.name}
</h3>
{member.instagram && (
<span
className="mt-1 inline-flex items-center gap-1.5 text-xs text-white/60 transition-colors hover:text-rose-400 sm:text-sm"
onClick={(e) => e.stopPropagation()}
>
{member.instagram.split("/").filter(Boolean).pop()}
</a>
</span>
)}
<Instagram size={12} className="shrink-0" />
<a
href={member.instagram}
target="_blank"
rel="noopener noreferrer"
>
{member.instagram.split("/").filter(Boolean).pop()}
</a>
</span>
)}
</div>
</div>
</Reveal>
))}
))}
</div>
{/* Side navigation arrows */}
<button
onClick={() => scroll("left")}
className="absolute left-2 top-1/2 -translate-y-1/2 hidden h-10 w-10 items-center justify-center rounded-full bg-black/50 text-white/80 backdrop-blur-sm transition-all hover:bg-rose-500/30 hover:text-white sm:flex"
aria-label="Назад"
>
<ChevronLeft size={22} />
</button>
<button
onClick={() => scroll("right")}
className="absolute right-2 top-1/2 -translate-y-1/2 hidden h-10 w-10 items-center justify-center rounded-full bg-black/50 text-white/80 backdrop-blur-sm transition-all hover:bg-rose-500/30 hover:text-white sm:flex"
aria-label="Вперёд"
>
<ChevronRight size={22} />
</button>
</div>
</div>
</Reveal>
<TeamMemberModal
member={selectedMember}

View File

@@ -6,12 +6,12 @@ import { X, Flame, Sparkles, Wind, Zap, Star, Monitor } from "lucide-react";
import type { ClassItem } from "@/types";
const iconMap: Record<string, React.ReactNode> = {
flame: <Flame size={40} />,
sparkles: <Sparkles size={40} />,
wind: <Wind size={40} />,
zap: <Zap size={40} />,
star: <Star size={40} />,
monitor: <Monitor size={40} />,
flame: <Flame size={20} />,
sparkles: <Sparkles size={20} />,
wind: <Wind size={20} />,
zap: <Zap size={20} />,
star: <Star size={20} />,
monitor: <Monitor size={20} />,
};
interface ClassModalProps {
@@ -38,55 +38,80 @@ export function ClassModal({ classItem, onClose }: ClassModalProps) {
if (!classItem) return null;
const heroImage = classItem.images?.[0];
return (
<div
className="modal-overlay fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
className="modal-overlay fixed inset-0 z-50 flex items-end justify-center bg-black/70 backdrop-blur-lg sm:items-center sm:p-4"
onClick={onClose}
>
<div
className="modal-content surface-base relative w-full max-w-md md:max-w-2xl max-h-[85vh] flex flex-col rounded-2xl shadow-xl"
className="modal-content relative flex w-full max-h-[90vh] flex-col overflow-hidden rounded-t-3xl bg-white sm:max-w-2xl sm:rounded-3xl dark:bg-[#111]"
onClick={(e) => e.stopPropagation()}
>
<button
onClick={onClose}
className="heading-text absolute right-4 top-4 z-10 rounded-full p-1 transition-opacity hover:opacity-70"
aria-label="Закрыть"
>
<X size={20} />
</button>
{/* Hero image banner */}
{heroImage && (
<div className="relative h-52 w-full shrink-0 sm:h-64">
<Image
src={heroImage}
alt={classItem.name}
fill
className="object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/30 to-transparent" />
<div className="overflow-y-auto p-6">
<div className="flex items-center gap-4">
<div className="heading-text shrink-0">{iconMap[classItem.icon]}</div>
<h3 className="heading-text text-xl font-semibold">
{classItem.name}
</h3>
{/* Close button */}
<button
onClick={onClose}
className="absolute right-4 top-4 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-black/40 text-white/80 backdrop-blur-sm transition-all hover:bg-black/60 hover:text-white"
aria-label="Закрыть"
>
<X size={16} />
</button>
{/* Title on image */}
<div className="absolute bottom-0 left-0 right-0 p-6">
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-white/15 text-white backdrop-blur-sm">
{iconMap[classItem.icon]}
</div>
<h3 className="text-2xl font-bold text-white">
{classItem.name}
</h3>
</div>
</div>
</div>
)}
{/* Content */}
<div className="overflow-y-auto">
{/* Title fallback when no image */}
{!heroImage && (
<div className="flex items-center justify-between p-6 pb-0">
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-rose-50 text-rose-600 dark:bg-rose-500/10 dark:text-rose-400">
{iconMap[classItem.icon]}
</div>
<h3 className="heading-text text-xl font-bold">
{classItem.name}
</h3>
</div>
<button
onClick={onClose}
className="rounded-full p-1.5 text-neutral-400 transition-all hover:bg-neutral-100 hover:text-neutral-900 dark:text-neutral-500 dark:hover:bg-white/[0.05] dark:hover:text-white"
aria-label="Закрыть"
>
<X size={18} />
</button>
</div>
)}
{classItem.detailedDescription && (
<div className="body-text mt-4 text-sm leading-relaxed whitespace-pre-line">
<div className="p-6 text-sm leading-relaxed whitespace-pre-line text-neutral-600 dark:text-neutral-400">
{classItem.detailedDescription}
</div>
)}
{classItem.images && classItem.images.length > 0 && (
<div className={`mt-6 flex gap-3 pb-2 ${classItem.images.length === 1 ? "justify-center" : "overflow-x-auto"}`}>
{classItem.images.map((src, i) => (
<div
key={i}
className="h-48 w-72 shrink-0 overflow-hidden rounded-xl"
>
<Image
src={src}
alt={`${classItem.name} ${i + 1}`}
width={288}
height={192}
className="h-full w-full object-cover"
/>
</div>
))}
</div>
)}
</div>
</div>
</div>

View File

@@ -0,0 +1,50 @@
"use client";
import { useEffect, useState } from "react";
interface Heart {
id: number;
left: number;
size: number;
delay: number;
duration: number;
opacity: number;
}
export function FloatingHearts() {
const [hearts, setHearts] = useState<Heart[]>([]);
useEffect(() => {
const generated: Heart[] = Array.from({ length: 12 }, (_, i) => ({
id: i,
left: Math.random() * 100,
size: 8 + Math.random() * 16,
delay: Math.random() * 10,
duration: 10 + Math.random() * 15,
opacity: 0.03 + Math.random() * 0.08,
}));
setHearts(generated);
}, []);
if (hearts.length === 0) return null;
return (
<div className="pointer-events-none absolute inset-0 overflow-hidden">
{hearts.map((heart) => (
<div
key={heart.id}
className="absolute text-rose-500"
style={{
left: `${heart.left}%`,
bottom: "-20px",
fontSize: `${heart.size}px`,
opacity: heart.opacity,
animation: `heart-float ${heart.duration}s ease-in ${heart.delay}s infinite`,
}}
>
&#9829;
</div>
))}
</div>
);
}

View File

@@ -9,7 +9,7 @@ export function SectionHeading({ children, className = "" }: SectionHeadingProps
className={`font-display text-3xl font-bold tracking-tight sm:text-4xl lg:text-5xl ${className}`}
>
{children}
<span className="mt-2 block h-1 w-16 rounded bg-neutral-900 dark:bg-white" />
<span className="mt-3 block h-[2px] w-16 rounded-full bg-gradient-to-r from-rose-500 to-rose-500/0" />
</h2>
);
}

View File

@@ -31,58 +31,60 @@ export function TeamMemberModal({ member, onClose }: TeamMemberModalProps) {
return (
<div
className="modal-overlay fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
className="modal-overlay fixed inset-0 z-50 flex items-end justify-center bg-black/70 backdrop-blur-lg sm:items-center sm:p-4"
onClick={onClose}
>
<div
className="modal-content surface-base relative w-full max-w-md md:max-w-2xl max-h-[85vh] flex flex-col rounded-2xl shadow-xl"
className="modal-content relative flex w-full max-h-[90vh] flex-col overflow-hidden rounded-t-3xl bg-white sm:max-w-lg sm:rounded-3xl dark:bg-[#111]"
onClick={(e) => e.stopPropagation()}
>
<button
onClick={onClose}
className="heading-text absolute right-4 top-4 z-10 rounded-full p-1 transition-opacity hover:opacity-70"
aria-label="Закрыть"
>
<X size={20} />
</button>
{/* Hero photo */}
<div className="relative h-72 w-full shrink-0 sm:h-80">
<Image
src={member.image}
alt={member.name}
fill
className="object-cover"
/>
{/* Gradient overlay */}
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/30 to-transparent" />
<div className="flex flex-col md:flex-row overflow-y-auto p-6 gap-6">
<div className="flex flex-col items-center md:items-start shrink-0">
<div className="h-40 w-40 md:h-48 md:w-48 overflow-hidden rounded-full ring-2 ring-rose-500/30">
<Image
src={member.image}
alt={member.name}
width={192}
height={192}
className="h-full w-full object-cover"
/>
</div>
{/* Close button */}
<button
onClick={onClose}
className="absolute right-4 top-4 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-black/40 text-white/80 backdrop-blur-sm transition-all hover:bg-black/60 hover:text-white"
aria-label="Закрыть"
>
<X size={16} />
</button>
<h3 className="heading-text mt-4 text-xl font-semibold text-center md:text-left w-full">
{/* Name + Instagram on photo */}
<div className="absolute bottom-0 left-0 right-0 p-6">
<h3 className="text-2xl font-bold text-white">
{member.name}
</h3>
{member.instagram && (
<a
href={member.instagram}
target="_blank"
rel="noopener noreferrer"
className="nav-link mt-1 inline-flex gap-1.5 text-sm"
className="mt-2 inline-flex items-center gap-2 text-sm text-white/70 transition-colors hover:text-rose-400"
>
<Instagram size={14} className="shrink-0 mt-[3px]" />
<Instagram size={15} className="shrink-0" />
<span>{member.instagram.split("/").filter(Boolean).pop()}</span>
</a>
)}
</div>
{member.description && (
<div className="md:border-l md:theme-border md:pl-6 flex items-center">
<p className="body-text text-sm leading-relaxed">
{member.description}
</p>
</div>
)}
</div>
{/* Description */}
{member.description && (
<div className="overflow-y-auto p-6">
<p className="text-sm leading-relaxed text-neutral-600 dark:text-neutral-400">
{member.description}
</p>
</div>
)}
</div>
</div>
);