feat(secciones): puntos de inflexión y dolores, con gráfico en toda la página

Dos secciones nuevas tomadas del deck de la dueña, verbatim y sin
reescribir: los 6 puntos de inflexión y los 4 dolores en primera persona.

El resto de las secciones incorpora la capa gráfica: fondos de puntos,
duotono sobre foto en Mecanismo y Cómo empezar, pantalla partida sí/no en
Para quién, y pasos conectados con línea de progreso.

Las fotos son CC BY-SA 4.0 y llevan crédito en el footer, como exige la
licencia al haberlas modificado.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
erikghandClaude Opus 5 committed 2026-07-31 10:44:54 -06:00
1 parent 5b400cb161
commit 69b4069525
25 files changed
+592 -120

No files matched your search

+14 -14
View File
@@ -3,6 +3,7 @@ import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { CTA } from '@/components/primitives/CTA';
import { Wordmark } from '@/components/primitives/Wordmark';
import { FondoPuntos } from '@/components/primitives/FondoPuntos';
import { cierre } from '@/lib/content';
import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, motionProps } from '@/lib/motion';
@@ -10,25 +11,24 @@ export function Cierre() {
const reduced = usePrefersReducedMotion();
const { whatsapp, email, linkedin } = cierre.contact;
return (
<section id="contacto" className="bg-azul-profundo text-white scroll-mt-24">
<Container className="py-24 md:py-32 text-center">
<section id="contacto" className="bg-azul-profundo text-white scroll-mt-24 relative overflow-hidden">
<FondoPuntos className="top-12 -left-16 w-72 md:w-[30rem] text-dorado/25" distance={36} />
<Container className="relative py-24 md:py-32 text-center">
<motion.div variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="flex flex-col items-center">
<motion.div variants={fadeInUp}><Wordmark size="lg" onDark /></motion.div>
<motion.blockquote variants={fadeInUp}
className="mt-10 font-sans italic text-2xl md:text-4xl font-bold leading-snug max-w-3xl">
<span className="text-dorado">&ldquo;</span>{cierre.quote}<span className="text-dorado">&rdquo;</span>
</motion.blockquote>
<motion.div variants={fadeInUp}><Wordmark size="lg" onDark showTagline /></motion.div>
<motion.p variants={fadeInUp}
className="mt-12 font-sans font-black text-3xl md:text-5xl leading-[1.05] tracking-tight max-w-3xl">
{cierre.quote}
</motion.p>
<motion.div variants={fadeInUp} className="mt-10">
<CTA href={whatsapp.href} label={cierre.cta} variant="dark" />
</motion.div>
<motion.nav variants={fadeInUp} aria-label="Contacto" className="mt-12">
<ul className="flex flex-wrap justify-center gap-6 text-sm">
<li><a href={whatsapp.href} className="hover:text-dorado transition-colors">{whatsapp.label}</a></li>
<li aria-hidden="true" className="text-gris-medio">·</li>
<li><a href={email.href} className="hover:text-dorado transition-colors">{email.label}</a></li>
<li aria-hidden="true" className="text-gris-medio">·</li>
<li><a href={linkedin.href} className="hover:text-dorado transition-colors">{linkedin.label}</a></li>
<motion.nav variants={fadeInUp} aria-label="Contacto" className="mt-14">
<ul className="flex flex-wrap justify-center gap-3 text-sm">
<li><a href={whatsapp.href} className="inline-block px-5 py-2.5 rounded-full border border-white/20 hover:border-dorado hover:text-dorado transition-colors">{whatsapp.label}</a></li>
<li><a href={email.href} className="inline-block px-5 py-2.5 rounded-full border border-white/20 hover:border-dorado hover:text-dorado transition-colors">{email.label}</a></li>
<li><a href={linkedin.href} className="inline-block px-5 py-2.5 rounded-full border border-white/20 hover:border-dorado hover:text-dorado transition-colors">{linkedin.label}</a></li>
</ul>
</motion.nav>
</motion.div>
+47 -20
View File
@@ -1,35 +1,62 @@
'use client';
import { motion } from 'framer-motion';
import Image from 'next/image';
import { Container } from '@/components/primitives/Container';
import { CTA } from '@/components/primitives/CTA';
import { Icono, type IconoNombre } from '@/components/primitives/Icono';
import { comoEmpezar, cierre } from '@/lib/content';
import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, motionProps } from '@/lib/motion';
import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, motionProps, useAutoStep } from '@/lib/motion';
const ICONOS: IconoNombre[] = ['escribeme', 'diagnostico', 'plan'];
export function ComoEmpezar() {
const reduced = usePrefersReducedMotion();
const waHref = `${cierre.contact.whatsapp.href}?text=${encodeURIComponent(comoEmpezar.whatsappText)}`;
const { active, setActive, hoverProps } = useAutoStep(comoEmpezar.steps.length, 3200);
return (
<section className="bg-white">
<Container className="py-24 md:py-32">
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="text-xs tracking-[0.3em] uppercase font-semibold text-dorado">{comoEmpezar.eyebrow}</motion.p>
<motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-4 font-sans font-bold text-3xl md:text-5xl text-azul-profundo">{comoEmpezar.title}</motion.h2>
<motion.ol variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-12 grid gap-10 md:grid-cols-3">
{comoEmpezar.steps.map((step) => (
<motion.li key={step.n} variants={fadeInUp}
className="border-t border-dorado/30 pt-6 transition-all duration-300 ease-out hover:-translate-y-1.5 hover:border-dorado/70 motion-reduce:transform-none motion-reduce:transition-none">
<div className="font-sans font-black text-5xl md:text-6xl text-dorado leading-none">{step.n}</div>
<h3 className="mt-4 font-sans font-bold text-xl text-azul-profundo">{step.name}</h3>
<p className="mt-3 text-gris-medio leading-relaxed">{step.desc}</p>
</motion.li>
))}
</motion.ol>
<section className="bg-gris-claro">
<Container className="py-24 md:py-32 grid gap-12 md:grid-cols-[1fr_auto] md:items-center">
<motion.div variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}>
<motion.p variants={fadeInUp} className="text-xs tracking-[0.3em] uppercase font-black text-dorado">{comoEmpezar.eyebrow}</motion.p>
<motion.h2 variants={fadeInUp} className="mt-4 font-sans font-black text-4xl md:text-6xl leading-[0.95] tracking-tight text-azul-profundo max-w-xl">{comoEmpezar.title}</motion.h2>
<motion.ol variants={staggerContainer} {...hoverProps} className="mt-12 grid gap-10 sm:grid-cols-3 md:grid-cols-1 md:gap-8">
{comoEmpezar.steps.map((step, i) => (
<motion.li key={step.n} variants={fadeInUp}>
<button
type="button"
onClick={() => setActive(i)}
aria-pressed={active === i}
className={`w-full text-left border-t pt-6 transition-all duration-300 ease-out hover:-translate-y-1.5 motion-reduce:transform-none motion-reduce:transition-none
md:flex md:items-baseline md:gap-4 md:border-t-0 md:border-l-2 md:pt-0 md:pl-5 ${
active === i ? 'border-dorado' : 'border-dorado/30 md:opacity-40'
}`}
>
<div className="font-sans font-black text-5xl md:text-3xl text-dorado leading-none shrink-0">{step.n}</div>
<div>
<div className="mt-4 md:mt-0 flex items-center gap-3">
<Icono nombre={ICONOS[i]} className="w-6 h-6 text-dorado shrink-0" />
<h3 className="font-sans font-bold text-xl text-azul-profundo">{step.name}</h3>
</div>
<p className="mt-3 md:mt-1 text-gris-medio leading-relaxed">{step.desc}</p>
</div>
</button>
</motion.li>
))}
</motion.ol>
<motion.div variants={fadeInUp} className="mt-12">
<CTA href={waHref} label={comoEmpezar.cta} variant="light" />
</motion.div>
</motion.div>
{/* Foto: amanecer sobre la cumbre del Monte Nardi, Tony 1212, CC BY-SA 4.0,
modificada (duotono) — créditos en el footer. "Tu siguiente movimiento
empieza…": el amanecer como metáfora del comienzo. */}
<motion.div variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-12">
<CTA href={waHref} label={comoEmpezar.cta} variant="light" />
className="hidden md:block relative w-72 lg:w-96 aspect-[4/5] rounded-lg overflow-hidden shadow-2xl shadow-azul-profundo/20 rotate-2 motion-reduce:rotate-0">
<div className="absolute inset-0 bg-gradient-to-br from-azul-profundo to-dorado" />
<Image src="/images/comoempezar-bg.jpg" alt="" fill sizes="(min-width: 1024px) 384px, 288px"
className="object-cover mix-blend-luminosity" />
<div className="absolute inset-0 bg-azul-profundo/20" />
</motion.div>
</Container>
</section>
+16 -7
View File
@@ -1,27 +1,36 @@
'use client';
import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { TarjetaSpotlight } from '@/components/primitives/TarjetaSpotlight';
import { Icono, type IconoNombre } from '@/components/primitives/Icono';
import { disciplinas } from '@/lib/content';
import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, motionProps } from '@/lib/motion';
const ICONOS: IconoNombre[] = ['comunicacion', 'digital', 'experiencias'];
export function Disciplinas() {
const reduced = usePrefersReducedMotion();
return (
<section className="bg-white">
<Container className="py-24 md:py-32">
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="text-xs tracking-[0.3em] uppercase font-semibold text-dorado">{disciplinas.eyebrow}</motion.p>
className="text-xs tracking-[0.3em] uppercase font-black text-dorado">{disciplinas.eyebrow}</motion.p>
<motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-4 font-sans font-bold text-3xl md:text-5xl text-azul-profundo">{disciplinas.title}</motion.h2>
className="mt-4 font-sans font-black text-4xl md:text-6xl text-azul-profundo tracking-tight">{disciplinas.title}</motion.h2>
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-4 text-gris-medio text-lg">{disciplinas.lead}</motion.p>
<motion.ul variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-12 grid gap-8 md:grid-cols-3 list-none p-0">
{disciplinas.items.map((item) => (
className="mt-12 grid gap-6 md:grid-cols-3 list-none p-0">
{disciplinas.items.map((item, i) => (
<motion.li key={item.name} variants={fadeInUp}
className="border-t-2 border-dorado pt-5 transition-all duration-300 ease-out hover:-translate-y-1.5 hover:border-dorado motion-reduce:transform-none motion-reduce:transition-none">
<h3 className="font-sans font-bold text-2xl text-azul-profundo">{item.name}</h3>
<p className="mt-3 text-gris-medio leading-relaxed">{item.desc}</p>
className="transition-transform duration-300 ease-out hover:-translate-y-1.5 motion-reduce:transform-none motion-reduce:transition-none">
<TarjetaSpotlight className="h-full rounded-lg bg-azul-profundo text-white p-7">
<div className="grid place-items-center w-14 h-14 rounded-full bg-white/10">
<Icono nombre={ICONOS[i]} className="w-7 h-7 text-dorado" />
</div>
<h3 className="mt-5 font-sans font-black text-2xl">{item.name}</h3>
<p className="mt-3 text-gris-medio leading-relaxed">{item.desc}</p>
</TarjetaSpotlight>
</motion.li>
))}
</motion.ul>
+41
View File
@@ -0,0 +1,41 @@
'use client';
import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { TarjetaSpotlight } from '@/components/primitives/TarjetaSpotlight';
import { dolores } from '@/lib/content';
import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, motionProps } from '@/lib/motion';
export function Dolores() {
const reduced = usePrefersReducedMotion();
return (
<section className="bg-white">
<Container className="py-24 md:py-32">
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="text-xs tracking-[0.3em] uppercase font-black text-dorado">{dolores.eyebrow}</motion.p>
<motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-4 font-sans font-black text-4xl md:text-6xl text-azul-profundo leading-[0.95] max-w-3xl tracking-tight">{dolores.title}</motion.h2>
<motion.ul variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-14 grid gap-8 md:grid-cols-2 list-none p-0">
{dolores.quotes.map((quote) => (
<motion.li key={quote} variants={fadeInUp}
className="transition-transform duration-300 ease-out hover:-translate-y-1.5 motion-reduce:transform-none motion-reduce:transition-none">
<TarjetaSpotlight className="h-full rounded-lg bg-gris-claro shadow-lg shadow-azul-profundo/5 border-l-4 border-dorado p-7">
<blockquote className="font-sans italic text-xl md:text-2xl leading-snug text-azul-profundo">
<span className="text-dorado not-italic">“</span>
{quote}
<span className="text-dorado not-italic">”</span>
</blockquote>
</TarjetaSpotlight>
</motion.li>
))}
</motion.ul>
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-16 font-sans font-black text-3xl md:text-5xl leading-[1.05] text-azul-profundo max-w-4xl tracking-tight">
{dolores.remate.before}{' '}
<span className="text-dorado">{dolores.remate.accent}</span>
</motion.p>
</Container>
</section>
);
}
+19 -6
View File
@@ -1,6 +1,9 @@
'use client';
import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { PatronPuntos } from '@/components/primitives/PatronPuntos';
import { Wordmark } from '@/components/primitives/Wordmark';
import { ContadorNumero } from '@/components/primitives/ContadorNumero';
import { estratega } from '@/lib/content';
import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, motionProps } from '@/lib/motion';
@@ -11,15 +14,25 @@ export function Estratega() {
<Container className="py-24 md:py-32">
<motion.div variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="grid gap-12 md:grid-cols-[auto_1fr] md:items-center">
{/* Portrait — placeholder gradient until dueña provides a photo */}
{/* TODO dueña: real portrait. Until then, a branded placeholder built
from the manual's dot pattern rather than a stand-in stock photo. */}
<motion.div variants={fadeInUp}
className="w-40 h-40 md:w-56 md:h-56 rounded-full flex-shrink-0 bg-gradient-to-br from-azul-profundo to-dorado"
aria-label="Retrato de la estratega"
className="w-44 h-44 md:w-64 md:h-64 rounded-full flex-shrink-0 overflow-hidden relative bg-azul-profundo grid place-items-center ring-4 ring-dorado ring-offset-4"
aria-label="Retrato de la estratega — pendiente"
role="img"
/>
>
<PatronPuntos rows={4} cols={7} className="absolute inset-0 m-auto w-[150%] h-auto text-white/15" />
<span className="relative"><Wordmark size="sm" onDark /></span>
</motion.div>
<div>
<motion.p variants={fadeInUp} className="text-xs tracking-[0.3em] uppercase font-semibold text-dorado">{estratega.eyebrow}</motion.p>
<motion.h2 variants={fadeInUp} className="mt-4 font-sans font-bold text-3xl md:text-5xl text-azul-profundo">{estratega.title}</motion.h2>
<motion.p variants={fadeInUp} className="text-xs tracking-[0.3em] uppercase font-black text-dorado">
{estratega.eyebrow.before} <span className="normal-case">{estratega.eyebrow.brand}</span>
</motion.p>
<motion.h2 variants={fadeInUp} className="mt-4 font-sans font-black text-4xl md:text-6xl text-azul-profundo tracking-tight">{estratega.title}</motion.h2>
<motion.div variants={fadeInUp} className="mt-6 flex items-baseline gap-3">
<ContadorNumero hasta={30} sufijo="+" className="font-sans font-black text-5xl md:text-6xl text-dorado leading-none" />
<p className="text-xs uppercase tracking-[0.2em] text-gris-medio font-semibold max-w-[8rem] leading-tight">años construyendo marcas</p>
</motion.div>
<motion.p variants={fadeInUp} className="mt-6 text-lg text-gris-medio leading-relaxed max-w-2xl">{estratega.bio}</motion.p>
<motion.blockquote variants={fadeInUp} className="mt-6 font-sans italic text-xl md:text-2xl text-azul-profundo border-l-4 border-dorado pl-4">
{estratega.quote}
+17 -9
View File
@@ -2,27 +2,35 @@
import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { SimboloTresLineas } from '@/components/primitives/SimboloTresLineas';
import { TextoDesenfoque } from '@/components/primitives/TextoDesenfoque';
import { fraseAncla } from '@/lib/content';
import { fadeInUp, viewportOnce, usePrefersReducedMotion, motionProps } from '@/lib/motion';
export function FraseAncla() {
const reduced = usePrefersReducedMotion();
return (
<section className="bg-white">
<Container className="py-24 md:py-32 text-center max-w-4xl">
<motion.blockquote
<section className="bg-azul-profundo relative overflow-hidden">
<Container className="relative py-28 md:py-40 text-center max-w-5xl">
<motion.div variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="flex justify-center">
<SimboloTresLineas className="pointer-events-none select-none w-14 text-dorado" />
</motion.div>
<motion.p
variants={fadeInUp}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
{...motionProps(reduced)}
className="font-sans italic text-2xl md:text-4xl font-bold leading-snug text-azul-profundo"
className="mt-10 font-sans font-black text-4xl md:text-6xl leading-[0.95] tracking-tight text-white"
>
<span className="text-dorado">“</span>
{fraseAncla.quote.before}{' '}
<span className="text-dorado">{fraseAncla.quote.accent}</span>
<span className="text-dorado">”</span>
</motion.blockquote>
<TextoDesenfoque
segments={[
{ text: fraseAncla.quote.before },
{ text: fraseAncla.quote.accent, className: 'text-dorado' },
]}
/>
</motion.p>
</Container>
</section>
);
+51 -20
View File
@@ -4,7 +4,11 @@ import { motion } from 'framer-motion';
import { Wordmark } from '@/components/primitives/Wordmark';
import { CTA } from '@/components/primitives/CTA';
import { Container } from '@/components/primitives/Container';
import { hero } from '@/lib/content';
import { FondoPuntos } from '@/components/primitives/FondoPuntos';
import { Carrusel } from '@/components/primitives/Carrusel';
import { TextoDesenfoque } from '@/components/primitives/TextoDesenfoque';
import { TarjetaSpotlight } from '@/components/primitives/TarjetaSpotlight';
import { hero, dolores } from '@/lib/content';
import { fadeInUp, staggerContainer, usePrefersReducedMotion, motionProps } from '@/lib/motion';
type Variant = 'hormozi' | 'editorial';
@@ -15,28 +19,55 @@ export function Hero({ variant = 'editorial' }: { variant?: Variant }) {
const headlineBase = isHormozi
? 'font-sans font-black uppercase tracking-tight'
: 'font-sans font-bold';
const headlineSize = 'text-4xl md:text-6xl leading-[1.1]';
const headlineSize = 'text-5xl md:text-7xl leading-[0.98] tracking-tight';
const clarityBorder = isHormozi ? 'border-b-[5px] border-dorado pb-0.5' : 'border-b-[3px] border-dorado pb-0.5';
return (
<section className="bg-white">
<Container className="min-h-[88vh] flex flex-col items-center justify-center text-center py-24">
<motion.div variants={staggerContainer} initial="hidden" animate="visible" {...motionProps(reduced)} className="flex flex-col items-center">
<motion.div variants={fadeInUp}><Wordmark size="xl" /></motion.div>
<motion.p variants={fadeInUp} className="mt-6 text-xs md:text-sm tracking-[0.3em] uppercase font-semibold text-dorado">
{hero.eyebrow}
</motion.p>
<motion.h1 variants={fadeInUp} className={`mt-8 ${headlineBase} ${headlineSize} text-azul-profundo`}>
{hero.headline.line1}{' '}
<span className="text-dorado">{hero.headline.effort}</span>{' '}
{hero.headline.line3}{' '}
<span style={{ borderBottom: isHormozi ? '5px solid #C4942E' : '3px solid #C4942E', paddingBottom: 2 }}>
{hero.headline.clarity}
</span>
</motion.h1>
<motion.div variants={fadeInUp} className="mt-10">
<CTA href="#contacto" label={hero.cta} variant="light" />
<section className="bg-white relative overflow-hidden">
<FondoPuntos className="top-24 -right-16 w-72 md:w-[28rem] text-azul-profundo/10" interactivo />
<FondoPuntos className="bottom-16 -left-20 w-64 md:w-96 text-dorado/20" distance={28} rows={2} interactivo />
<Container className="relative min-h-[88vh] flex items-center py-24">
<div className="grid gap-14 lg:grid-cols-[1.1fr_0.9fr] lg:items-center w-full">
<motion.div variants={staggerContainer} initial="hidden" animate="visible" {...motionProps(reduced)}
className="min-w-0 flex flex-col items-center text-center lg:items-start lg:text-left">
<motion.div variants={fadeInUp}><Wordmark size="xl" showTagline /></motion.div>
<motion.p variants={fadeInUp} className="mt-6 text-xs md:text-sm tracking-[0.3em] uppercase font-black text-dorado">
{hero.eyebrow}
</motion.p>
<motion.h1 variants={fadeInUp} className={`mt-8 ${headlineBase} ${headlineSize} text-azul-profundo`}>
<TextoDesenfoque
as="span"
segments={[
{ text: hero.headline.line1 },
{ text: hero.headline.effort, className: 'text-dorado' },
{ text: hero.headline.line3 },
{ text: hero.headline.clarity, className: clarityBorder },
]}
/>
</motion.h1>
<motion.div variants={fadeInUp} className="mt-10 flex flex-col md:flex-row items-center gap-4">
<CTA href="#contacto" label={hero.cta} variant="light" className="md:whitespace-nowrap" />
<CTA href="#mecanismo" label={hero.ctaSecundario} variant="ghost" className="md:whitespace-nowrap" />
</motion.div>
</motion.div>
</motion.div>
{/* Rota las frases textuales de `dolores` — quien llega al hero se ve
reflejado antes de haber bajado a leer la sección completa. */}
<motion.div variants={fadeInUp} initial="hidden" animate="visible" {...motionProps(reduced)}
className="hidden lg:block min-w-0">
<Carrusel
ariaLabel="Lo que sienten nuestros clientes"
autoplayMs={4500}
slides={dolores.quotes.map((quote) => (
<TarjetaSpotlight key={quote} className="rounded-2xl border border-azul-profundo/10 bg-white shadow-xl shadow-azul-profundo/5 p-8 lg:p-10 min-h-[15rem] flex flex-col justify-center">
<p className="text-xs uppercase tracking-[0.3em] font-semibold text-dorado">lo que escuchamos</p>
<blockquote className="mt-4 font-sans italic text-xl lg:text-2xl leading-snug text-azul-profundo">
<span className="text-dorado not-italic">&ldquo;</span>{quote}<span className="text-dorado not-italic">&rdquo;</span>
</blockquote>
</TarjetaSpotlight>
))}
/>
</motion.div>
</div>
</Container>
</section>
);
+64 -12
View File
@@ -2,26 +2,78 @@
import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { FondoFoto } from '@/components/primitives/FondoFoto';
import { Icono, type IconoNombre } from '@/components/primitives/Icono';
import { mecanismo } from '@/lib/content';
import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, motionProps } from '@/lib/motion';
import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, motionProps, useAutoStep } from '@/lib/motion';
const ICONOS: IconoNombre[] = ['inmersion', 'desafiar', 'estrategia'];
export function Mecanismo() {
const reduced = usePrefersReducedMotion();
const { active, setActive, hoverProps } = useAutoStep(mecanismo.steps.length, 3200);
return (
<section className="bg-azul-profundo text-white">
<Container className="py-24 md:py-32">
<section id="mecanismo" className="bg-azul-profundo text-white scroll-mt-24 relative overflow-hidden">
{/* Foto: Golden Gate Bridge, Dietmar Rabich, CC BY-SA 4.0, modificada (duotono) — créditos en el footer */}
<FondoFoto src="/images/mecanismo-bg.jpg" className="inset-0 opacity-25" wash="bg-azul-profundo/80" />
<Container className="relative py-24 md:py-32">
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="text-xs tracking-[0.3em] uppercase font-semibold text-dorado">{mecanismo.eyebrow}</motion.p>
className="text-xs tracking-[0.3em] uppercase font-black text-dorado">{mecanismo.eyebrow}</motion.p>
<motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-4 font-sans font-bold text-3xl md:text-5xl">{mecanismo.title}</motion.h2>
<motion.ol variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-4 font-sans font-black text-4xl md:text-6xl tracking-tight">{mecanismo.title}</motion.h2>
{/* Conector de pasos: avanza solo cada ~3s (se pausa con hover o si el
usuario elige un paso), y adelanta la línea de progreso — el mismo
lenguaje de "proceso conectado" de en-ti.com. */}
<div {...hoverProps} className="mt-14 hidden md:flex items-center" role="tablist" aria-label="Pasos del mecanismo">
{mecanismo.steps.map((step, i) => (
<div key={step.n} className="flex items-center flex-1 last:flex-none">
<button
type="button"
role="tab"
aria-selected={active === i}
aria-label={`Paso ${step.n}: ${step.name}`}
onClick={() => setActive(i)}
onMouseEnter={() => setActive(i)}
className={`shrink-0 grid place-items-center w-14 h-14 rounded-full border-2 font-sans font-black text-lg transition-colors duration-300 ${
active === i ? 'bg-dorado border-dorado text-azul-profundo' : 'border-white/25 text-white/60 hover:border-dorado/60 hover:text-white'
}`}
>
{step.n}
</button>
{i < mecanismo.steps.length - 1 && (
<div className="flex-1 h-px mx-2 bg-white/15 relative overflow-hidden">
<motion.div
className="absolute inset-y-0 left-0 w-full origin-left bg-dorado motion-reduce:transition-none"
initial={false}
animate={{ scaleX: active > i ? 1 : 0 }}
transition={{ duration: 0.4, ease: 'easeOut' }}
/>
</div>
)}
</div>
))}
</div>
<motion.ol variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)} {...hoverProps}
className="mt-12 grid gap-10 md:grid-cols-3">
{mecanismo.steps.map((step) => (
<motion.li key={step.n} variants={fadeInUp}
className="border-t border-dorado/30 pt-6 transition-all duration-300 ease-out hover:-translate-y-1.5 hover:border-dorado/70 motion-reduce:transform-none motion-reduce:transition-none">
<div className="font-sans font-black text-5xl md:text-6xl text-dorado leading-none">{step.n}</div>
<h3 className="mt-4 font-sans font-bold text-xl">{step.name}</h3>
<p className="mt-3 text-gris-medio leading-relaxed">{step.desc}</p>
{mecanismo.steps.map((step, i) => (
<motion.li key={step.n} variants={fadeInUp}>
<button
type="button"
onClick={() => setActive(i)}
aria-pressed={active === i}
className={`w-full text-left border-t pt-6 transition-all duration-300 ease-out hover:-translate-y-1.5 motion-reduce:transform-none motion-reduce:transition-none ${
active === i ? 'border-dorado md:-translate-y-1.5' : 'border-dorado/30 md:opacity-40'
}`}
>
<div className="flex items-center justify-between">
<div className="font-sans font-black text-5xl md:text-6xl text-dorado leading-none">{step.n}</div>
<Icono nombre={ICONOS[i]} className="w-10 h-10 md:w-12 md:h-12 text-white/70" />
</div>
<h3 className="mt-4 font-sans font-bold text-xl">{step.name}</h3>
<p className="mt-3 text-gris-medio leading-relaxed">{step.desc}</p>
</button>
</motion.li>
))}
</motion.ol>
+30 -12
View File
@@ -8,23 +8,41 @@ import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, moti
export function ParaQuien() {
const reduced = usePrefersReducedMotion();
return (
<section className="bg-white">
<Container className="py-24 md:py-32">
<motion.div variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)} className="max-w-3xl">
<motion.h2 variants={fadeInUp} className="font-sans font-bold text-3xl md:text-5xl text-azul-profundo leading-tight">
{paraQuien.title}
</motion.h2>
<motion.p variants={fadeInUp} className="mt-6 text-lg text-gris-medio">{paraQuien.lead}</motion.p>
<motion.ul variants={fadeInUp} className="mt-8 space-y-3">
<section className="bg-white relative overflow-hidden">
<Container className="relative pt-24 md:pt-32 pb-12 md:pb-16">
<motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="font-sans font-black text-4xl md:text-6xl text-azul-profundo leading-[0.95] max-w-4xl tracking-tight">{paraQuien.title}</motion.h2>
</Container>
{/* Pantalla partida: luz contra oscuridad, para dramatizar la bifurcación
"sí es para ti / no es para ti" — sale del ancho del Container a
propósito, a todo lo ancho de la ventana. */}
<motion.div variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="grid md:grid-cols-2">
<motion.div variants={fadeInUp} className="bg-white px-6 md:px-16 py-16 md:py-20 border-t-4 border-dorado">
<p className="text-xs tracking-[0.3em] uppercase font-black text-dorado">Sí</p>
<p className="mt-3 text-2xl font-sans font-bold text-azul-profundo max-w-sm">{paraQuien.lead}</p>
<ul className="mt-8 space-y-4">
{paraQuien.criteria.map((c) => (
<li key={c} className="flex items-start gap-3">
<span className="text-dorado font-bold mt-1" aria-hidden="true">✓</span>
<span className="text-azul-profundo text-lg">{c}</span>
<span aria-hidden="true" className="grid place-items-center w-7 h-7 rounded-full bg-dorado text-azul-profundo font-black text-sm shrink-0">✓</span>
<span className="text-azul-profundo text-lg leading-snug">{c}</span>
</li>
))}
</motion.ul>
</ul>
</motion.div>
</Container>
<motion.div variants={fadeInUp} className="bg-azul-profundo text-white px-6 md:px-16 py-16 md:py-20 border-t-4 border-white/10">
<p className="text-xs tracking-[0.3em] uppercase font-black text-white/50">No</p>
<p className="mt-3 text-2xl font-sans font-bold text-white/70 max-w-sm">{paraQuien.leadNo}</p>
<ul className="mt-8 space-y-4">
{paraQuien.criteriaNo.map((c) => (
<li key={c} className="flex items-start gap-3">
<span aria-hidden="true" className="grid place-items-center w-7 h-7 rounded-full border border-white/30 text-white/50 font-black text-sm shrink-0">✕</span>
<span className="text-white/60 text-lg leading-snug">{c}</span>
</li>
))}
</ul>
</motion.div>
</motion.div>
</section>
);
}
+64
View File
@@ -0,0 +1,64 @@
'use client';
import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { FondoPuntos } from '@/components/primitives/FondoPuntos';
import { Carrusel } from '@/components/primitives/Carrusel';
import { TarjetaSpotlight } from '@/components/primitives/TarjetaSpotlight';
import { IlustracionInflexion } from '@/components/primitives/IlustracionInflexion';
import { puntosInflexion } from '@/lib/content';
import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, motionProps } from '@/lib/motion';
function Tarjeta({ n, name, desc }: { n: number; name: string; desc: string }) {
return (
<TarjetaSpotlight className="relative h-full rounded-md border-t-2 border-dorado pt-5 pl-1 pr-1 pb-1 overflow-visible">
<span aria-hidden="true" className="pointer-events-none absolute -top-2 right-0 font-sans font-black text-5xl text-azul-profundo/[0.06] leading-none select-none">
{String(n).padStart(2, '0')}
</span>
<h3 className="relative font-sans font-bold text-xl text-azul-profundo">{name}</h3>
<p className="relative mt-2 text-gris-medio leading-relaxed">{desc}</p>
</TarjetaSpotlight>
);
}
export function PuntosInflexion() {
const reduced = usePrefersReducedMotion();
return (
<section className="bg-gris-claro relative overflow-hidden">
<FondoPuntos className="-bottom-6 -right-12 w-80 md:w-[32rem] text-azul-profundo/10" distance={32} />
<Container className="relative py-24 md:py-32">
<div className="flex flex-col md:flex-row md:items-center md:justify-between md:gap-12">
<div>
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="text-xs tracking-[0.3em] uppercase font-black text-dorado">{puntosInflexion.eyebrow}</motion.p>
<motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-4 font-sans font-black text-4xl md:text-6xl text-azul-profundo leading-[0.95] tracking-tight max-w-3xl">{puntosInflexion.title}</motion.h2>
</div>
<motion.div variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-8 md:mt-0 shrink-0">
<IlustracionInflexion className="w-56 md:w-72 h-auto mx-auto" />
</motion.div>
</div>
{/* Carrusel táctil en móvil (contador "01 / 06", como en-ti.com); grid fijo desde md. */}
<div className="mt-12 md:hidden">
<Carrusel
ariaLabel="Los seis puntos de inflexión"
mostrarContador
slides={puntosInflexion.items.map((item, i) => <Tarjeta key={item.name} n={i + 1} {...item} />)}
slideClassName="pr-4"
/>
</div>
<motion.ul variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-12 hidden md:grid md:grid-cols-3 md:gap-8 list-none p-0">
{puntosInflexion.items.map((item, i) => (
<motion.li key={item.name} variants={fadeInUp}
className="transition-transform duration-300 ease-out hover:-translate-y-1.5 motion-reduce:transform-none motion-reduce:transition-none">
<Tarjeta n={i + 1} {...item} />
</motion.li>
))}
</motion.ul>
</Container>
</section>
);
}
@@ -1,5 +1,5 @@
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { render, screen, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { ComoEmpezar } from '../ComoEmpezar';
describe('<ComoEmpezar />', () => {
@@ -15,4 +15,26 @@ describe('<ComoEmpezar />', () => {
expect(href).toMatch(/^https:\/\/wa\.me\//);
expect(href).toContain('text=');
});
it('renders the sunrise photo panel as decorative, not real content', () => {
const { container } = render(<ComoEmpezar />);
const img = container.querySelector('img');
// next/image rewrites `src` through its loader in this test environment;
// the static export (images.unoptimized: true) serves the plain path.
expect(img?.getAttribute('src')).toContain('comoempezar-bg.jpg');
expect(img).toHaveAttribute('alt', '');
});
describe('auto-advancing step highlight', () => {
beforeEach(() => { vi.useFakeTimers(); });
afterEach(() => { vi.useRealTimers(); });
it('highlights the first step by default, then advances on its own', () => {
render(<ComoEmpezar />);
const first = screen.getByText('Escríbeme').closest('button');
const second = screen.getByText('Sesión de diagnóstico').closest('button');
expect(first).toHaveAttribute('aria-pressed', 'true');
act(() => { vi.advanceTimersByTime(3300); });
expect(second).toHaveAttribute('aria-pressed', 'true');
});
});
});
@@ -0,0 +1,23 @@
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Dolores } from '../Dolores';
describe('<Dolores />', () => {
it('renders the four pains verbatim, in the client\'s first person', () => {
render(<Dolores />);
expect(screen.getByText(/Trabajo más y gano casi lo mismo que hace dos años\./)).toBeInTheDocument();
expect(screen.getByText(/no sé en cuál enfocar mi tiempo y mi dinero\./)).toBeInTheDocument();
expect(screen.getByText(/no puede crecer si yo no estoy en cada decisión\./)).toBeInTheDocument();
expect(screen.getByText(/ya no me sirve para el siguiente nivel\./)).toBeInTheDocument();
expect(screen.getAllByRole('listitem')).toHaveLength(4);
});
it('closes with the resolution, not with more effort', () => {
const { container } = render(<Dolores />);
expect(container.textContent).toContain('Esto no se resuelve con más esfuerzo.');
expect(container.textContent).toContain('Se resuelve con una estrategia clara.');
});
it('does NOT introduce scarcity', () => {
const { container } = render(<Dolores />);
expect(container.textContent).not.toMatch(/cupos|cohorte|generación fundadora/i);
});
});
@@ -11,4 +11,12 @@ describe('<Estratega />', () => {
render(<Estratega />);
expect(screen.getByText(/El tiempo no se compra/i)).toBeInTheDocument();
});
it('never lets CSS uppercase turn the brand name into CREEER', () => {
const { container } = render(<Estratega />);
const brand = screen.getByText('creeer');
expect(brand.className).toContain('normal-case');
// the eyebrow around it may be uppercase, the brand name may not
expect(brand.closest('p')?.className).toContain('uppercase');
expect(container.textContent).not.toMatch(/CREEER|Creeer/);
});
});
@@ -1,11 +1,11 @@
import { render, screen } from '@testing-library/react';
import { render } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { FraseAncla } from '../FraseAncla';
describe('<FraseAncla />', () => {
it('renders the anchor quote', () => {
render(<FraseAncla />);
expect(screen.getByText(/Las empresas no dejan de crecer/i)).toBeInTheDocument();
expect(screen.getByText(/falta de claridad/i)).toBeInTheDocument();
const { container } = render(<FraseAncla />);
expect(container.textContent).toMatch(/Las empresas no dejan de crecer/i);
expect(container.textContent).toMatch(/falta de claridad/i);
});
});
+14 -5
View File
@@ -8,17 +8,26 @@ describe('<Hero />', () => {
expect(screen.getByText(/tu estratega de crecimiento/i)).toBeInTheDocument();
});
it('renders the headline copy', () => {
render(<Hero variant="hormozi" />);
expect(screen.getByText(/Tu negocio/i)).toBeInTheDocument();
expect(screen.getByText(/más esfuerzo/i)).toBeInTheDocument();
expect(screen.getByText(/más claridad/i)).toBeInTheDocument();
const { container } = render(<Hero variant="hormozi" />);
expect(container.textContent).toMatch(/Tu negocio/i);
expect(container.textContent).toMatch(/más esfuerzo/i);
expect(container.textContent).toMatch(/más claridad/i);
});
it('renders the CTA', () => {
render(<Hero variant="editorial" />);
expect(screen.getByRole('link', { name: /define tu siguiente movimiento/i })).toBeInTheDocument();
expect(screen.getByRole('link', { name: /define tu siguiente movimiento/i })).toHaveAttribute('href', '#contacto');
});
it('offers a secondary route into the process for readers not ready to convert', () => {
render(<Hero variant="editorial" />);
expect(screen.getByRole('link', { name: /ver cómo trabajo/i })).toHaveAttribute('href', '#mecanismo');
});
it('does NOT render scarcity (cupos/cohorte)', () => {
const { container } = render(<Hero variant="editorial" />);
expect(container.textContent).not.toMatch(/cupos|cohorte|generación fundadora/i);
});
it('mirrors the client\'s own words back in a rotating carousel', () => {
render(<Hero variant="editorial" />);
expect(screen.getByRole('group', { name: /lo que sienten nuestros clientes/i })).toBeInTheDocument();
expect(screen.getByText(/Trabajo más y gano casi lo mismo/i)).toBeInTheDocument();
});
});
@@ -1,5 +1,5 @@
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { render, screen, fireEvent, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { Mecanismo } from '../Mecanismo';
describe('<Mecanismo />', () => {
@@ -13,4 +13,31 @@ describe('<Mecanismo />', () => {
const { container } = render(<Mecanismo />);
expect(container.textContent).toMatch(/01.+02.+03/);
});
it('keeps the background photo subtle enough not to fight the text', () => {
const { container } = render(<Mecanismo />);
const bg = container.querySelector('img');
expect(bg).toHaveAttribute('src', '/images/mecanismo-bg.jpg');
expect(bg?.closest('[class*="opacity-"]')).not.toBeNull();
});
describe('auto-advancing step indicator', () => {
beforeEach(() => { vi.useFakeTimers(); });
afterEach(() => { vi.useRealTimers(); });
it('highlights the first step by default, then advances on its own', () => {
render(<Mecanismo />);
const tabs = screen.getAllByRole('tab');
expect(tabs[0]).toHaveAttribute('aria-selected', 'true');
act(() => { vi.advanceTimersByTime(3300); });
expect(tabs[1]).toHaveAttribute('aria-selected', 'true');
});
it('stops advancing while the pointer is over the steps', () => {
render(<Mecanismo />);
const tabs = screen.getAllByRole('tab');
fireEvent.mouseEnter(screen.getByRole('tablist'));
act(() => { vi.advanceTimersByTime(6000); });
expect(tabs[0]).toHaveAttribute('aria-selected', 'true');
});
});
});
@@ -7,10 +7,23 @@ describe('<ParaQuien />', () => {
render(<ParaQuien />);
expect(screen.getByText(/punto de inflexión/i)).toBeInTheDocument();
});
it('renders all 3 criteria', () => {
it('renders the qualifying criteria', () => {
render(<ParaQuien />);
expect(screen.getByText(/tienes tracción/i)).toBeInTheDocument();
expect(screen.getByText(/trabajar más ya no rinde/i)).toBeInTheDocument();
expect(screen.getByText(/necesitas claridad/i)).toBeInTheDocument();
expect(screen.getByText(/sin perder tu esencia/i)).toBeInTheDocument();
});
it('renders the disqualifying criteria from the dueña deck', () => {
render(<ParaQuien />);
expect(screen.getByText(/Esto no es para ti si/i)).toBeInTheDocument();
expect(screen.getByText(/fase inicial o sin tracción/i)).toBeInTheDocument();
expect(screen.getByText(/solución rápida de venta/i)).toBeInTheDocument();
expect(screen.getByText(/un curso, no una transformación/i)).toBeInTheDocument();
expect(screen.getByText(/crisis de supervivencia/i)).toBeInTheDocument();
});
it('keeps the disqualifiers impersonal, so they read as a profile and not an accusation', () => {
render(<ParaQuien />);
expect(screen.getByText(/fase inicial o sin tracción/i).textContent).not.toMatch(/\btú\b|\btu\b/i);
});
});
@@ -0,0 +1,24 @@
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { PuntosInflexion } from '../PuntosInflexion';
describe('<PuntosInflexion />', () => {
it('renders the section title', () => {
render(<PuntosInflexion />);
expect(screen.getByText(/llegan a un punto de inflexión/i)).toBeInTheDocument();
});
it('renders the six inflection points from the dueña deck', () => {
render(<PuntosInflexion />);
// Each point renders twice in the markup — once in the mobile carousel,
// once in the desktop grid — with CSS `hidden`/`md:` toggling which one
// is actually visible at a given breakpoint.
for (const name of ['Crecer', 'Consolidarse', 'Recuperar rentabilidad', 'Simplificar', 'Expandirse', 'Replantear el portafolio']) {
expect(screen.getAllByText(name).length).toBeGreaterThanOrEqual(1);
}
expect(screen.getAllByRole('listitem')).toHaveLength(6);
});
it('does NOT introduce scarcity', () => {
const { container } = render(<PuntosInflexion />);
expect(container.textContent).not.toMatch(/cupos|cohorte|generación fundadora/i);
});
});