2335 lines
85 KiB
TypeScript
2335 lines
85 KiB
TypeScript
'use client'
|
||
|
||
import {
|
||
motion,
|
||
MotionConfig,
|
||
useInView,
|
||
AnimatePresence,
|
||
useScroll,
|
||
useTransform,
|
||
useReducedMotion,
|
||
useMotionValue,
|
||
useSpring,
|
||
type Variants,
|
||
} from "framer-motion";
|
||
|
||
import { Megaphone, MessageSquare, LineChart, Users, Wrench, ClipboardCheck, Mail, Phone, Menu, X } from "lucide-react";
|
||
|
||
|
||
import React, { useRef, useEffect, useState, useCallback } from 'react'
|
||
|
||
import { Plus_Jakarta_Sans } from "next/font/google";
|
||
|
||
const headingFont = Plus_Jakarta_Sans({
|
||
subsets: ["latin"],
|
||
weight: ["700", "800"],
|
||
display: "swap",
|
||
});
|
||
|
||
|
||
import { Manrope } from "next/font/google";
|
||
|
||
const sytuacjeFont = Manrope({
|
||
subsets: ["latin"],
|
||
weight: ["700", "800"],
|
||
display: "swap",
|
||
});
|
||
|
||
|
||
|
||
/* =========================================================
|
||
Prime Code — Neo-Editorial+, full-bleed video hero
|
||
Tekst zachowany 1:1
|
||
========================================================= */
|
||
|
||
export default function Home() {
|
||
const [openIndex, setOpenIndex] = useState<number | null>(null)
|
||
const [activeSection, setActiveSection] = useState<string>('PrimeCode')
|
||
const [mobileOpen, setMobileOpen] = useState(false);
|
||
|
||
// blokuj scroll body, gdy menu otwarte
|
||
useEffect(() => {
|
||
document.body.style.overflow = mobileOpen ? "hidden" : "";
|
||
return () => { document.body.style.overflow = ""; };
|
||
}, [mobileOpen]);
|
||
|
||
|
||
useEffect(() => {
|
||
const sections = document.querySelectorAll('section[id]')
|
||
const obs = new IntersectionObserver(
|
||
(entries) => entries.forEach((e) => e.isIntersecting && setActiveSection(e.target.id)),
|
||
{ rootMargin: '-55% 0px -45% 0px' }
|
||
)
|
||
sections.forEach((s) => obs.observe(s))
|
||
return () => obs.disconnect()
|
||
}, [])
|
||
|
||
const navItems = [
|
||
{ id: 'PrimeCode', label: 'O nas' },
|
||
{ id: 'Sytuacje', label: 'Wyzwania' },
|
||
{ id: 'Partner', label: 'Twoje potrzeby' },
|
||
{ id: 'Model', label: 'Model Prime Code' },
|
||
{ id: 'CoPotrafimy', label: 'Co potrafimy?' },
|
||
{ id: 'JakDzialamy', label: 'Jak działamy?' },
|
||
{ id: 'PomogliśmyCaseStudies', label: 'Pomogliśmy' },
|
||
{ id: 'Kontakt', label: 'Kontakt' },
|
||
]
|
||
|
||
const caseStudies = [
|
||
{
|
||
title: 'Firma kosmetyczna',
|
||
short: 'Nowa strategia detaliczna i transformacja doświadczenia klientek w salonach.',
|
||
details: [
|
||
'Opracowanie nowej strategii detalicznej, integrującej sprzedaż z usługami.',
|
||
'Zaprojektowanie atmosfery sklepów oraz selekcja unikalnego asortymentu.',
|
||
'Transformacja komunikacji – od eksperckiej wiedzy do relacji opartej na empatii.',
|
||
'Redefinicja propozycji wartości – skupienie na doświadczeniu troski o zdrowie i piękno.',
|
||
'Stworzenie concept store’u jako pełnego doświadczenia marki.',
|
||
'Przejście z jednego formatu sklepu na trzy lepiej dopasowane formaty detaliczne.',
|
||
'Zmiana podejścia operacyjnego: od 4P do 7P, z naciskiem na doświadczenie klientki.',
|
||
],
|
||
},
|
||
{
|
||
title: 'Firma paliwowa',
|
||
short: 'Integracja doświadczenia na stacjach z tożsamością marki i branding sensoryczny.',
|
||
details: [
|
||
'Integracja strategii marki ze strategią doświadczeń klienta.',
|
||
'Badania neuromarketingowe i ponad 100 rekomendacji optymalizacyjnych.',
|
||
'Wielowarstwowa propozycja wartości – od tankowania po rozbudowane doświadczenie marki.',
|
||
'Identyfikacja momentów prawdy i plan zarządzania nimi.',
|
||
'Mapa ścieżek klienta z instrukcją optymalizacji.',
|
||
'Projekt atmosfery stacji (kolorystyka, zapach, układ).',
|
||
'Kompleksowy branding sensoryczny angażujący wszystkie zmysły.',
|
||
'Standardy obsługi klienta odzwierciedlające wartości marki.',
|
||
'Mapa wdrożenia z harmonogramem i wytycznymi operacyjnymi.',
|
||
],
|
||
},
|
||
{
|
||
title: 'Firma technologiczna',
|
||
short: 'Nowa architektura marki, system kompetencji i kultura klientocentryczna.',
|
||
details: [
|
||
'Zaprojektowanie struktury organizacyjnej i systemu kompetencji.',
|
||
'Redefinicja architektury marki (przejście z house of brands na branded house).',
|
||
'System zarządzania wartością klienta – poziom strategiczny, operacyjny i personalny.',
|
||
'„Playbooki” wdrażające pozycjonowanie do działań codziennych.',
|
||
'Usługi o wysokiej wartości dodanej, zmieniające postrzeganie firmy.',
|
||
'Model „high tech + high touch” łączący technologie z uważnością na klienta.',
|
||
'Strategia CRM wspierająca długofalowe relacje i wzrost.',
|
||
],
|
||
},
|
||
]
|
||
|
||
|
||
|
||
// Dlaczego (treść 1:1)
|
||
const why = {
|
||
p1: (
|
||
<>
|
||
<strong className="text-neutral-900">76% konsumentów oczekuje</strong>, że marka będzie
|
||
<strong className="text-neutral-900"> przewidywać ich potrzeby</strong>, nie tylko reagować
|
||
<span className="text-neutral-500"> (Salesforce, 2023)</span>.
|
||
</>
|
||
),
|
||
p2: (
|
||
<>
|
||
Pokolenie Z <strong className="text-neutral-900">nie buduje lojalności przez reklamy</strong>.
|
||
Buduje ją przez <strong className="text-neutral-900">wartość, użyteczność i autentyczność</strong>.
|
||
<span className="font-semibold text-neutral-900"> Aż 65% młodych konsumentów</span> deklaruje,
|
||
że lojalność wobec marki zależy od spójnych i realnych doświadczeń
|
||
<span className="text-neutral-500"> (McKinsey, 2023)</span>.
|
||
</>
|
||
),
|
||
bulletsIntro: 'Marka nie żyje w kampaniach. Żyje w:',
|
||
bullets: [
|
||
'chatbotach, landingach, mailach i pushach,',
|
||
'procesach sprzedażowych i onboardingowych,',
|
||
'sposobie, w jaki działa Twój model cenowy, UX i obsługa klienta.',
|
||
],
|
||
p3: (
|
||
<>
|
||
Jeśli to wszystko nie gra razem, marka <strong className="text-neutral-900">traci moc</strong>.
|
||
<br />
|
||
<strong className="text-neutral-900">I traci klientów.</strong>
|
||
<span className="text-neutral-500"> Według badania PwC aż 32% klientów</span> porzuca markę po jednej
|
||
złej interakcji, nawet jeśli wcześniej byli zadowoleni.
|
||
</>
|
||
),
|
||
}
|
||
|
||
const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
|
||
|
||
// === NOWE tiles (podmień całą dotychczasową definicję) ===
|
||
// === KAFELKI z jednolitym punktorowaniem ===
|
||
const tiles = [
|
||
{
|
||
title: "Brand Daily Scan™ – diagnoza w rytmie codzienności",
|
||
short: "Błyskawiczna analiza punktów styku, decyzji, procesów i sygnałów marki.",
|
||
output: "Mapa luk, insighty, benchmarki, rekomendacje.",
|
||
efekt:
|
||
"Mapa luk i niespójności, insighty z rzeczywistego użycia marki, benchmarki branżowe oraz rekomendacje naprawcze i wzmacniające.",
|
||
details: (
|
||
<>
|
||
<p >
|
||
Nie patrzymy na markę z dystansu. Wchodzimy w jej codzienne tętno.
|
||
Skupiamy się na tym, co naprawdę <strong>generuje doświadczenie klienta</strong>:
|
||
</p>
|
||
|
||
{/* stałe, eleganckie kropki */}
|
||
<ul className="mt-1 space-y-1">
|
||
<li className="relative pl-4 before:content-[''] before:absolute before:left-0 before:top-[0.55rem] before:w-1.5 before:h-1.5 before:rounded-full before:bg-cyan-600">
|
||
punkty styku (touchpointy),
|
||
</li>
|
||
<li className="relative pl-4 before:content-[''] before:absolute before:left-0 before:top-[0.55rem] before:w-1.5 before:h-1.5 before:rounded-full before:bg-cyan-600">
|
||
mikrodecyzje zespołów,
|
||
</li>
|
||
<li className="relative pl-4 before:content-[''] before:absolute before:left-0 before:top-[0.55rem] before:w-1.5 before:h-1.5 before:rounded-full before:bg-cyan-600">
|
||
automatyczne procesy i sygnały marki.
|
||
</li>
|
||
</ul>
|
||
</>
|
||
),
|
||
},
|
||
{
|
||
title: "Synchronizacja rytmu – faza pacingu",
|
||
short: "Zgrywamy Twoje działania z logiką marki. Automatyzujemy. Personalizujemy.",
|
||
output: "Roadmapa wdrożenia z KPI i zestawem eksperymentów.",
|
||
efekt:
|
||
"„Zgrane zegary” marki, marketingu, sprzedaży i obsługi; klarowne KPI oraz szybkie testy hipotez.",
|
||
details: (
|
||
<>
|
||
<p>
|
||
Integrujemy markę z codzienną dynamiką operacyjną. Nie robimy rewolucji —
|
||
<strong> synchronizujemy rytm</strong>:
|
||
</p>
|
||
|
||
{/* stałe, eleganckie kropki */}
|
||
<ul className="mt-1 space-y-1">
|
||
<li className="relative pl-4 before:content-[''] before:absolute before:left-0 before:top-[0.55rem] before:w-1.5 before:h-1.5 before:rounded-full before:bg-cyan-600">
|
||
automatyzujemy to, co można,
|
||
</li>
|
||
<li className="relative pl-4 before:content-[''] before:absolute before:left-0 before:top-[0.55rem] before:w-1.5 before:h-1.5 before:rounded-full before:bg-cyan-600">
|
||
personalizujemy to, co warto,
|
||
</li>
|
||
<li className="relative pl-4 before:content-[''] before:absolute before:left-0 before:top-[0.55rem] before:w-1.5 before:h-1.5 before:rounded-full before:bg-cyan-600">
|
||
urealniamy to, co obiecujesz jako marka.
|
||
</li>
|
||
</ul>
|
||
</>
|
||
),
|
||
},
|
||
{
|
||
title: "Wdrożenie i skalowanie",
|
||
short: "Marka w akcji: od call center i e-maili po pricing, CRM i fizyczną dostępność.",
|
||
output: "Efektywność, zaangażowanie, lojalność.",
|
||
efekt:
|
||
"Wzrost efektywności działań marketingowych, wyższe zaangażowanie użytkowników, zwiększenie lojalności i retencji — zmierzone.",
|
||
details: (
|
||
<>
|
||
<p >
|
||
Marka przestaje być tylko deklaracją. Zaczyna
|
||
<strong> działać w realnym czasie i miejscu</strong>:
|
||
</p>
|
||
|
||
{/* stałe, eleganckie kropki */}
|
||
<ul className="mt-1 space-y-1">
|
||
<li className="relative pl-4 before:content-[''] before:absolute before:left-0 before:top-[0.55rem] before:w-1.5 before:h-1.5 before:rounded-full before:bg-cyan-600">
|
||
w aplikacji i e-mailach,
|
||
</li>
|
||
<li className="relative pl-4 before:content-[''] before:absolute before:left-0 before:top-[0.55rem] before:w-1.5 before:h-1.5 before:rounded-full before:bg-cyan-600">
|
||
w polityce cenowej i mechanizmach CRM,
|
||
</li>
|
||
<li className="relative pl-4 before:content-[''] before:absolute before:left-0 before:top-[0.55rem] before:w-1.5 before:h-1.5 before:rounded-full before:bg-cyan-600">
|
||
w sklepie internetowym i na półce w punkcie sprzedaży.
|
||
</li>
|
||
</ul>
|
||
</>
|
||
),
|
||
},
|
||
];
|
||
|
||
const scrollToIntegration = useCallback(() => {
|
||
document.getElementById('DlaczegoIntegracji')?.scrollIntoView({ behavior: 'smooth' })
|
||
}, [])
|
||
|
||
return (
|
||
<div className="relative min-h-screen text-neutral-900 overflow-x-hidden">
|
||
{/* Subtelny akcent kolorystyczny + siatka */}
|
||
<BackgroundGrid />
|
||
{/* Pasek postępu scrolla */}
|
||
<ScrollProgress />
|
||
|
||
{/* NAV */}
|
||
<nav className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur border-b border-black/10">
|
||
<div className="max-w-7xl mx-auto px-4 h-16 flex justify-between items-center">
|
||
<a href="#PrimeCode" className="flex items-center gap-3">
|
||
<img src="/LogoPrimeCode-transparent.png" alt="Prime Code" className="h-7 w-auto" />
|
||
<span className="sr-only">Prime Code — O nas</span>
|
||
</a>
|
||
<ul className="hidden md:flex gap-7 font-medium">
|
||
{navItems.map((item) => (
|
||
<li key={item.id}>
|
||
<a
|
||
href={`#${item.id}`}
|
||
className={`hover:underline underline-offset-4 ${activeSection === item.id ? 'underline decoration-2' : 'decoration-1'
|
||
}`}
|
||
>
|
||
{item.label}
|
||
</a>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
<button
|
||
onClick={() => setMobileOpen(v => !v)}
|
||
aria-label={mobileOpen ? "Zamknij menu" : "Otwórz menu"}
|
||
aria-expanded={mobileOpen}
|
||
aria-controls="mobileMenu"
|
||
className="md:hidden h-10 w-10 grid place-items-center rounded-none ring-1 ring-black/15 hover:bg-black/5"
|
||
>
|
||
{mobileOpen ? <X className="size-5" /> : <Menu className="size-5" />}
|
||
</button>
|
||
|
||
</div>
|
||
</nav>
|
||
<AnimatePresence>
|
||
{mobileOpen && (
|
||
<>
|
||
{/* półprzezroczyste tło – kliknięcie zamyka */}
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
exit={{ opacity: 0 }}
|
||
transition={{ duration: 0.15 }}
|
||
className="fixed inset-0 z-40 bg-black/30 md:hidden"
|
||
onClick={() => setMobileOpen(false)}
|
||
/>
|
||
|
||
{/* sam panel */}
|
||
<motion.div
|
||
id="mobileMenu"
|
||
initial={{ opacity: 0, y: -8 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -8 }}
|
||
transition={{ duration: 0.2, ease: "easeOut" }}
|
||
className="fixed top-16 left-0 right-0 z-50 bg-white/95 backdrop-blur border-b border-black/10 md:hidden"
|
||
>
|
||
<ul className="px-2 py-2">
|
||
{navItems.map((item) => (
|
||
<li key={item.id}>
|
||
<a
|
||
href={`#${item.id}`}
|
||
onClick={() => setMobileOpen(false)}
|
||
className="block px-4 py-3 text-base hover:bg-black/5"
|
||
>
|
||
{item.label}
|
||
</a>
|
||
</li>
|
||
))}
|
||
<li className="my-1 border-t border-black/10" />
|
||
<li>
|
||
<a href="mailto:primecode@primecode.pl" className="flex items-center gap-2 px-4 py-3 hover:bg-black/5">
|
||
<Mail className="size-4" /> Email
|
||
</a>
|
||
</li>
|
||
<li>
|
||
<a href="tel:+48500133609" className="flex items-center gap-2 px-4 py-3 hover:bg-black/5">
|
||
<Phone className="size-4" /> Zadzwoń
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
</motion.div>
|
||
</>
|
||
)}
|
||
</AnimatePresence>
|
||
|
||
{/* było: <HeroInkCinematic ... /> */}
|
||
<HeroPixabayBG
|
||
src="/visuals/20072-307163785_small.mp4"
|
||
poster="/visuals/pixabay_bg_poster.jpg"
|
||
onScrollDown={scrollToIntegration}
|
||
/>
|
||
|
||
|
||
{/* PRZEJŚCIE/WIPE */}
|
||
<SectionWipe />
|
||
|
||
<DlaczegoIntegracjiWarm why={why} />
|
||
|
||
|
||
|
||
|
||
{/* SYTUACJE — horyzontalne wejścia kart */}
|
||
<SytuacjeSection />
|
||
|
||
|
||
{/* PARTNER */}
|
||
<PartnerSection />
|
||
|
||
<ModelSection tiles={tiles} />
|
||
|
||
<CoPotrafimySection />
|
||
|
||
{/* JAK DZIAŁAMY */}
|
||
<RiskFreeSection />
|
||
|
||
<PomoglismySection
|
||
caseStudies={caseStudies}
|
||
openIndex={openIndex}
|
||
setOpenIndex={setOpenIndex}
|
||
/>
|
||
|
||
|
||
|
||
<CaseStudiesSection caseStudies={caseStudies} />
|
||
|
||
|
||
|
||
|
||
|
||
{/* KONTAKT */}
|
||
<section id="Kontakt" className="relative py-24 overflow-hidden">
|
||
{/* tło: stonowany niebieski gradient (mniej jaskrawy) */}
|
||
<div aria-hidden className="absolute inset-0 -z-10">
|
||
{/* główny gradient – desat + ciut ciemniej */}
|
||
<div
|
||
className="absolute inset-0"
|
||
style={{
|
||
background:
|
||
"linear-gradient(100deg, #1578A1 0%, #2E6CC1 52%, #4B57C5 100%)",
|
||
}}
|
||
/>
|
||
{/* delikatne „wygaszenie” bielą – tonuje bez zmiany koloru */}
|
||
<div
|
||
className="absolute inset-0"
|
||
style={{
|
||
background: "linear-gradient(0deg, rgba(255,255,255,.12), rgba(255,255,255,.12))",
|
||
}}
|
||
/>
|
||
{/* bardzo subtelne rozświetlenie jak wcześniej, ale słabsze */}
|
||
<div
|
||
className="absolute inset-0 pointer-events-none"
|
||
style={{
|
||
background:
|
||
"radial-gradient(900px 420px at 88% -120px, rgba(255,255,255,0.22), transparent 70%)",
|
||
}}
|
||
/>
|
||
{/* hairline’y */}
|
||
<div className="absolute inset-x-0 top-0 h-[2px] bg-white/20 mix-blend-screen" />
|
||
<div className="absolute inset-x-0 bottom-0 h-px bg-white/25 mix-blend-screen" />
|
||
</div>
|
||
|
||
|
||
|
||
<div className="max-w-6xl mx-auto px-6 grid lg:grid-cols-[1.15fr_0.85fr] gap-12 items-center">
|
||
{/* LEWA: nagłówek + CTA (biel jak na screenie) */}
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 16, filter: "blur(6px)" }}
|
||
whileInView={{ opacity: 1, y: 0, filter: "blur(0px)" }}
|
||
viewport={{ once: true, amount: 0.6 }}
|
||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||
className="text-white"
|
||
>
|
||
<h2 className="text-4xl md:text-5xl font-extrabold tracking-[-0.01em]">
|
||
Zacznij od eksperymentu
|
||
</h2>
|
||
<p className="mt-4 text-lg text-white/85 max-w-xl">
|
||
Zobacz, co marka może zrobić — w 7 dni.
|
||
</p>
|
||
|
||
{/* Lokalizacje – chipsy/glass */}
|
||
<div className="mt-5 flex flex-wrap items-center gap-2 text-sm">
|
||
<span className="inline-flex items-center gap-2 px-3 py-1 rounded-full
|
||
bg-white/10 ring-1 ring-white/20 backdrop-blur">
|
||
<span className="h-2 w-2 rounded-full
|
||
bg-[radial-gradient(circle,#F28241_30%,#8C2E47_95%)]" />
|
||
Warszawa
|
||
</span>
|
||
<span className="inline-flex items-center gap-2 px-3 py-1 rounded-full
|
||
bg-white/10 ring-1 ring-white/20 backdrop-blur">
|
||
<span className="h-2 w-2 rounded-full
|
||
bg-[radial-gradient(circle,#F28241_30%,#8C2E47_95%)]" />
|
||
Kraków
|
||
</span>
|
||
<span className="inline-flex items-center gap-2 px-3 py-1 rounded-full
|
||
bg-white/10 ring-1 ring-white/20 backdrop-blur">
|
||
<span className="h-2 w-2 rounded-full
|
||
bg-[radial-gradient(circle,#F28241_30%,#8C2E47_95%)]" />
|
||
Gdańsk
|
||
</span>
|
||
<span className="inline-flex items-center gap-2 px-3 py-1 rounded-full
|
||
bg-white/10 ring-1 ring-white/20 backdrop-blur">
|
||
<span className="h-2 w-2 rounded-full
|
||
bg-[radial-gradient(circle,#F28241_30%,#8C2E47_95%)]" />
|
||
Wrocław
|
||
</span>
|
||
<span className="px-2 text-white/85">— działamy zdalnie i hybrydowo</span>
|
||
</div>
|
||
|
||
{/* CTA – biała i obrysowana, jak na zrzucie */}
|
||
<div className="mt-8 flex flex-wrap gap-3">
|
||
<a
|
||
href="mailto:primecode@primecode.pl"
|
||
className="inline-flex items-center gap-2 h-12 px-6 rounded bg-white text-sky-900
|
||
ring-1 ring-white/20 hover:bg-white/95 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/60"
|
||
>
|
||
<Mail className="size-5" aria-hidden />
|
||
<span>Napisz maila</span>
|
||
</a>
|
||
<a
|
||
href="tel:+48500133609"
|
||
className="inline-flex items-center gap-2 h-12 px-6 rounded text-white
|
||
ring-1 ring-white/65 hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
|
||
>
|
||
<Phone className="size-5" aria-hidden />
|
||
<span>Zadzwoń</span>
|
||
</a>
|
||
</div>
|
||
|
||
</motion.div>
|
||
|
||
{/* PRAWA: karta kontaktowa – jasna, z cieniem (jak na screenie) */}
|
||
<motion.aside
|
||
initial={{ opacity: 0, y: 18, scale: 0.985, filter: "blur(6px)" }}
|
||
whileInView={{ opacity: 1, y: 0, scale: 1, filter: "blur(0px)" }}
|
||
viewport={{ once: true, amount: 0.6 }}
|
||
transition={{ duration: 0.8, ease: [0.22, 1, 0.36, 1], delay: 0.05 }}
|
||
className="relative bg-white text-neutral-800 rounded-md p-6 lg:p-7 shadow-[0_14px_40px_rgba(0,0,0,0.18)] ring-1 ring-black/5"
|
||
>
|
||
{/* cienka chłodna linia u góry karty (bardzo subtelna) */}
|
||
<div aria-hidden className="absolute inset-x-0 top-0 h-[2px] bg-gradient-to-r from-sky-300 via-blue-400 to-indigo-500 rounded-t-md" />
|
||
|
||
<div className="flex items-center gap-4">
|
||
<img
|
||
src="/jacek.jpg"
|
||
alt="Jacek Pogorzelski – Prime Code"
|
||
className="w-16 h-16 object-cover rounded-full"
|
||
/>
|
||
<div>
|
||
<a
|
||
href="https://jacekpogorzelski.pl"
|
||
target="_blank"
|
||
rel="noopener"
|
||
className="text-lg font-semibold underline underline-offset-4 text-sky-800"
|
||
>
|
||
Jacek Pogorzelski
|
||
</a>
|
||
<p className="text-sm text-neutral-600 mt-0.5">Prime Code</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Mail i telefon – osobne linie z ikonkami */}
|
||
<div className="mt-5 flex flex-col gap-2 text-[15px]">
|
||
<a href="mailto:primecode@primecode.pl" className="flex items-center gap-2 hover:underline">
|
||
<Mail className="size-4 text-sky-600" aria-hidden />
|
||
<span>primecode@primecode.pl</span>
|
||
</a>
|
||
<a href="tel:+48500133609" className="flex items-center gap-2 hover:underline">
|
||
<Phone className="size-4 text-sky-600" aria-hidden />
|
||
<span>+48 500 133 609</span>
|
||
</a>
|
||
|
||
|
||
</div>
|
||
</motion.aside>
|
||
</div>
|
||
</section>
|
||
|
||
|
||
|
||
|
||
|
||
{/* FOOTER */}
|
||
<footer className="bg-white border-t border-black/10">
|
||
<div className="max-w-6xl mx-auto px-6 py-8">
|
||
<p className="text-center text-sm text-neutral-500">© 2025 Prime Code</p>
|
||
</div>
|
||
</footer>
|
||
|
||
{/* QUICK CONTACT */}
|
||
<div
|
||
aria-label="Szybki kontakt"
|
||
className="fixed z-50 bottom-4 right-4 sm:right-6 flex items-center gap-3 bg-white/95 backdrop-blur ring-1 ring-black/10 px-3 py-2"
|
||
>
|
||
<img src="/jacek.jpg" alt="Jacek Pogorzelski" className="w-7 h-7 rounded-full object-cover" />
|
||
<a href="mailto:primecode@primecode.pl" className="text-sm underline underline-offset-4">
|
||
Email
|
||
</a>
|
||
<span className="text-neutral-300">•</span>
|
||
<a href="tel:+48500133609" className="text-sm underline underline-offset-4">
|
||
Zadzwoń
|
||
</a>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/* ============================ VISUAL LAYER ============================ */
|
||
|
||
function BackgroundGrid() {
|
||
// pastelowy akcent + siatka + bardzo delikatny „glow” od góry
|
||
return (
|
||
<div aria-hidden className="fixed inset-0 -z-10 bg-white">
|
||
<div className="absolute inset-0 opacity-[0.05] [background-image:repeating-linear-gradient(0deg,transparent,transparent_23px,rgba(0,0,0,.08)_24px),repeating-linear-gradient(90deg,transparent,transparent_23px,rgba(0,0,0,.08)_24px)]" />
|
||
<div className="absolute inset-0 pointer-events-none [background:radial-gradient(800px_360px_at_50%_-120px,rgba(6,182,212,.18),transparent_70%)]" />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function ScrollProgress() {
|
||
const { scrollYProgress } = useScroll()
|
||
const scaleX = useTransform(scrollYProgress, [0, 1], [0, 1])
|
||
return (
|
||
<motion.div
|
||
style={{ scaleX }}
|
||
className="fixed top-0 left-0 right-0 h-[3px] origin-left z-[60] bg-gradient-to-r from-cyan-500 to-blue-500"
|
||
/>
|
||
)
|
||
}
|
||
|
||
/** Miękki „wipe” między sekcjami (kolorowy, ale subtelny) */
|
||
function SectionWipe() {
|
||
return (
|
||
<div aria-hidden className="relative h-10 -mt-10">
|
||
<div className="sticky top-10 h-10 bg-gradient-to-b from-transparent via-white to-white [mask-image:linear-gradient(to_bottom,transparent,black)]" />
|
||
<div className="absolute inset-0 -z-10 bg-gradient-to-r from-cyan-400/20 via-transparent to-blue-400/20" />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
|
||
|
||
function Header({
|
||
title,
|
||
className = "",
|
||
gradient = "none",
|
||
}: {
|
||
title: string;
|
||
className?: string;
|
||
// --- w sygnaturze Header:
|
||
gradient?: "none" | "warm" | "cool" | "coolSoft" | "sunset" | "bloom" | "bloomSoft" | "bloomReverse" | "prime" | "primeSoft" | "primeReverse";
|
||
|
||
}) {
|
||
const TEXT = {
|
||
warm: "text-transparent bg-clip-text bg-[linear-gradient(90deg,#FDBA74_0%,#F97316_55%,#F472B6_100%)]",
|
||
cool: "text-transparent bg-clip-text bg-[linear-gradient(90deg,#0284C7_0%,#2563EB_55%,#4F46E5_100%)]",
|
||
coolSoft: "text-transparent bg-clip-text bg-[linear-gradient(90deg,#38BDF8_0%,#3B82F6_55%,#6366F1_100%)]",
|
||
sunset: "text-transparent bg-clip-text bg-[linear-gradient(90deg,#FF7A00_0%,#FF3D81_70%)]",
|
||
// NOWE — motyw PRIME (Twoje kolory)
|
||
prime:
|
||
"text-transparent bg-clip-text bg-[linear-gradient(90deg,#F28241_0%,#CC2E47_100%)]",
|
||
primeSoft:
|
||
// delikatniejsza, nadal kończy się #8C2E47 jako najciemniejszym
|
||
"text-transparent bg-clip-text bg-[linear-gradient(90deg,#F7B891_0%,#F28241_45%,#8C2E47_100%)]",
|
||
primeReverse:
|
||
// jeśli potrzebujesz odwróconego kierunku, też kończymy #8C2E47
|
||
"text-transparent bg-clip-text bg-[linear-gradient(90deg,#F28241_0%,#8C2E47_100%)]",
|
||
|
||
// Twoja wersja
|
||
bloom:
|
||
"text-transparent bg-clip-text bg-[linear-gradient(90deg,#F59E0B,#F97316)]",
|
||
|
||
// Subtelniejsza na białym tle (lekko rozjaśnione odcienie)
|
||
bloomSoft:
|
||
"text-transparent bg-clip-text bg-[linear-gradient(90deg,#0E3E59_0%,#E68A56_40%,#96402A_56%,#C96A58_78%,#9A3A56_100%)]",
|
||
|
||
// Odwrócony kierunek (ciepłe → chłodne)
|
||
bloomReverse:
|
||
"text-transparent bg-clip-text bg-[linear-gradient(90deg,#8C2E47_0%,#BF533B_24%,#731702_45%,#F28241_70%,#083A59_100%)]",
|
||
|
||
none: "",
|
||
} as const;
|
||
|
||
const RULE = {
|
||
warm: "linear-gradient(90deg,rgba(253,186,116,.9),rgba(249,115,22,.9),rgba(244,114,182,.9))",
|
||
cool: "linear-gradient(90deg,rgba(2,132,199,.9),rgba(37,99,235,.9),rgba(79,70,229,.9))",
|
||
coolSoft: "linear-gradient(90deg,rgba(56,189,248,.9),rgba(59,130,246,.9),rgba(99,102,241,.9))",
|
||
sunset: "linear-gradient(90deg,#FF7A00,#FF3D81)",
|
||
bloom: "linear-gradient(90deg,#8C2E47,#083A59,#F28241,#731702,#BF533B)",
|
||
|
||
bloomSoft: "linear-gradient(90deg,#0E3E59,#E68A56,#96402A,#C96A58,#9A3A56)",
|
||
bloomReverse: "linear-gradient(90deg,#8C2E47,#BF533B,#731702,#F28241,#083A59)",
|
||
none: "linear-gradient(90deg,rgba(0,0,0,.12),rgba(0,0,0,.12))",
|
||
// NOWE — motyw PRIME (Twoje kolory)
|
||
prime: "linear-gradient(90deg,#F28241,#8C2E47)",
|
||
primeSoft: "linear-gradient(90deg,#F7B891,#F28241,#8C2E47)",
|
||
primeReverse: "linear-gradient(90deg,#F28241,#8C2E47)",
|
||
} as const;
|
||
|
||
return (
|
||
<header className="max-w-6xl mx-auto px-6 mb-8">
|
||
<h2
|
||
className={[
|
||
"inline-block",
|
||
"text-4xl md:text-5xl font-extrabold tracking-[-0.01em] antialiased",
|
||
"leading-[1.18] md:leading-[1.2] pt-[2px] pb-[6px]",
|
||
TEXT[gradient],
|
||
className,
|
||
].join(" ")}
|
||
>
|
||
{title}
|
||
</h2>
|
||
|
||
{/* <div className="mt-6 h-[3px] rounded-full [mask-image:linear-gradient(90deg,transparent,black_10%,black_90%,transparent)]" style={{ background: RULE[gradient] }} /> */}
|
||
</header>
|
||
);
|
||
}
|
||
|
||
|
||
|
||
function BackgroundMedia({
|
||
src,
|
||
poster,
|
||
sectionRef, // zostawiamy sygnaturę, ale hooka nie używamy
|
||
}: {
|
||
src: string;
|
||
poster?: string;
|
||
sectionRef: React.RefObject<HTMLElement | null>;
|
||
}) {
|
||
return (
|
||
<div className="absolute inset-0" aria-hidden>
|
||
<video
|
||
className="absolute inset-0 w-full h-full object-cover object-[60%_45%] bg-video"
|
||
autoPlay
|
||
loop
|
||
muted
|
||
playsInline
|
||
preload="metadata"
|
||
poster={poster}
|
||
>
|
||
<source src={src} type="video/mp4" />
|
||
</video>
|
||
|
||
{/* delikatne overlaye jak wcześniej */}
|
||
<div className="absolute inset-0 pointer-events-none">
|
||
<div className="absolute inset-0 bg-gradient-to-r from-black/45 via-black/10 to-transparent" />
|
||
<div className="absolute inset-0 bg-gradient-to-b from-black/10 via-transparent to-black/12" />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
|
||
|
||
|
||
type HeroPixabayBGProps = {
|
||
onScrollDown: () => void;
|
||
src?: string;
|
||
poster?: string;
|
||
};
|
||
|
||
function HeroPixabayBG({
|
||
onScrollDown,
|
||
src = "/visuals/20072-307163785_small.mp4",
|
||
poster = "/visuals/pixabay_bg_poster.jpg",
|
||
}: HeroPixabayBGProps) {
|
||
const sectionRef = React.useRef<HTMLElement | null>(null);
|
||
|
||
// 1) fail-safe: po montażu i tak pokaż treść (gdyby iOS nie wyzwolił IO)
|
||
const [mounted, setMounted] = React.useState(false);
|
||
React.useEffect(() => setMounted(true), []);
|
||
|
||
// 2) parallax
|
||
const { scrollYProgress } = useScroll({ target: sectionRef, offset: ["start start", "end start"] });
|
||
const titleY = useTransform(scrollYProgress, [0, 1], ["0%", "-6%"]);
|
||
const canParallax = mounted;
|
||
|
||
// 3) łagodniejszy trigger + margines
|
||
const seen = useInView(sectionRef, { once: true, amount: 0.3, margin: "-10% 0px -10% 0px" });
|
||
|
||
// 4) jeśli widoczny LUB zamontowany – pokazuj (koniec „pustki” na starcie)
|
||
const ready = seen || mounted;
|
||
|
||
// --- warianty ---
|
||
const h1V: Variants = {
|
||
hidden: { opacity: 0, y: 40, filter: "blur(4px)" },
|
||
show: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration: 1.2, ease: [0.22, 1, 0.36, 1] } },
|
||
};
|
||
const stackHero: Variants = {
|
||
hidden: { opacity: 0, y: 14 },
|
||
show: {
|
||
opacity: 1, y: 0,
|
||
transition: { duration: 0.8, ease: "easeOut", when: "beforeChildren", delayChildren: 0.3, staggerChildren: 0.22 },
|
||
},
|
||
};
|
||
const itemHero: Variants = {
|
||
hidden: { opacity: 0, y: 16 },
|
||
show: { opacity: 1, y: 0, transition: { duration: 0.85, ease: "easeOut" } },
|
||
};
|
||
const ruleHero: Variants = {
|
||
hidden: { opacity: 0, scaleX: 0 },
|
||
show: { opacity: 1, scaleX: 1, transition: { duration: 0.95, ease: "easeOut" } },
|
||
};
|
||
|
||
return (
|
||
<section
|
||
id="PrimeCode"
|
||
ref={sectionRef}
|
||
className="relative min-h-[100dvh] md:min-h-[calc(100svh-4rem)] isolate scroll-mt-16" // ← dvh na mobile
|
||
>
|
||
<div className="md:sticky md:top-16 md:min-h-[calc(100svh-4rem)]">
|
||
<BackgroundMedia src={src} poster={poster} sectionRef={sectionRef} />
|
||
|
||
<MotionConfig reducedMotion="never">
|
||
<div className="relative z-10 flex items-start md:items-start h-full text-white py-20 sm:py-24 md:pt-12 md:pb-60">
|
||
<div className="max-w-7xl mx-auto w-full px-4 sm:px-6 grid grid-cols-1 md:grid-cols-[1.12fr_0.88fr] gap-8 sm:gap-12 items-start md:items-center">
|
||
|
||
<motion.div
|
||
style={canParallax ? { y: titleY, willChange: "transform, opacity" } : undefined}
|
||
variants={h1V}
|
||
initial="hidden"
|
||
animate={ready ? "show" : "hidden"}
|
||
className="relative w-fit select-none pointer-events-none mb-5 sm:mb-6 md:mb-2 md:mt-10 lg:mt-14 md:min-h-[90px] mx-auto justify-self-center md:justify-self-start md:mx-0">
|
||
|
||
|
||
{/* WŁAŚCIWE LOGO – większe i wyśrodkowane na mobile, z offsetem w lewo od md */}
|
||
<img
|
||
src="/LogoPrimeCode-transparent.png"
|
||
alt=""
|
||
className="
|
||
h-auto
|
||
/* mobile – tak jak było: duże i czytelne */
|
||
w-[clamp(300px,88vw,760px)]
|
||
sm:w-[clamp(340px,80vw,780px)]
|
||
|
||
/* md – WYRAŹNIE większe (zawsze większe niż blok H2) */
|
||
md:w-[clamp(420px,48vw,680px)]
|
||
lg:w-[clamp(540px,46vw,760px)]
|
||
xl:w-[clamp(600px,44vw,820px)]
|
||
|
||
/* wyrównanie do lewego brzegu copy */
|
||
mx-auto md:mx-0
|
||
md:-ml-5 lg:-ml-6 xl:-ml-8
|
||
|
||
brightness-0 invert drop-shadow-[0_8px_22px_rgba(0,0,0,.45)]
|
||
"
|
||
draggable={false}
|
||
/>
|
||
</motion.div>
|
||
|
||
<h1 className="sr-only">Prime Code</h1>
|
||
|
||
|
||
|
||
|
||
{/* === COPY (z mobilnym scrimem) === */}
|
||
<motion.div
|
||
variants={stackHero}
|
||
initial="hidden"
|
||
animate={ready ? "show" : "hidden"}
|
||
transition={{ delay: 0.25 }}
|
||
className="md:col-start-1 md:row-start-2"
|
||
style={{ willChange: "transform, opacity" }}
|
||
>
|
||
{/* wrapper: tło pod tekstem tylko na mobile */}
|
||
<div className="relative md:p-0 p-4 md:mx-0 -mx-2 md:rounded-none rounded-2xl">
|
||
{/* scrim (tylko mobile) */}
|
||
<div
|
||
aria-hidden
|
||
className="md:hidden absolute inset-0 z-0
|
||
bg-black/10 backdrop-blur-[1.5px]"
|
||
/>
|
||
|
||
{/* właściwa treść */}
|
||
<div className="relative z-10">
|
||
<motion.h2 variants={itemHero} className="mt-2 text-3xl md:text-4xl font-semibold drop-shadow-[0_2px_8px_rgba(0,0,0,.55)]">
|
||
Integrujemy markę w codziennych działaniach
|
||
</motion.h2>
|
||
|
||
<motion.p variants={itemHero} className="mt-4 text-xl md:text-2xl text-white/95 drop-shadow-[0_2px_6px_rgba(0,0,0,.55)]">
|
||
Marka, która działa codziennie. W czasie rzeczywistym. W każdym punkcie styku.
|
||
</motion.p>
|
||
|
||
<motion.div variants={ruleHero} className="mt-6 h-[3px] w-28 origin-left bg-gradient-to-r from-cyan-400 to-blue-500" />
|
||
|
||
<motion.p variants={itemHero} className="max-w-3xl text-lg md:text-xl text-white/92 leading-relaxed mt-6 drop-shadow-[0_2px_6px_rgba(0,0,0,.55)]">
|
||
W Prime Code integrujemy markę z tym, co najważniejsze – realnymi działaniami, danymi,
|
||
technologią i zespołami. Nie tworzymy kampanii dla samej widoczności. Projektujemy
|
||
<strong> mechanizmy wzrostu</strong>, w których marka staje się fundamentem
|
||
operacyjnym: w komunikacji, sprzedaży, doświadczeniu klienta, polityce cenowej i kanałach dystrybucji.
|
||
</motion.p>
|
||
|
||
<motion.p variants={itemHero} className="mt-4 max-w-3xl text-lg md:text-xl text-white/92 leading-relaxed drop-shadow-[0_2px_6px_rgba(0,0,0,.55)]">
|
||
Bo marka <strong>to nie tylko opowieść.</strong> To system. Aktywny, skalowalny, zsynchronizowany z organizacją.
|
||
</motion.p>
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
|
||
</div>
|
||
|
||
<button
|
||
onClick={onScrollDown}
|
||
aria-label="Przewiń"
|
||
className="absolute bottom-8 left-1/2 -translate-x-1/2 text-white/90"
|
||
>
|
||
<svg xmlns="http://www.w3.org/2000/svg" className="h-9 w-9" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</MotionConfig>
|
||
</div >
|
||
</section >
|
||
);
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
// H1: wolno, z lekkim blur-em
|
||
const h1Slow: Variants = {
|
||
hidden: { opacity: 0, y: 30, filter: "blur(3px)" },
|
||
show: {
|
||
opacity: 1,
|
||
y: 0,
|
||
filter: "blur(0px)",
|
||
transition: { duration: 1.15, ease: [0.16, 1, 0.3, 1] },
|
||
},
|
||
};
|
||
|
||
// Kontener reszty treści (start po H1); w środku będzie stagger
|
||
const stack: Variants = {
|
||
hidden: { opacity: 0, y: 12 },
|
||
show: {
|
||
opacity: 1,
|
||
y: 0,
|
||
transition: {
|
||
duration: 0.75,
|
||
ease: "easeOut",
|
||
when: "beforeChildren",
|
||
// Stagger – wolniejszy rytm
|
||
staggerChildren: 0.18,
|
||
delayChildren: 0.12,
|
||
},
|
||
},
|
||
};
|
||
|
||
// Pojedyncze elementy treści (H2, lead, akapity)
|
||
const itemSlow: Variants = {
|
||
hidden: { opacity: 0, y: 12 },
|
||
show: {
|
||
opacity: 1,
|
||
y: 0,
|
||
transition: { duration: 0.7, ease: "easeOut" },
|
||
},
|
||
};
|
||
|
||
// Niebieska linia pod leadem – ładne „narysowanie”
|
||
// było: duration ~1.2, x:56
|
||
const itemV: Variants = {
|
||
hidden: (fromLeft: boolean) => ({
|
||
opacity: 0,
|
||
x: fromLeft ? -72 : 72,
|
||
y: 8,
|
||
filter: "blur(8px)",
|
||
}),
|
||
show: {
|
||
opacity: 1,
|
||
x: 0,
|
||
y: 0,
|
||
filter: "blur(0px)",
|
||
transition: { duration: 1.45, ease: [0.22, 1, 0.36, 1] }, // wolniej
|
||
},
|
||
};
|
||
|
||
|
||
type WhyContent = {
|
||
p1: React.ReactElement;
|
||
p2: React.ReactElement;
|
||
bulletsIntro: string;
|
||
bullets: string[];
|
||
p3: React.ReactElement;
|
||
};
|
||
|
||
function DlaczegoIntegracjiWarm({ why }: { why: WhyContent }) {
|
||
const items: React.ReactNode[] = [
|
||
why.p1,
|
||
why.p2,
|
||
(
|
||
<div className="text-[1.05rem] md:text-[1.15rem] leading-relaxed">
|
||
<p className="font-medium">{why.bulletsIntro}</p>
|
||
<ul className="mt-3 list-disc pl-6 space-y-2">
|
||
{why.bullets.map((b, i) => (
|
||
<li key={i}>{b}</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
),
|
||
why.p3,
|
||
];
|
||
|
||
// ——— Tytuł: powolne wejście + delikatny parallax kursorem ———
|
||
const titleRef = React.useRef<HTMLDivElement | null>(null);
|
||
const mx = useMotionValue(0);
|
||
const my = useMotionValue(0);
|
||
const onMoveTitle = (e: React.PointerEvent<HTMLDivElement>) => {
|
||
const r = titleRef.current?.getBoundingClientRect();
|
||
if (!r) return;
|
||
mx.set((e.clientX - (r.left + r.width / 2)) / r.width);
|
||
my.set((e.clientY - (r.top + r.height / 2)) / r.height);
|
||
};
|
||
const onLeaveTitle = () => { mx.set(0); my.set(0); };
|
||
|
||
const titleV: Variants = {
|
||
hidden: { opacity: 0, y: 22, filter: "blur(6px)" },
|
||
show: {
|
||
opacity: 1,
|
||
y: 0,
|
||
filter: "blur(0px)",
|
||
transition: { duration: 1.4, ease: [0.22, 1, 0.36, 1] },
|
||
},
|
||
};
|
||
|
||
const GRAD_H_L = "linear-gradient(90deg, rgba(253,186,116,1) 0%, rgba(249,115,22,1) 55%, rgba(244,114,182,1) 100%)";
|
||
const GRAD_H_R = "linear-gradient(270deg, rgba(253,186,116,1) 0%, rgba(249,115,22,1) 55%, rgba(244,114,182,1) 100%)";
|
||
const GRAD_V = "linear-gradient(180deg, rgba(253,186,116,1) 0%, rgba(249,115,22,1) 55%, rgba(244,114,182,1) 100%)";
|
||
|
||
const itemV: Variants = {
|
||
hidden: (fromLeft: boolean) => ({ opacity: 0, x: fromLeft ? -64 : 64, y: 8, filter: "blur(8px)" }),
|
||
show: { opacity: 1, x: 0, y: 0, filter: "blur(0px)", transition: { duration: 1.3, ease: [0.22, 1, 0.36, 1] } },
|
||
};
|
||
|
||
return (
|
||
<section id="DlaczegoIntegracji" className="relative py-28">
|
||
<header className="max-w-6xl mx-auto px-6">
|
||
<motion.div
|
||
ref={titleRef}
|
||
onPointerMove={onMoveTitle}
|
||
onPointerLeave={onLeaveTitle}
|
||
>
|
||
<motion.h2
|
||
variants={titleV}
|
||
initial="hidden"
|
||
whileInView="show"
|
||
viewport={{ once: true, amount: 0.55, margin: "-8% 0px -12% 0px" }} // ↓↓↓ KLUCZOWA ZMIANA
|
||
className={`${headingFont.className}
|
||
inline-block
|
||
text-[clamp(2rem,6vw,3.2rem)]
|
||
font-extrabold tracking-[-0.02em]
|
||
leading-[1.18]
|
||
pt-[2px] pb-[6px]
|
||
text-transparent bg-clip-text
|
||
bg-[linear-gradient(90deg,#F28241_0%,#CC2E47_100%)]
|
||
`}
|
||
|
||
>
|
||
Dlaczego integracja marki to konieczność?
|
||
</motion.h2>
|
||
</motion.div>
|
||
{/* <FancySeparator className="mt-5 max-w-4xl" /> */}
|
||
</header>
|
||
|
||
<div className="relative max-w-4xl mx-auto px-6 mt-12">
|
||
<div className="space-y-16">
|
||
{items.map((node, i) => {
|
||
const fromLeft = i % 2 === 0;
|
||
return (
|
||
<motion.article
|
||
key={i}
|
||
custom={fromLeft}
|
||
variants={itemV}
|
||
initial="hidden"
|
||
whileInView="show"
|
||
viewport={{ once: true, amount: 0.3, margin: "-6% 0px -14% 0px" }} // ↓↓↓ KLUCZOWA ZMIANA
|
||
className={fromLeft ? "md:pl-6" : "md:pr-6"}
|
||
>
|
||
<BracketCorner
|
||
side={fromLeft ? "left" : "right"}
|
||
gradH={fromLeft ? GRAD_H_L : GRAD_H_R}
|
||
gradV={GRAD_V}
|
||
/>
|
||
<div className="relative text-neutral-900 text-[1.07rem] md:text-[1.18rem] leading-relaxed pt-3">
|
||
{node}
|
||
</div>
|
||
</motion.article>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
|
||
|
||
/* narożnik w kształcie „L” (po lewej lub prawej), rysowany bez zwiększania box height */
|
||
function BracketCorner({
|
||
side,
|
||
gradH,
|
||
gradV,
|
||
}: {
|
||
side: "left" | "right";
|
||
gradH: string;
|
||
gradV: string;
|
||
}) {
|
||
const vLineV: Variants = {
|
||
hidden: { opacity: 0.9, scaleY: 0 },
|
||
show: { opacity: 1, scaleY: 1, transition: { duration: 1.15, ease: "easeOut" } },
|
||
};
|
||
const hLineV: Variants = {
|
||
hidden: { opacity: 0.9, scaleX: 0 },
|
||
show: { opacity: 1, scaleX: 1, transition: { duration: 1.15, ease: "easeOut", delay: 0.05 } },
|
||
};
|
||
|
||
return (
|
||
<div aria-hidden className="pointer-events-none absolute inset-0">
|
||
{/* pionowa kreska */}
|
||
<motion.div
|
||
variants={vLineV}
|
||
initial="hidden"
|
||
whileInView="show"
|
||
viewport={{ once: true, amount: 0.95 }}
|
||
className={`absolute top-0 ${side === "left"
|
||
? "left-[-10px] md:-left-4 origin-top" // ↓↓↓ KLUCZOWA ZMIANA
|
||
: "right-[-10px] md:-right-4 origin-top" // ↓↓↓ KLUCZOWA ZMIANA
|
||
} w-[3px] h-8 md:h-10 rounded-full`}
|
||
style={{ background: gradV }}
|
||
/>
|
||
{/* pozioma kreska */}
|
||
<motion.div
|
||
variants={hLineV}
|
||
initial="hidden"
|
||
whileInView="show"
|
||
viewport={{ once: true, amount: 0.95 }}
|
||
className={`absolute top-0 h-[3px] w-24 md:w-28 rounded-full ${side === "left"
|
||
? "left-[-10px] md:-left-4 origin-left" // ↓↓↓ KLUCZOWA ZMIANA
|
||
: "right-[-10px] md:-right-4 origin-right" // ↓↓↓ KLUCZOWA ZMIANA
|
||
}`}
|
||
style={{ background: gradH }}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
|
||
|
||
|
||
// function FancySeparator({ className = "" }: { className?: string }) {
|
||
// return (
|
||
// <div className={`relative w-full ${className}`}>
|
||
// {/* główna linia (gradient, przygaszone końce) */}
|
||
// <div
|
||
// className="h-[2px] w-full rounded-full"
|
||
// style={{
|
||
// background:
|
||
// "linear-gradient(90deg, rgba(253,186,116,1) 0%, rgba(249,115,22,1) 55%, rgba(244,114,182,1) 100%)",
|
||
// WebkitMaskImage:
|
||
// "linear-gradient(90deg, transparent 0, black 8%, black 92%, transparent 100%)",
|
||
// maskImage:
|
||
// "linear-gradient(90deg, transparent 0, black 8%, black 92%, transparent 100%)",
|
||
// }}
|
||
// />
|
||
// {/* subtelny highlight nad linią */}
|
||
// <div
|
||
// className="absolute inset-0 -translate-y-[2px] h-[1px] bg-white/60 mix-blend-screen"
|
||
// style={{
|
||
// WebkitMaskImage:
|
||
// "linear-gradient(90deg, transparent 0, black 10%, black 90%, transparent 100%)",
|
||
// maskImage:
|
||
// "linear-gradient(90deg, transparent 0, black 10%, black 90%, transparent 100%)",
|
||
// }}
|
||
// />
|
||
// {/* delikatny glow */}
|
||
// <div
|
||
// className="pointer-events-none absolute -inset-x-1 -inset-y-[6px] blur-md"
|
||
// style={{
|
||
// background:
|
||
// "linear-gradient(90deg, rgba(253,186,116,.18), rgba(249,115,22,.18), rgba(244,114,182,.18))",
|
||
// WebkitMaskImage:
|
||
// "linear-gradient(90deg, transparent 0, black 12%, black 88%, transparent 100%)",
|
||
// maskImage:
|
||
// "linear-gradient(90deg, transparent 0, black 12%, black 88%, transparent 100%)",
|
||
// }}
|
||
// />
|
||
// </div>
|
||
// );
|
||
// }
|
||
|
||
|
||
/* ======================= CZY ZNASZ TE SYTUACJE? (nowa wersja) ======================= */
|
||
|
||
// === ANIMACJE SEKCJI "SYTUACJE" ===
|
||
|
||
function SytuacjeSection() {
|
||
// warianty: nagłówek i siatka wchodzą dopiero przy wejściu w viewport
|
||
const sectionV: Variants = {
|
||
hidden: { opacity: 0, y: 18, filter: 'blur(6px)' },
|
||
show: {
|
||
opacity: 1,
|
||
y: 0,
|
||
filter: 'blur(0px)',
|
||
transition: { duration: 0.55, ease: 'easeOut' },
|
||
},
|
||
};
|
||
|
||
// siatka – tylko steruje staggerem dzieci
|
||
const gridV: Variants = {
|
||
hidden: { opacity: 1 }, // niewidoczność trzymamy na kartach
|
||
show: {
|
||
opacity: 1,
|
||
transition: {
|
||
delayChildren: 0.12,
|
||
staggerChildren: 0.08,
|
||
ease: 'easeOut',
|
||
},
|
||
},
|
||
};
|
||
|
||
const items = [
|
||
{
|
||
title: 'Brak spójności w komunikacji',
|
||
text: 'Różne działy (np. marketing, sprzedaż, obsługa klienta) komunikują się w różnym tonie i z odmiennym przekazem.',
|
||
Icon: MessageSquare,
|
||
accent: 'from-sky-300 to-blue-500', // było: from-sky-400 to-blue-500
|
||
},
|
||
{
|
||
title: 'Realizacja działań promocyjnych bez odniesienia do tożsamości marki',
|
||
text: 'Firma wdraża modne działania (np. TikTok, influencerzy, AI copywriting) bez filtrowania ich przez tożsamość i wartości marki.',
|
||
Icon: Megaphone,
|
||
accent: 'from-cyan-300 to-sky-500', // było: from-cyan-400 to-sky-600
|
||
},
|
||
{
|
||
title: 'Problemy w skalowaniu działań',
|
||
text: 'Przy szybkim wzroście lub ekspansji różne zespoły zaczynają prowadzić działania niespójne z tożsamością marki.',
|
||
Icon: LineChart,
|
||
accent: 'from-blue-300 to-indigo-500', // było: from-blue-400 to-indigo-600
|
||
},
|
||
{
|
||
title: 'Zmiany w zespole / outsourcing marketingu',
|
||
text: 'Nowe osoby lub agencje marketingowe nie rozumieją w pełni marki i działają według własnych schematów.',
|
||
Icon: Users,
|
||
accent: 'from-sky-300 to-blue-500',
|
||
},
|
||
{
|
||
title: 'Brak narzędzi lub procesów ułatwiających pracę z marką',
|
||
text: 'Pracownicy nie mają dostępu do aktualnych wytycznych (brand plan, key visuals, archetypy, kroki milowe).',
|
||
Icon: Wrench,
|
||
accent: 'from-cyan-300 to-blue-500', // łagodniej
|
||
},
|
||
{
|
||
title: 'Brak mierzalnego modelu integracji marki',
|
||
text: 'Firma nie ma sposobu, by sprawdzić, czy kampania, post czy oferta są zgodne z marką.',
|
||
Icon: ClipboardCheck,
|
||
accent: 'from-sky-300 to-indigo-500',
|
||
},
|
||
];
|
||
|
||
|
||
return (
|
||
<section id="Sytuacje" className="relative py-24">
|
||
{/* tło + delikatne rozświetlenie na wejściu */}
|
||
<SytuacjeBackground />
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
whileInView={{ opacity: 1 }}
|
||
viewport={{ once: true, amount: 0.25 }}
|
||
transition={{ duration: 0.5, ease: 'easeOut' }}
|
||
className="absolute inset-0 -z-10"
|
||
/>
|
||
|
||
{/* nagłówek wsuwa się miękko przy wejściu w viewport */}
|
||
<motion.div
|
||
variants={sectionV}
|
||
initial="hidden"
|
||
whileInView="show"
|
||
viewport={{ once: true, amount: 0.55 }}
|
||
>
|
||
<Header
|
||
title="Czy znasz te sytuacje?"
|
||
gradient="coolSoft"
|
||
className={sytuacjeFont.className}
|
||
/>
|
||
|
||
</motion.div>
|
||
|
||
|
||
|
||
{/* siatka – karty z kaskadą */}
|
||
<motion.div
|
||
variants={gridV}
|
||
initial="hidden"
|
||
whileInView="show"
|
||
viewport={{ once: true, amount: 0.35 }}
|
||
className="max-w-6xl mx-auto px-6 grid gap-5 md:grid-cols-2 lg:grid-cols-3 items-stretch"
|
||
>
|
||
{items.map((it, i) => (
|
||
<SituationCardLite
|
||
key={i}
|
||
index={i}
|
||
title={it.title}
|
||
text={it.text}
|
||
Icon={it.Icon}
|
||
accent={it.accent}
|
||
/>
|
||
))}
|
||
</motion.div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function SituationCardLite({
|
||
index,
|
||
title,
|
||
text,
|
||
Icon,
|
||
accent,
|
||
}: {
|
||
index: number;
|
||
title: string;
|
||
text: string;
|
||
Icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
||
accent: string;
|
||
}) {
|
||
const fromLeft = index % 2 === 0;
|
||
|
||
// wariant pojedynczej karty – krótkie i miękkie wejście
|
||
const cardV: Variants = {
|
||
hidden: {
|
||
opacity: 0,
|
||
y: 12,
|
||
x: fromLeft ? -6 : 6,
|
||
scale: 0.985,
|
||
filter: 'blur(4px)',
|
||
},
|
||
show: {
|
||
opacity: 1,
|
||
y: 0,
|
||
x: 0,
|
||
scale: 1,
|
||
filter: 'blur(0px)',
|
||
transition: { duration: 0.45, ease: 'easeOut' },
|
||
},
|
||
};
|
||
|
||
return (
|
||
<motion.article
|
||
variants={cardV}
|
||
className="h-full relative overflow-hidden rounded-none bg-white ring-1 ring-black/8 hover:ring-black/10 transition
|
||
shadow-none hover:shadow-[0_8px_24px_rgba(16,24,40,0.06)]"
|
||
>
|
||
{/* cienka linia akcentowa u góry */}
|
||
<div className={`absolute inset-x-0 top-0 h-[2px] bg-gradient-to-r ${accent}`} />
|
||
|
||
{/* ultra-delikatny pattern w tle */}
|
||
<div className="absolute inset-0 pointer-events-none opacity-[0.04]
|
||
[background-image:radial-gradient(#0ea5e9_1px,transparent_1px)]
|
||
[background-size:16px_16px]" />
|
||
|
||
<div className="p-5">
|
||
<div className="grid grid-cols-[2.25rem_1fr] items-start gap-3
|
||
min-h-[5.6rem] md:min-h-[4rem]">
|
||
<div className={`shrink-0 grid place-items-center h-9 w-9 rounded-full bg-gradient-to-br ${accent} text-white`}>
|
||
<Icon className="h-[18px] w-[18px]" aria-hidden />
|
||
</div>
|
||
<h3 className="text-[1.02rem] font-semibold leading-[1.3]">
|
||
{title}
|
||
</h3>
|
||
</div>
|
||
|
||
<p className="mt-2 text-[0.94rem] leading-relaxed text-neutral-700">{text}</p>
|
||
</div>
|
||
</motion.article>
|
||
);
|
||
}
|
||
|
||
|
||
/* --------------------------- Płynące tło (scroll) --------------------------- */
|
||
|
||
/* ---------------------- TŁO SEKCJI: siatka + belka ---------------------- */
|
||
/* ---------------------- TŁO SEKCJI: siatka + ciepło/zimny split + belka ---------------------- */
|
||
function SytuacjeBackground() {
|
||
const ref = React.useRef<HTMLDivElement | null>(null);
|
||
const { scrollYProgress } = useScroll({ target: ref, offset: ['start end', 'end start'] });
|
||
const prefersReduced = useReducedMotion();
|
||
|
||
// >>> ważne: dopiero po mountcie włączamy x/y
|
||
const [mounted, setMounted] = React.useState(false);
|
||
React.useEffect(() => setMounted(true), []);
|
||
|
||
const y = useTransform(scrollYProgress, [0, 1], prefersReduced ? [0, 0] : [8, -8]);
|
||
const x = useTransform(scrollYProgress, [0, 1], prefersReduced ? [0, 0] : [-6, 6]);
|
||
|
||
return (
|
||
<div ref={ref} aria-hidden className="absolute inset-0 -z-10 overflow-visible">
|
||
<div
|
||
className="absolute inset-0"
|
||
style={{ background: 'linear-gradient(to bottom, #fafbfd 0%, #f4f6fb 35%, #eef2f9 65%, #f7f9fd 100%)' }}
|
||
/>
|
||
|
||
<motion.div
|
||
// suppressHydrationWarning nie jest konieczne, ale pomaga gdyby devtools nadal krzyczał
|
||
suppressHydrationWarning
|
||
className="absolute inset-0 pointer-events-none"
|
||
style={{
|
||
// przed mountem: brak translacji (SSR = CSR)
|
||
x: mounted ? x : 0,
|
||
y: mounted ? y : 0,
|
||
background: [
|
||
'radial-gradient(560px 280px at 18% 12%, rgba(255,255,255,.40), transparent 68%)',
|
||
'radial-gradient(640px 320px at 80% 18%, rgba(255,255,255,.35), transparent 70%)',
|
||
'radial-gradient(700px 360px at 72% 82%, rgba(255,255,255,.30), transparent 70%)',
|
||
].join(','),
|
||
}}
|
||
/>
|
||
|
||
<div className="absolute inset-x-0 -top-8 h-8 pointer-events-none"
|
||
style={{ background: 'linear-gradient(to bottom, #ffffff, rgba(255,255,255,0))' }} />
|
||
<div className="absolute inset-x-0 -bottom-8 h-8 pointer-events-none"
|
||
style={{ background: 'linear-gradient(to top, #ffffff, rgba(255,255,255,0))' }} />
|
||
<div className="absolute inset-0 pointer-events-none"
|
||
style={{
|
||
WebkitMaskImage: 'linear-gradient(to bottom, transparent 0%, black 2.5%, black 97.5%, transparent 100%)',
|
||
maskImage: 'linear-gradient(to bottom, transparent 0%, black 2.5%, black 97.5%, transparent 100%)',
|
||
}}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
|
||
// --- PartnerSection: nagłówek + wcześniejszy start listy ---
|
||
function PartnerSection() {
|
||
const items = [
|
||
'myśli procesami, nie kanałami?',
|
||
'łączy branding z danymi, CX i automatyzacją?',
|
||
'projektuje mechanizmy wzrostu, a nie tylko „fajny content”?',
|
||
'wdraża AI w obsłudze marki, zamiast tylko mówić o trendach?',
|
||
'testuje i optymalizuje, zamiast zgadywać?',
|
||
];
|
||
|
||
const headingV: Variants = {
|
||
hidden: { opacity: 0, y: 14, filter: 'blur(6px)' },
|
||
show: { opacity: 1, y: 0, filter: 'blur(0px)', transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] } },
|
||
};
|
||
|
||
return (
|
||
<section id="Partner" className="relative py-24 bg-white">
|
||
{/* Kreski w tle — ważne: z-0 (nad tłem sekcji), pointer-events-none */}
|
||
{/* <FlowThread className="hidden md:block z-0 pointer-events-none" /> */}
|
||
|
||
{/* Treść nad kreskami */}
|
||
<div className="relative z-10">
|
||
<motion.div
|
||
variants={headingV}
|
||
initial="hidden"
|
||
whileInView="show"
|
||
viewport={{ once: true, amount: 0.95 }}
|
||
>
|
||
<Header title="Potrzebujesz partnera, który:" gradient="prime" />
|
||
</motion.div>
|
||
|
||
<div className="max-w-5xl mx-auto px-6">
|
||
<NumberedList
|
||
items={items}
|
||
viewportMargin="0px 0px 22% 0px"
|
||
startDelay={0.08}
|
||
stagger={0.24}
|
||
itemDuration={0.9}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
// --- NumberedList: kontrola wcześniejszego wyzwalania + tempa ---
|
||
function NumberedList({
|
||
items,
|
||
viewportMargin = '0px',
|
||
startDelay = 0.02,
|
||
stagger = 0.24,
|
||
itemDuration = 0.9,
|
||
}: {
|
||
items: string[];
|
||
viewportMargin?: string;
|
||
startDelay?: number;
|
||
stagger?: number;
|
||
itemDuration?: number;
|
||
}) {
|
||
return (
|
||
// NumberedList — wymagaj większej części elementu w kadrze
|
||
<motion.ol
|
||
initial="hidden"
|
||
whileInView="show"
|
||
viewport={{ once: true, amount: 0.25, margin: viewportMargin }} // było: amount 0.10
|
||
variants={{
|
||
hidden: {},
|
||
show: { transition: { delayChildren: startDelay, staggerChildren: stagger } },
|
||
}}
|
||
className="divide-y divide-neutral-200"
|
||
>
|
||
|
||
{items.map((t, i) => (
|
||
<NumberedRow key={i} index={i} text={t} duration={itemDuration} />
|
||
))}
|
||
</motion.ol>
|
||
);
|
||
}
|
||
|
||
|
||
// --- NumberedRow: wolniejsze pojedyncze wejście (bez zmiany kolorów punktów) ---
|
||
function NumberedRow({ index, text, duration }: { index: number; text: string; duration: number }) {
|
||
const n = String(index + 1).padStart(2, '0');
|
||
const [first, ...rest] = text.split(' ');
|
||
const restText = rest.join(' ');
|
||
|
||
const itemV = {
|
||
hidden: { opacity: 0, y: 14, filter: 'blur(6px)' },
|
||
show: { opacity: 1, y: 0, filter: 'blur(0px)', transition: { duration, ease: [0.22, 1, 0.36, 1] } },
|
||
} as const;
|
||
|
||
return (
|
||
<motion.li variants={itemV} className="relative grid grid-cols-[82px_1fr] md:grid-cols-[110px_1fr] items-baseline py-6">
|
||
<div className="relative">
|
||
<motion.span
|
||
initial={{ opacity: 0, y: 10, scale: 0.97 }}
|
||
whileInView={{ opacity: 1, y: 0, scale: 1 }}
|
||
viewport={{ once: true }}
|
||
transition={{ duration: duration * 0.8, ease: 'easeOut' }}
|
||
className="block select-none tabular-nums font-extrabold leading-none
|
||
text-transparent bg-clip-text
|
||
bg-[linear-gradient(180deg,#FDBA74_0%,#F97316_45%,#F472B6_100%)]
|
||
text-[3.2rem] md:text-[4.4rem]"
|
||
aria-hidden
|
||
>
|
||
{n}
|
||
</motion.span>
|
||
</div>
|
||
|
||
<div className="relative">
|
||
<div className="flex flex-wrap items-baseline gap-x-3">
|
||
<span className="text-2xl md:text-3xl font-extrabold tracking-[-0.01em]">{first}</span>
|
||
<span className="text-[1.05rem] md:text-[1.15rem] text-neutral-800">{restText}</span>
|
||
</div>
|
||
<motion.div
|
||
initial={{ scaleX: 0, opacity: 0 }}
|
||
whileInView={{ scaleX: 1, opacity: 1 }}
|
||
viewport={{ once: true }}
|
||
transition={{ duration: duration * 0.7, ease: 'easeOut', delay: 0.05 }}
|
||
className="mt-3 h-[2px] origin-left rounded-full
|
||
bg-[linear-gradient(90deg,rgba(253,186,116,.85),rgba(249,115,22,.85),rgba(244,114,182,.85))]
|
||
[mask-image:linear-gradient(90deg,transparent,black_12%,black_88%,transparent)]"
|
||
/>
|
||
</div>
|
||
</motion.li>
|
||
);
|
||
}
|
||
|
||
|
||
/* ======================= MODEL PRIME CODE — fixed ======================= */
|
||
|
||
type ModelTile = {
|
||
title: string;
|
||
short: string;
|
||
output: string;
|
||
efekt: string;
|
||
details: React.ReactNode;
|
||
};
|
||
|
||
function ModelSection({ tiles }: { tiles: ModelTile[] }) {
|
||
const [openIndex, setOpenIndex] = useState<number | null>(null); // <- tylko jedna otwarta
|
||
|
||
const headingV: Variants = {
|
||
hidden: { opacity: 0, y: 18, filter: 'blur(6px)' },
|
||
show: { opacity: 1, y: 0, filter: 'blur(0px)', transition: { duration: 0.55, ease: 'easeOut' } },
|
||
};
|
||
const gridV: Variants = {
|
||
hidden: { opacity: 1 },
|
||
show: { opacity: 1, transition: { delayChildren: 0.12, staggerChildren: 0.08, ease: 'easeOut' } },
|
||
};
|
||
|
||
return (
|
||
<section id="Model" className="relative py-24">
|
||
<RiskFreeBackground />
|
||
|
||
<motion.div variants={headingV} initial="hidden" whileInView="show" viewport={{ once: true, amount: 0.55 }}>
|
||
<Header
|
||
title="Model Prime Code: marka jako system operacyjny"
|
||
gradient="coolSoft"
|
||
className={sytuacjeFont.className}
|
||
/>
|
||
<div className="max-w-6xl mx-auto px-6">
|
||
<p className="text-lg text-neutral-700 max-w-3xl">
|
||
W klasycznym ujęciu marka to obietnica. W dzisiejszej rzeczywistości to{' '}
|
||
<strong>system działania</strong>. Prime Code traktuje markę jak{' '}
|
||
<strong>system operacyjny</strong> firmy: regulujący rytm decyzji,
|
||
komunikacji i doświadczeń klienta. Spójność przestaje być tylko
|
||
kwestią estetyki a staje się{' '}
|
||
<strong>mierzalnym źródłem przewagi</strong>.
|
||
</p>
|
||
</div>
|
||
</motion.div>
|
||
|
||
<motion.div
|
||
variants={gridV}
|
||
initial="hidden"
|
||
whileInView="show"
|
||
viewport={{ once: true, amount: 0.35 }}
|
||
className="max-w-6xl mx-auto px-6 grid gap-5 md:grid-cols-2 lg:grid-cols-3 mt-10 items-start" // <- items-start!
|
||
>
|
||
{tiles.map((t, i) => (
|
||
<ModelCard
|
||
key={i}
|
||
index={i}
|
||
tile={t}
|
||
open={openIndex === i}
|
||
onToggle={() => setOpenIndex(prev => (prev === i ? null : i))} // <- tylko jedna otwarta
|
||
/>
|
||
))}
|
||
</motion.div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function ModelCard({
|
||
index,
|
||
tile,
|
||
open,
|
||
onToggle,
|
||
}: {
|
||
index: number;
|
||
tile: ModelTile;
|
||
open: boolean;
|
||
onToggle: () => void;
|
||
}) {
|
||
const fromLeft = index % 2 === 0;
|
||
|
||
const cardV: Variants = {
|
||
hidden: { opacity: 0, y: 12, x: fromLeft ? -6 : 6, scale: 0.985, filter: 'blur(4px)' },
|
||
show: { opacity: 1, y: 0, x: 0, scale: 1, filter: 'blur(0px)', transition: { duration: 0.45, ease: 'easeOut' } },
|
||
};
|
||
|
||
return (
|
||
<motion.article
|
||
variants={cardV}
|
||
layout="position" // <- pozycja sąsiadów, bez „poszerzania”
|
||
className="self-start relative overflow-hidden rounded-none bg-white ring-1 ring-black/8 hover:ring-black/10 transition
|
||
shadow-none hover:shadow-[0_8px_24px_rgba(16,24,40,0.06)]"
|
||
>
|
||
<div className="absolute inset-x-0 top-0 h-[2px] bg-gradient-to-r from-sky-300 via-blue-400 to-indigo-500" />
|
||
<div className="absolute inset-0 pointer-events-none opacity-[0.04]
|
||
[background-image:radial-gradient(#0ea5e9_1px,transparent_1px)]
|
||
[background-size:16px_16px]" />
|
||
|
||
<div className="p-5">
|
||
<div className="flex items-start gap-3">
|
||
<div className="shrink-0 mt-[2px] grid place-items-center h-9 w-9 rounded-full bg-gradient-to-br from-sky-300 to-indigo-500 text-white">
|
||
<span className="text-sm font-bold">{String(index + 1).padStart(2, '0')}</span>
|
||
</div>
|
||
<h3 className="text-[1.02rem] font-semibold leading-snug">{tile.title}</h3>
|
||
</div>
|
||
|
||
<p className="mt-2 text-[0.94rem] leading-relaxed text-neutral-700">{tile.short}</p>
|
||
|
||
{!open && (
|
||
<button onClick={onToggle} className="mt-4 underline underline-offset-4 text-left cursor-pointer" aria-expanded={open}>
|
||
Pokaż szczegóły
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
<AnimatePresence initial={false}>
|
||
{open && (
|
||
<motion.div
|
||
key="details"
|
||
initial={{ opacity: 0, height: 0 }}
|
||
animate={{ opacity: 1, height: 'auto' }}
|
||
exit={{ opacity: 0, height: 0 }}
|
||
transition={{ duration: 0.35, ease: 'easeOut' }}
|
||
className="px-5 pb-5"
|
||
>
|
||
<div className="text-[0.95rem] text-neutral-800 leading-relaxed [&_ul]:my-2 [&_li]:leading-relaxed">
|
||
<div className="prose prose-neutral max-w-none">{tile.details}</div>
|
||
<p className="mt-3 text-sm"><span className="font-semibold">Output:</span> {tile.output}</p>
|
||
<p className="mt-1 text-sm"><span className="font-semibold">Efekt:</span> {tile.efekt}</p>
|
||
</div>
|
||
|
||
<button onClick={onToggle} className="mt-4 underline underline-offset-4 text-left cursor-pointer">
|
||
Zwiń
|
||
</button>
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
</motion.article>
|
||
);
|
||
}
|
||
|
||
/* =================== CO POTRAFIMY — ostre, czyste =================== */
|
||
|
||
function CoPotrafimySection() {
|
||
// dokładnie te teksty z Twojej pierwotnej wersji
|
||
const items = [
|
||
"Projektujemy marki, które rosną dzięki danym i doświadczeniu użytkownika",
|
||
"Tworzymy systemy komunikacji, a nie tylko tone of voice",
|
||
"Zarządzamy marką tak jak innowacyjnym produktem lub usługą: zwinnie, testy, feedback, iteracje",
|
||
"Wdrażamy AI w miejscach, które mają znaczenie dla doświadczenia marki",
|
||
"Projektujemy onboarding, optymalizujemy punkty styku z klientem i mechanizmy wzrostu",
|
||
"Projektujemy szablony i piszemy playbooki dla marketingu, sprzedaży, HR i obsługi klienta z marką w centrum",
|
||
];
|
||
|
||
const prefersReduced = (useReducedMotion() ?? false);
|
||
|
||
const titleV: Variants = {
|
||
hidden: { opacity: 0, y: 22, filter: "blur(6px)" },
|
||
show: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration: 1.0, ease: [0.22, 1, 0.36, 1] } },
|
||
};
|
||
const gridV: Variants = { hidden: {}, show: { transition: { delayChildren: 0.12, staggerChildren: 0.08 } } };
|
||
|
||
return (
|
||
<section id="CoPotrafimy" className="relative py-28">
|
||
{/* <FlowThread className="hidden md:block" /> */}
|
||
|
||
<header className="max-w-6xl mx-auto px-6 mb-10 md:mb-14">
|
||
<motion.h2
|
||
variants={titleV}
|
||
initial="hidden"
|
||
whileInView="show"
|
||
viewport={{ once: true, amount: 0.95 }}
|
||
className={`${headingFont.className}
|
||
inline-block text-[clamp(2rem,6vw,3.2rem)] font-extrabold tracking-[-0.02em]
|
||
text-transparent bg-clip-text
|
||
bg-[linear-gradient(90deg,#F28241_0%,#CC2E47_100%)]`}
|
||
>
|
||
Co potrafimy?
|
||
</motion.h2>
|
||
{/* <FancySeparator className="mt-5 max-w-4xl" /> */}
|
||
</header>
|
||
|
||
<motion.ul
|
||
variants={gridV}
|
||
initial="hidden"
|
||
whileInView="show"
|
||
viewport={{ once: true, amount: 0.35 }}
|
||
className="max-w-6xl mx-auto px-6 grid gap-5 md:grid-cols-2"
|
||
>
|
||
{items.map((txt, i) => (
|
||
<CoCard key={i} index={i} text={txt} prefersReduced={prefersReduced} />
|
||
))}
|
||
</motion.ul>
|
||
|
||
<motion.p
|
||
initial={{ opacity: 0, y: 10 }}
|
||
whileInView={{ opacity: 1, y: 0 }}
|
||
viewport={{ once: true, amount: 0.7 }}
|
||
transition={{ duration: 0.5, ease: "easeOut", delay: 0.06 }}
|
||
className="max-w-6xl mx-auto px-6 mt-12 text-lg"
|
||
>
|
||
|
||
</motion.p>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
/* ---------- KARTA: ostre krawędzie, bez dodatkowych tekstów ---------- */
|
||
function CoCard({
|
||
index,
|
||
text,
|
||
prefersReduced,
|
||
}: {
|
||
index: number;
|
||
text: string;
|
||
prefersReduced: boolean;
|
||
}) {
|
||
const fromLeft = index % 2 === 0;
|
||
|
||
const cardV: Variants = {
|
||
hidden: { opacity: 0, y: 14, x: fromLeft ? -8 : 8, scale: 0.985, filter: "blur(6px)" },
|
||
show: { opacity: 1, y: 0, x: 0, scale: 1, filter: "blur(0px)", transition: { duration: 0.55, ease: "easeOut" } },
|
||
};
|
||
|
||
// micro-tilt (z poszanowaniem prefers-reduced-motion)
|
||
const rx = useMotionValue(0);
|
||
const ry = useMotionValue(0);
|
||
const rotateX = useSpring(useTransform(ry, [-0.5, 0.5], [6, -6]), { stiffness: 140, damping: 12 });
|
||
const rotateY = useSpring(useTransform(rx, [-0.5, 0.5], [-6, 6]), { stiffness: 140, damping: 12 });
|
||
const onMove = (e: React.PointerEvent<HTMLLIElement>) => {
|
||
const r = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||
rx.set((e.clientX - (r.left + r.width / 2)) / r.width);
|
||
ry.set((e.clientY - (r.top + r.height / 2)) / r.height);
|
||
};
|
||
|
||
return (
|
||
<motion.li
|
||
variants={cardV}
|
||
whileHover={prefersReduced ? undefined : { y: -4, scale: 1.01 }}
|
||
onPointerMove={prefersReduced ? undefined : onMove}
|
||
onPointerLeave={() => { rx.set(0); ry.set(0); }}
|
||
style={prefersReduced ? undefined : { rotateX, rotateY }}
|
||
className="group relative overflow-hidden rounded-none bg-white/70 backdrop-blur-xl ring-1 ring-black/10
|
||
shadow-[0_10px_28px_rgba(16,24,40,0.08)]"
|
||
>
|
||
{/* cienka ciepła belka u góry */}
|
||
<div className="absolute inset-x-0 top-0 h-[2px] bg-gradient-to-r from-amber-400 via-rose-400 to-fuchsia-500" />
|
||
|
||
{/* bardzo subtelny pattern w tle */}
|
||
<div className="absolute inset-0 pointer-events-none opacity-[0.03]
|
||
[background-image:radial-gradient(#fb7185_1px,transparent_1px)]
|
||
[background-size:18px_18px]" />
|
||
|
||
{/* jednorazowy „shine” przy wejściu w viewport */}
|
||
<motion.span
|
||
initial={{ x: "-30%", opacity: 0 }}
|
||
whileInView={{ x: "130%", opacity: 0.35 }}
|
||
viewport={{ once: true, amount: 0.85 }}
|
||
transition={{ duration: 0.9, ease: [0.22, 1, 0.36, 1] }}
|
||
className="pointer-events-none absolute left-0 right-0 top-1/2 -translate-y-1/2 h-10
|
||
bg-gradient-to-r from-white/0 via-white/70 to-white/0"
|
||
/>
|
||
|
||
{/* pionowy akcent (spójny z całą stroną) */}
|
||
<motion.span
|
||
initial={{ scaleY: 0 }}
|
||
whileInView={{ scaleY: 1 }}
|
||
viewport={{ once: true, amount: 0.9 }}
|
||
transition={{ duration: 0.65, ease: "easeOut" }}
|
||
className="absolute left-0 top-0 origin-top h-full w-[3px]
|
||
bg-[linear-gradient(180deg,rgba(253,186,116,1),rgba(249,115,22,1),rgba(244,114,182,1))]"
|
||
aria-hidden
|
||
/>
|
||
|
||
<div className="p-5 md:p-6">
|
||
<div className="flex items-start gap-4">
|
||
{/* numer — można zostawić (to nie “tekst” dodany, a etykieta) */}
|
||
<div className="shrink-0 grid place-items-center h-9 w-9 text-white
|
||
bg-[linear-gradient(135deg,#FDBA74,#F472B6)]">
|
||
<span className="text-sm font-bold tabular-nums">{String(index + 1).padStart(2, "0")}</span>
|
||
</div>
|
||
|
||
{/* wyłącznie główny tekst z pierwotnej listy */}
|
||
<p className="text-[1.02rem] leading-relaxed text-neutral-900">{text}</p>
|
||
</div>
|
||
</div>
|
||
</motion.li>
|
||
);
|
||
}
|
||
|
||
/* ---------- tło: animowana, cienka „nić” w gradiencie (zero glow) ---------- */
|
||
function FlowThread({ className = "" }: { className?: string }) {
|
||
return (
|
||
<div aria-hidden className={`absolute inset-0 -z-10 ${className}`}>
|
||
<motion.svg
|
||
viewBox="0 0 1200 600"
|
||
className="absolute inset-0 w-full h-full"
|
||
preserveAspectRatio="none"
|
||
>
|
||
<defs>
|
||
<linearGradient id="thread" x1="0" x2="1" y1="0" y2="0">
|
||
<stop offset="0%" stopColor="#FDBA74" />
|
||
<stop offset="55%" stopColor="#F472B6" />
|
||
<stop offset="100%" stopColor="#3B82F6" />
|
||
</linearGradient>
|
||
</defs>
|
||
{/* miękkie esy-floresy za kartami, ale bardzo subtelne */}
|
||
<motion.path
|
||
d="M 40 140 C 260 60 380 210 600 140 C 820 70 940 240 1160 160"
|
||
fill="none"
|
||
stroke="url(#thread)"
|
||
strokeWidth="2"
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
style={{ filter: "url(#)" }}
|
||
strokeDasharray="6 12"
|
||
initial={{ pathLength: 0, opacity: 0.25 }}
|
||
whileInView={{ pathLength: 1, opacity: 0.45 }}
|
||
viewport={{ once: true, amount: 0.2 }}
|
||
transition={{ duration: 1.2, ease: "easeOut" }}
|
||
/>
|
||
<motion.path
|
||
d="M 40 360 C 260 300 380 420 600 360 C 820 300 940 460 1160 380"
|
||
fill="none"
|
||
stroke="url(#thread)"
|
||
strokeWidth="2"
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeDasharray="6 12"
|
||
initial={{ pathLength: 0, opacity: 0.25 }}
|
||
whileInView={{ pathLength: 1, opacity: 0.45 }}
|
||
viewport={{ once: true, amount: 0.2 }}
|
||
transition={{ duration: 1.2, ease: "easeOut", delay: 0.15 }}
|
||
/>
|
||
</motion.svg>
|
||
{/* maska wygładzająca brzegi sekcji */}
|
||
<div
|
||
className="absolute inset-0 pointer-events-none"
|
||
style={{
|
||
WebkitMaskImage:
|
||
"linear-gradient(to bottom, transparent 0%, black 3%, black 97%, transparent 100%)",
|
||
maskImage:
|
||
"linear-gradient(to bottom, transparent 0%, black 3%, black 97%, transparent 100%)",
|
||
}}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
|
||
|
||
function RiskFreeSection() {
|
||
const items = [
|
||
'mini-audyt doświadczeń i danych',
|
||
'mapa punktów styku',
|
||
'insighty do poprawy spójności i skuteczności',
|
||
'1 eksperyment do wdrożenia natychmiast',
|
||
];
|
||
|
||
// nagłówek — miękko, jak w "Sytuacjach"
|
||
const headingV: Variants = {
|
||
hidden: { opacity: 0, y: 16, filter: 'blur(6px)' },
|
||
show: {
|
||
opacity: 1, y: 0, filter: 'blur(0px)',
|
||
transition: { duration: 0.55, ease: 'easeOut' },
|
||
},
|
||
};
|
||
|
||
// siatka — kontroluje kaskadę kart
|
||
const gridV: Variants = {
|
||
hidden: {},
|
||
show: {
|
||
transition: { delayChildren: 0.12, staggerChildren: 0.14 },
|
||
},
|
||
};
|
||
|
||
return (
|
||
<section id="JakDzialamy" className="relative py-24">
|
||
<RiskFreeBackground />
|
||
|
||
{/* Nagłówek */}
|
||
<motion.div
|
||
variants={headingV}
|
||
initial="hidden"
|
||
whileInView="show"
|
||
viewport={{ once: true, amount: 0.55 }}
|
||
>
|
||
<Header
|
||
title="Zobacz, jak to działa bez ryzyka"
|
||
gradient="coolSoft"
|
||
className={sytuacjeFont.className}
|
||
/>
|
||
<div className="max-w-6xl mx-auto px-6">
|
||
<motion.h3
|
||
initial={{ opacity: 0, y: 10, filter: 'blur(4px)' }}
|
||
whileInView={{ opacity: 1, y: 0, filter: 'blur(0px)' }}
|
||
viewport={{ once: true, amount: 0.7 }}
|
||
transition={{ duration: 0.5, ease: 'easeOut' }}
|
||
className="text-2xl font-semibold"
|
||
>
|
||
Brand Daily Test
|
||
</motion.h3>
|
||
<motion.p
|
||
initial={{ opacity: 0, y: 10 }}
|
||
whileInView={{ opacity: 1, y: 0 }}
|
||
viewport={{ once: true, amount: 0.7 }}
|
||
transition={{ duration: 0.5, ease: 'easeOut', delay: 0.06 }}
|
||
className="text-lg text-neutral-700 max-w-2xl mt-3"
|
||
>
|
||
W 7 dni pokażemy Ci realne luki, szanse i jeden test do wdrożenia od ręki.
|
||
</motion.p>
|
||
</div>
|
||
</motion.div>
|
||
|
||
{/* Karty – chłodna paleta + shine + kolejność */}
|
||
<motion.div
|
||
variants={gridV}
|
||
initial="hidden"
|
||
whileInView="show"
|
||
viewport={{ once: true, amount: 0.35 }}
|
||
className="max-w-6xl mx-auto px-6 grid md:grid-cols-2 gap-5 mt-10"
|
||
>
|
||
{items.map((t, i) => (
|
||
<RiskFreeCard key={i} index={i} text={t} />
|
||
))}
|
||
</motion.div>
|
||
|
||
{/* CTA pod siatką */}
|
||
<motion.p
|
||
initial={{ opacity: 0, y: 10 }}
|
||
whileInView={{ opacity: 1, y: 0 }}
|
||
viewport={{ once: true, amount: 0.6 }}
|
||
transition={{ duration: 0.5, ease: 'easeOut', delay: 0.1 }}
|
||
className="max-w-6xl mx-auto px-6 mt-12 text-lg"
|
||
>
|
||
<a href="#Kontakt" className="inline-flex items-center gap-2 underline underline-offset-4">
|
||
Efekty zanim podejmiesz decyzję o dalszej współpracy.
|
||
<svg width="18" height="18" viewBox="0 0 24 24" className="-mb-[1px]">
|
||
<path d="M5 12h14M13 5l7 7-7 7" stroke="currentColor" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round" />
|
||
</svg>
|
||
</a>
|
||
</motion.p>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function RiskFreeCard({ index, text }: { index: number; text: string }) {
|
||
const fromLeft = index % 2 === 0;
|
||
|
||
const cardV: Variants = {
|
||
hidden: {
|
||
opacity: 0,
|
||
y: 14,
|
||
x: fromLeft ? -8 : 8,
|
||
scale: 0.985,
|
||
filter: 'blur(6px)',
|
||
},
|
||
show: {
|
||
opacity: 1,
|
||
y: 0,
|
||
x: 0,
|
||
scale: 1,
|
||
filter: 'blur(0px)',
|
||
transition: { duration: 0.55, ease: 'easeOut' },
|
||
},
|
||
};
|
||
|
||
return (
|
||
<motion.article
|
||
variants={cardV}
|
||
className="relative overflow-hidden rounded-none bg-white ring-1 ring-black/8 hover:ring-black/10 transition
|
||
shadow-none hover:shadow-[0_10px_28px_rgba(16,24,40,0.08)]"
|
||
>
|
||
{/* cienka chłodna linia jak w „Sytuacjach” */}
|
||
<div className="absolute inset-x-0 top-0 h-[2px] bg-gradient-to-r from-sky-300 via-blue-400 to-indigo-500" />
|
||
|
||
{/* delikatny pattern + shine-once */}
|
||
<div className="absolute inset-0 pointer-events-none opacity-[0.04]
|
||
[background-image:radial-gradient(#0ea5e9_1px,transparent_1px)]
|
||
[background-size:16px_16px]" />
|
||
<motion.span
|
||
initial={{ x: '-25%', opacity: 0 }}
|
||
whileInView={{ x: '120%', opacity: 0.35 }}
|
||
viewport={{ once: true, amount: 0.9 }}
|
||
transition={{ duration: 0.9, ease: [0.22, 1, 0.36, 1] }}
|
||
className="pointer-events-none absolute left-0 right-0 top-1/2 -translate-y-1/2 h-8 rounded
|
||
bg-gradient-to-r from-white/0 via-white/70 to-white/0"
|
||
aria-hidden
|
||
/>
|
||
|
||
<div className="p-5">
|
||
<div className="flex items-start gap-4">
|
||
{/* chłodny punkt — zostaje kolorystyka z „Sytuacji” */}
|
||
<span className="mt-[2px] inline-block h-3 w-3 rounded-full bg-gradient-to-br from-sky-300 to-indigo-500" />
|
||
<p className="text-[1.02rem] leading-relaxed">
|
||
{text}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</motion.article>
|
||
);
|
||
}
|
||
|
||
function RiskFreeBackground() {
|
||
const ref = React.useRef<HTMLDivElement | null>(null);
|
||
const { scrollYProgress } = useScroll({ target: ref, offset: ['start end', 'end start'] });
|
||
const prefersReduced = useReducedMotion();
|
||
|
||
const [mounted, setMounted] = React.useState(false);
|
||
React.useEffect(() => setMounted(true), []);
|
||
|
||
const y = useTransform(scrollYProgress, [0, 1], prefersReduced ? [0, 0] : [8, -8]);
|
||
const x = useTransform(scrollYProgress, [0, 1], prefersReduced ? [0, 0] : [-6, 6]);
|
||
|
||
return (
|
||
<div ref={ref} aria-hidden className="absolute inset-0 -z-10 overflow-visible">
|
||
<div className="absolute inset-0"
|
||
style={{ background: 'linear-gradient(to bottom, #fafbfd 0%, #f2f6fb 34%, #eaf1f9 66%, #f7f9fd 100%)' }} />
|
||
<motion.div
|
||
suppressHydrationWarning
|
||
className="absolute inset-0 pointer-events-none"
|
||
style={{
|
||
x: mounted ? x : 0,
|
||
y: mounted ? y : 0,
|
||
background: [
|
||
'radial-gradient(560px 280px at 18% 12%, rgba(255,255,255,.42), transparent 68%)',
|
||
'radial-gradient(640px 320px at 80% 18%, rgba(255,255,255,.38), transparent 70%)',
|
||
'radial-gradient(700px 360px at 72% 82%, rgba(255,255,255,.32), transparent 70%)',
|
||
].join(','),
|
||
}}
|
||
/>
|
||
<div className="absolute inset-x-0 -top-8 h-8 pointer-events-none"
|
||
style={{ background: 'linear-gradient(to bottom, #ffffff, rgba(255,255,255,0))' }} />
|
||
<div className="absolute inset-x-0 -bottom-8 h-8 pointer-events-none"
|
||
style={{ background: 'linear-gradient(to top, #ffffff, rgba(255,255,255,0))' }} />
|
||
<div className="absolute inset-0 pointer-events-none"
|
||
style={{
|
||
WebkitMaskImage: 'linear-gradient(to bottom, transparent 0%, black 2.5%, black 97.5%, transparent 100%)',
|
||
maskImage: 'linear-gradient(to bottom, transparent 0%, black 2.5%, black 97.5%, transparent 100%)',
|
||
}}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
|
||
function PomoglismySection({
|
||
caseStudies,
|
||
openIndex,
|
||
setOpenIndex,
|
||
}: {
|
||
caseStudies: { title: string; short: string; details: string[] }[];
|
||
openIndex: number | null;
|
||
setOpenIndex: (i: number | null) => void;
|
||
}) {
|
||
const helpedItems: string[] = [
|
||
'Ponad 20 firmom farmaceutycznym',
|
||
'Ponad 20 firmom FMCG',
|
||
'Ponad 15 firmom B2B',
|
||
'10 firmom technologicznym i startupom',
|
||
'4 bankom i 3 firmom ubezpieczeniowym',
|
||
'5 instytucjom edukacyjnym',
|
||
'Firmom z branży rozrywkowej, modowej, przemysłowej, wydawniczej, rolniczej i wielu innych',
|
||
'Globalnym centralom koncernów z USA, Kanady, Niemiec, Wielkiej Brytanii, Hiszpanii, Węgier i Polski',
|
||
];
|
||
|
||
// ten sam feeling co „Dlaczego”
|
||
const titleV: Variants = {
|
||
hidden: { opacity: 0, y: 22, filter: 'blur(6px)' },
|
||
show: { opacity: 1, y: 0, filter: 'blur(0px)', transition: { duration: 1.0, ease: [0.22, 1, 0.36, 1] } },
|
||
};
|
||
const listV: Variants = {
|
||
hidden: { opacity: 0, y: 10, filter: 'blur(4px)' },
|
||
show: {
|
||
opacity: 1, y: 0, filter: 'blur(0px)',
|
||
transition: { duration: 0.55, ease: 'easeOut', when: 'beforeChildren', delayChildren: 0.06, staggerChildren: 0.1 },
|
||
},
|
||
};
|
||
const rowV: Variants = {
|
||
hidden: { opacity: 0, y: 8 },
|
||
show: { opacity: 1, y: 0, transition: { duration: 0.45, ease: 'easeOut' } },
|
||
};
|
||
|
||
return (
|
||
<section id="PomogliśmyCaseStudies" className="relative py-24">
|
||
{/* <FlowThread className="hidden md:block" /> */}
|
||
{/* Tytuł w ciepłej tonacji + separator jak w „Dlaczego” */}
|
||
<header className="max-w-6xl mx-auto px-6">
|
||
<motion.h2
|
||
variants={titleV}
|
||
initial="hidden"
|
||
whileInView="show"
|
||
viewport={{ once: true, amount: 0.95 }}
|
||
className={`${headingFont.className}
|
||
inline-block text-[clamp(2rem,6vw,3.2rem)] font-extrabold tracking-[-0.02em]
|
||
text-transparent bg-clip-text
|
||
bg-[linear-gradient(90deg,#F28241_0%,#CC2E47_100%)]`}
|
||
>
|
||
Pomogliśmy
|
||
</motion.h2>
|
||
{/* <FancySeparator className="mt-5 max-w-4xl" /> */}
|
||
</header>
|
||
|
||
{/* Panel z wypunktowaniem – dwie kolumny od md */}
|
||
<div className="max-w-6xl mx-auto px-6 mt-10">
|
||
<motion.div
|
||
variants={listV}
|
||
initial="hidden"
|
||
whileInView="show"
|
||
viewport={{ once: true, amount: 0.6, margin: '-10% 0px -10% 0px' }}
|
||
className="relative overflow-hidden rounded-none bg-white ring-1 ring-black/10"
|
||
>
|
||
{/* cienka ciepła linia u góry + bardzo subtelny pattern, spójny z „Dlaczego” */}
|
||
<div className="absolute inset-x-0 top-0 h-[2px] bg-gradient-to-r from-amber-400 via-rose-400 to-fuchsia-500" />
|
||
<div className="absolute inset-0 opacity-[0.03] pointer-events-none
|
||
[background-image:radial-gradient(#fb7185_1px,transparent_1px)]
|
||
[background-size:18px_18px]" />
|
||
|
||
<ol className="relative grid gap-y-3 py-6 px-5 md:grid-cols-2 md:gap-x-8 md:py-7">
|
||
{helpedItems.map((txt, i) => (
|
||
<motion.li key={i} variants={rowV} className="relative pl-7 py-2">
|
||
{/* kropka-gradient z jednorazowym pulsem */}
|
||
<motion.span
|
||
initial={{ scale: 0, boxShadow: '0 0 0 0 rgba(244,114,182,0)' }}
|
||
whileInView={{
|
||
scale: 1,
|
||
boxShadow: ['0 0 0 0 rgba(244,114,182,0.45)', '0 0 0 12px rgba(244,114,182,0)'],
|
||
}}
|
||
viewport={{ once: true, amount: 0.9 }}
|
||
transition={{ duration: 0.6, ease: 'easeOut' }}
|
||
className="absolute left-0 top-[0.9rem] h-3 w-3 rounded-full bg-gradient-to-r from-amber-400 to-fuchsia-500"
|
||
aria-hidden
|
||
/>
|
||
{/* shimmer przez wiersz */}
|
||
<motion.span
|
||
initial={{ x: '-20%', opacity: 0 }}
|
||
whileInView={{ x: '120%', opacity: 0.28 }}
|
||
viewport={{ once: true, amount: 0.9 }}
|
||
transition={{ duration: 0.85, ease: [0.22, 1, 0.36, 1] }}
|
||
className="pointer-events-none absolute left-0 right-0 top-1/2 -translate-y-1/2 h-6 rounded
|
||
bg-gradient-to-r from-amber-200/0 via-amber-200/60 to-rose-200/0"
|
||
/>
|
||
<span className="relative text-[1.05rem] md:text-[1.1rem] leading-relaxed">{txt}</span>
|
||
</motion.li>
|
||
))}
|
||
</ol>
|
||
</motion.div>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
// mały helper — darmowe zdjęcia Unsplash (no-copyright)
|
||
const CASE_IMAGES: Record<string, string> = {
|
||
"Firma kosmetyczna":
|
||
"https://images.unsplash.com/photo-1522335789203-aabd1fc54bc9?q=80&w=1600&auto=format&fit=crop",
|
||
"Firma paliwowa":
|
||
"/visuals/fuel-station.jpg",
|
||
"Firma technologiczna":
|
||
"https://images.unsplash.com/photo-1522071820081-009f0129c71c?q=80&w=1600&auto=format&fit=crop",
|
||
};
|
||
const genericImage =
|
||
"https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?q=80&w=1600&auto=format&fit=crop";
|
||
const getCaseImage = (title: string, override?: string) =>
|
||
override ?? CASE_IMAGES[title] ?? genericImage;
|
||
|
||
function CaseStudiesSection({
|
||
caseStudies,
|
||
}: {
|
||
caseStudies: { title: string; short: string; details: string[]; image?: string }[];
|
||
}) {
|
||
// lokalny stan: tylko jedna karta otwarta w tej sekcji
|
||
const [openCard, setOpenCard] = React.useState<number | null>(null);
|
||
|
||
const headingV: Variants = {
|
||
hidden: { opacity: 0, y: 16, filter: "blur(6px)" },
|
||
show: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration: 0.55, ease: "easeOut" } },
|
||
};
|
||
const gridV: Variants = {
|
||
hidden: {},
|
||
show: { transition: { delayChildren: 0.2, staggerChildren: 0.18 } },
|
||
};
|
||
const cardV: Variants = {
|
||
hidden: { opacity: 0, y: 12, filter: "blur(4px)", scale: 0.985 },
|
||
show: { opacity: 1, y: 0, filter: "blur(0px)", scale: 1, transition: { duration: 0.45, ease: "easeOut" } },
|
||
};
|
||
|
||
return (
|
||
<section id="CaseStudies" className="relative py-24">
|
||
<RiskFreeBackground />
|
||
|
||
{/* Nagłówek */}
|
||
<motion.div variants={headingV} initial="hidden" whileInView="show" viewport={{ once: true, amount: 0.55 }}>
|
||
<Header title="Case Studies" gradient="coolSoft" className={sytuacjeFont.className} />
|
||
<div className="max-w-6xl mx-auto px-6">
|
||
<motion.p
|
||
initial={{ opacity: 0, y: 10 }}
|
||
whileInView={{ opacity: 1, y: 0 }}
|
||
viewport={{ once: true, amount: 0.7 }}
|
||
transition={{ duration: 0.5, ease: "easeOut", delay: 0.06 }}
|
||
className="text-lg text-neutral-700 max-w-2xl"
|
||
>
|
||
Krótko i konkretnie — przykłady wdrożeń, które przełożyły się na wynik.
|
||
</motion.p>
|
||
</div>
|
||
</motion.div>
|
||
|
||
{/* 3 kolumny na dużych, 1 kolumna poniżej */}
|
||
<motion.div
|
||
variants={gridV}
|
||
initial="hidden"
|
||
whileInView="show"
|
||
viewport={{ once: true, amount: 0.35 }}
|
||
className="max-w-6xl mx-auto px-6 grid grid-cols-1 lg:grid-cols-3 gap-5 mt-10 items-start"
|
||
>
|
||
{caseStudies.map((study, i) => (
|
||
<CSCard
|
||
key={i}
|
||
index={i}
|
||
isOpen={openCard === i}
|
||
onToggle={() => setOpenCard(prev => (prev === i ? null : i))} // tylko kliknięty kafelek
|
||
title={study.title}
|
||
short={study.short}
|
||
details={study.details}
|
||
imageSrc={getCaseImage(study.title, study.image)}
|
||
variants={cardV}
|
||
/>
|
||
))}
|
||
</motion.div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
|
||
/* ---------------- KARTA ---------------- */
|
||
|
||
type CSCardProps = {
|
||
index: number;
|
||
isOpen: boolean;
|
||
onToggle: () => void;
|
||
title: string;
|
||
short: string;
|
||
details: string[];
|
||
imageSrc?: string;
|
||
variants: Variants;
|
||
};
|
||
|
||
function CSCard({
|
||
index,
|
||
isOpen,
|
||
onToggle,
|
||
title,
|
||
short,
|
||
details,
|
||
imageSrc,
|
||
variants,
|
||
}: CSCardProps) {
|
||
return (
|
||
<motion.article
|
||
layout
|
||
variants={variants}
|
||
className="self-start bg-white ring-1 ring-black/10 rounded-none overflow-hidden flex flex-col"
|
||
>
|
||
{imageSrc && (
|
||
<img
|
||
src={imageSrc}
|
||
alt=""
|
||
className="block w-full h-48 object-cover"
|
||
loading="lazy"
|
||
/>
|
||
)}
|
||
|
||
<div className="p-5 flex flex-col min-h-[200px] lg:min-h-[220px]">
|
||
<div className="flex items-start gap-3">
|
||
{/* kropka – idealne koło, nie zdeformuje się */}
|
||
<span className="shrink-0 mt-[6px] inline-block w-2.5 h-2.5 rounded-full bg-gradient-to-br from-sky-400 to-indigo-500" />
|
||
<div>
|
||
<h4 className="text-lg font-semibold">{title}</h4>
|
||
<p className="text-neutral-700 mt-1">{short}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
onClick={onToggle}
|
||
aria-expanded={isOpen}
|
||
aria-controls={`cs-details-${index}`}
|
||
className="mt-auto underline underline-offset-4 text-left cursor-pointer"
|
||
>
|
||
{isOpen ? "Zwiń" : "Pokaż szczegóły"}
|
||
</button>
|
||
|
||
<AnimatePresence initial={false} mode="wait">
|
||
{isOpen && (
|
||
<motion.ul
|
||
id={`cs-details-${index}`}
|
||
key={`open-${index}`}
|
||
initial={{ opacity: 0, height: 0 }}
|
||
animate={{ opacity: 1, height: "auto" }}
|
||
exit={{ opacity: 0, height: 0 }}
|
||
transition={{ duration: 0.35, ease: "easeOut" }}
|
||
className="mt-3 list-disc pl-6 text-sm text-neutral-800 space-y-2"
|
||
>
|
||
{details.map((d, idx) => (
|
||
<li key={idx}>{d}</li>
|
||
))}
|
||
</motion.ul>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
</motion.article>
|
||
);
|
||
}
|
||
|
||
|