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
+580 -108

No files matched your search

+5 -1
View File
@@ -2,6 +2,8 @@ import { Header } from '@/components/layout/Header';
import { Footer } from '@/components/layout/Footer';
import { Hero } from '@/components/sections/Hero';
import { FraseAncla } from '@/components/sections/FraseAncla';
import { PuntosInflexion } from '@/components/sections/PuntosInflexion';
import { Dolores } from '@/components/sections/Dolores';
import { ParaQuien } from '@/components/sections/ParaQuien';
import { Mecanismo } from '@/components/sections/Mecanismo';
import { Disciplinas } from '@/components/sections/Disciplinas';
@@ -25,9 +27,11 @@ export default function Home() {
<main>
<Hero variant="editorial" /> {/* dueña puede cambiar a "hormozi" */}
<FraseAncla />
<ParaQuien />
<PuntosInflexion />
<Dolores />
<Mecanismo />
<Disciplinas />
<ParaQuien />
<Estratega />
<ComoEmpezar />
<Cierre />
+12 -1
View File
@@ -1,6 +1,6 @@
import { Wordmark } from '@/components/primitives/Wordmark';
import { Container } from '@/components/primitives/Container';
import { footer } from '@/lib/content';
import { footer, creditosFotos } from '@/lib/content';
export function Footer() {
return (
@@ -10,6 +10,17 @@ export function Footer() {
<p className="text-sm text-gris-medio">{footer.tagline}</p>
<p className="text-xs text-gris-medio">{footer.legal}</p>
</Container>
<Container className="pb-6 text-center md:text-left">
<p className="text-[10px] leading-relaxed text-gris-medio/60">
{creditosFotos.map((c, i) => (
<span key={c.href}>
{i > 0 && ' · '}
<a href={c.href} className="hover:text-gris-medio">{c.desc}</a>, modificada,{' '}
<a href={c.licenciaHref} className="hover:text-gris-medio">{c.licencia}</a>
</span>
))}
</p>
</Container>
</footer>
);
}
@@ -12,4 +12,11 @@ describe('<Footer />', () => {
render(<Footer />);
expect(screen.queryByRole('heading', { level: 1 })).toBeNull();
});
it('credits the stock photos per their CC BY-SA 4.0 licence', () => {
render(<Footer />);
expect(screen.getAllByText(/CC BY-SA 4\.0/)).toHaveLength(2);
expect(screen.getByText(/Golden Gate Bridge/)).toBeInTheDocument();
expect(screen.getByText(/Monte Nardi/)).toBeInTheDocument();
expect(screen.getAllByText(/modificada/).length).toBe(2);
});
});
+5 -3
View File
@@ -3,9 +3,11 @@ import { describe, it, expect } from 'vitest';
import { Header } from '../Header';
describe('<Header />', () => {
it('renders the wordmark', () => {
const { container } = render(<Header />);
expect(container.textContent).toContain('cre≡er');
it('renders the wordmark, linked to the top of the page', () => {
render(<Header />);
const home = screen.getByRole('link', { name: /creeer/i });
expect(home).toHaveAttribute('href', '#top');
expect(home.querySelector('[data-accent="true"]')).not.toBeNull();
});
it('renders a persistent CTA linking to the contact section', () => {
render(<Header />);
+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>
+44 -17
View File
@@ -1,36 +1,63 @@
'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>
<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} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-12">
<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="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>
);
+15 -6
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>
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>
);
+46 -15
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">
<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`}>
{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>
<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">
<CTA href="#contacto" label={hero.cta} variant="light" />
<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>
{/* 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>
);
+61 -9
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">
{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>
<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>
</Container>
</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);
});
});
+60 -1
View File
@@ -5,6 +5,12 @@ export const wordmark = {
parts: { before: 'cre', accent: '≡', after: 'er' },
};
export const brandPillars = {
pillar1: "Claridad",
pillar2: "Dirección",
pillar3: "Crecimiento",
};
export const hero = {
eyebrow: '— tu estratega de crecimiento —',
// Headline broken into segments for emphasis styling
@@ -15,6 +21,7 @@ export const hero = {
clarity: 'más claridad.', // underlined gold
},
cta: 'Define tu siguiente movimiento',
ctaSecundario: 'Ver cómo trabajo',
};
export const fraseAncla = {
@@ -25,6 +32,39 @@ export const fraseAncla = {
},
};
// The six inflection points, verbatim from the dueña's deck
// "Rumbo Estratégico · Propuesta Socio" (slide 2).
export const puntosInflexion = {
eyebrow: 'el contexto',
title: 'Todos los negocios consolidados llegan a un punto de inflexión',
items: [
{ name: 'Crecer', desc: 'Dar el siguiente salto sin saber por dónde.' },
{ name: 'Consolidarse', desc: 'Ordenar lo construido antes de escalar.' },
{ name: 'Recuperar rentabilidad', desc: 'El esfuerzo ya no se traduce en resultado.' },
{ name: 'Simplificar', desc: 'Demasiada complejidad operativa.' },
{ name: 'Expandirse', desc: 'Nuevos mercados, mismo mapa de decisión.' },
{ name: 'Replantear el portafolio', desc: 'Qué seguir, qué dejar ir.' },
],
};
// Slide 3 of the same deck. The four quotes are the client's own words —
// keep them verbatim, in first person.
export const dolores = {
eyebrow: 'lo que realmente sientes',
title: 'Los dolores detrás del punto de inflexión',
quotes: [
'Trabajo más y gano casi lo mismo que hace dos años.',
'Tengo varios proyectos y no sé en cuál enfocar mi tiempo y mi dinero.',
'Mi empresa no puede crecer si yo no estoy en cada decisión.',
'Lo que me trajo hasta aquí ya no me sirve para el siguiente nivel.',
],
// Split so the resolution renders in gold.
remate: {
before: 'Esto no se resuelve con más esfuerzo.',
accent: 'Se resuelve con una estrategia clara.',
},
};
export const paraQuien = {
title: '¿Llegaste a tu punto de inflexión?',
lead: 'Esto es para ti si…',
@@ -32,6 +72,16 @@ export const paraQuien = {
'tienes tracción y ya demostraste que sabes crecer',
'trabajar más ya no rinde como antes',
'necesitas claridad para decidir el siguiente movimiento',
'quieres profesionalizar sin perder tu esencia',
],
// Deck slide 9, "NO ES PARA". Kept impersonal on purpose: this disqualifies
// a profile, it does not accuse the reader.
leadNo: 'Esto no es para ti si…',
criteriaNo: [
'el negocio está en fase inicial o sin tracción',
'buscas una solución rápida de venta',
'buscas un curso, no una transformación',
'la empresa está en crisis de supervivencia, no en transición',
],
};
@@ -57,7 +107,9 @@ export const disciplinas = {
};
export const estratega = {
eyebrow: 'la mente detrás de creeer',
// Split so the brand name keeps its lowercase inside an uppercase eyebrow —
// CSS text-transform would otherwise render it as CREEER.
eyebrow: { before: 'la mente detrás de', brand: 'creeer' },
title: 'Sobre la estratega',
bio: 'Más de 30 años construyendo marcas. Hoy acompaño a empresarios que ya lograron tracción y necesitan claridad para su siguiente movimiento.',
quote: 'El tiempo no se compra, se invierte.',
@@ -94,3 +146,10 @@ export const footer = {
eslogan: 'Estrategia que transforma.',
legal: '© 2026 creeer',
};
// CC BY-SA 4.0 requires attribution + a link to the licence + noting the
// image was modified (both got a navy/gold duotone treatment).
export const creditosFotos = [
{ desc: 'Foto: Golden Gate Bridge — Dietmar Rabich', href: 'https://commons.wikimedia.org/wiki/File:San_Francisco_(CA,_USA),_Golden_Gate_Bridge_--_2022_--_3023_(bw).jpg', licencia: 'CC BY-SA 4.0', licenciaHref: 'https://creativecommons.org/licenses/by-sa/4.0/' },
{ desc: 'Foto: amanecer, Monte Nardi — Tony 1212', href: 'https://commons.wikimedia.org/wiki/File:Sunrise_over_the_summit_of_Mount_Nardi_from_the_south.jpg', licencia: 'CC BY-SA 4.0', licenciaHref: 'https://creativecommons.org/licenses/by-sa/4.0/' },
];
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB