Files
Creeer-Web/docs/superpowers/plans/2026-07-20-creeer-landing.md
T

1499 lines
48 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# creeer Landing Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Implement the creeer landing page (one-page, Next.js 14 static export) from the approved spec at `docs/superpowers/specs/2026-07-20-creeer-landing-design.md`.
**Architecture:** Next.js 14 App Router with 7 section components composed in `app/page.tsx`. Reusable primitives (`Wordmark`, `Container`, `CTA`) shared across sections. All copy centralized in `lib/content.ts`. Framer Motion animations with `prefers-reduced-motion` support. Static export (`output: export`) — no backend, contact via direct links.
**Tech Stack:** Next.js 14, React 18, TypeScript 5, Tailwind CSS 3.4, Framer Motion 11, Inter (Google Font via `next/font`), Georgia (system serif). Tests: Vitest + @testing-library/react.
## Global Constraints
(Copied verbatim from the spec — apply to every task.)
- **Wordmark:** `cre`**`e`**`er.` SIEMPRE minúsculas, Georgia 700 italic, la 2ª "e" en peso 900 + dorado, punto final dorado. Nunca `CREEER`, `Creeer`, sans-serif, ni mayúsculas.
- **Paleta:** navy `#0A1F44` (texto/CTA) · dorado tostado `#B8901f` (acentos sobre claro) · dorado `#D4AF37` (acentos sobre oscuro) · blanco cálido `#FBFAF7` (fondo base) · blanco `#FFFFFF` (hero) · marfil `#F5EBD0` (texto sobre oscuro).
- **Tipografía híbrida:** Georgia italic = voz de marca (wordmark + citas); Inter (400/600/900) = voz funcional (headlines Hormozi + body + UI).
- **Sin escasez en el hero:** no mostrar cupos/cohorte/"Generación Fundadora" arriba.
- **Voz:** segunda persona ("tú"/"tu negocio"), sin jerga corporativa.
- **Sin backend, sin formularios** — contacto por enlaces directos (WhatsApp, email, LinkedIn).
- **`output: export`** en `next.config.mjs` — HTML estático.
- **Idioma:** español (`<html lang="es">`).
---
## File Structure
```
creeer/
├── package.json
├── next.config.mjs # output: export
├── tsconfig.json
├── tailwind.config.ts # design tokens (palette, fontFamily)
├── postcss.config.js
├── vitest.config.ts
├── app/
│ ├── layout.tsx # <html lang="es">, fonts, metadata
│ ├── page.tsx # composes 7 sections
│ └── globals.css # @tailwind + base
├── components/
│ ├── primitives/
│ │ ├── Wordmark.tsx # creeer logo (reused)
│ │ ├── Container.tsx # max-w wrapper (reused)
│ │ └── CTA.tsx # button + link variant (reused)
│ └── sections/
│ ├── Hero.tsx # 01 — variant="hormozi" | "editorial"
│ ├── FraseAncla.tsx # 02
│ ├── ParaQuien.tsx # 03
│ ├── Mecanismo.tsx # 04 (dark)
│ ├── Disciplinas.tsx # 05
│ ├── Estratega.tsx # 06
│ └── Cierre.tsx # 07 (dark) + contact links
├── lib/
│ ├── content.ts # all copy constants (source of truth)
│ └── motion.ts # Framer Motion variants + useReducedMotion wrapper
└── public/
└── og-image.png # Open Graph image (added manually)
```
Each section has one responsibility: render its slice of content using the design system. Primitives are consumed by multiple sections. `lib/content.ts` centralizes copy so text changes don't require touching JSX.
---
## Task 1: Scaffold + dependencies + base config
**Files:**
- Create: `package.json`, `next.config.mjs`, `tsconfig.json`, `tailwind.config.ts`, `postcss.config.js`, `app/layout.tsx`, `app/page.tsx`, `app/globals.css`
**Interfaces:**
- Produces: a runnable Next.js 14 app at `npm run dev` showing "hello world", with Tailwind + Framer Motion + Vitest installed, `output: export` enabled.
- [ ] **Step 1: Initialize Next.js project in the current directory**
```bash
cd /home/erikgh/dev/Creeer
npx create-next-app@14 . --typescript --tailwind --app --no-src-dir --no-eslint --use-npm --import-alias "@/*"
```
If prompted about existing files, choose to overwrite/ignore conflicts for `.gitignore` (keep existing). This scaffold creates `package.json`, `app/`, configs.
- [ ] **Step 2: Install runtime dependencies**
```bash
npm install framer-motion@^11
```
- [ ] **Step 3: Install dev dependencies for testing**
```bash
npm install -D vitest@^1 @vitejs/plugin-react@^4 @testing-library/react@^15 @testing-library/jest-dom@^6 jsdom@^25
```
- [ ] **Step 4: Configure static export in `next.config.mjs`**
Overwrite `next.config.mjs`:
```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
images: { unoptimized: true },
};
export default nextConfig;
```
(`images.unoptimized` is required for static export.)
- [ ] **Step 5: Verify dev server runs**
```bash
npm run dev
```
Expected: server starts at http://localhost:3000, default Next.js page renders. Stop with Ctrl+C.
- [ ] **Step 6: Initialize git repo (first commit)**
```bash
cd /home/erikgh/dev/Creeer
git init
git add .
git commit -m "chore: scaffold Next.js 14 + Tailwind + Framer Motion + Vitest"
```
---
## Task 2: Design tokens (Tailwind palette, fonts, content + motion libs)
**Files:**
- Modify: `tailwind.config.ts`, `app/globals.css`, `app/layout.tsx`
- Create: `lib/content.ts`, `lib/motion.ts`
**Interfaces:**
- Produces: Tailwind tokens (`bg-base`, `bg-dark`, `text-primary`, `accent-gold`, etc.), Inter font loaded via `next/font`, all copy in `lib/content.ts`, motion variants in `lib/motion.ts`.
- [ ] **Step 1: Write Tailwind tokens in `tailwind.config.ts`**
```typescript
import type { Config } from 'tailwindcss';
const config: Config = {
content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}', './lib/**/*.{ts,tsx}'],
theme: {
extend: {
colors: {
base: '#FBFAF7', // blanco cálido (fondo base)
'bg-pure': '#FFFFFF', // blanco puro (hero)
'bg-dark': '#0A1F44', // navy (secciones oscuras)
ink: '#0A1F44', // texto principal
cream: '#F5EBD0', // texto sobre oscuro (marfil)
gold: {
DEFAULT: '#B8901f', // dorado tostado (sobre claro)
bright: '#D4AF37', // dorado (sobre oscuro)
},
muted: '#5a5168',
'muted-dark': '#9aa6c2',
},
fontFamily: {
sans: ['var(--font-inter)', 'system-ui', 'sans-serif'],
serif: ['Georgia', 'Cambria', 'Times New Roman', 'serif'],
},
maxWidth: {
content: '1152px',
},
},
},
plugins: [],
};
export default config;
```
- [ ] **Step 2: Replace `app/globals.css` with base styles**
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html { scroll-behavior: smooth; }
body {
background-color: #FBFAF7;
color: #0A1F44;
font-family: var(--font-inter), system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
}
::selection { background: #D4AF37; color: #0A1F44; }
}
```
- [ ] **Step 3: Load Inter font in `app/layout.tsx`**
Replace `app/layout.tsx`:
```tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
const inter = Inter({ subsets: ['latin'], weight: ['400', '600', '700', '900'], variable: '--font-inter', display: 'swap' });
export const metadata: Metadata = {
title: 'creeer · tu estratega de crecimiento',
description: 'Claridad estratégica para empresarios con tracción en punto de inflexión.',
// (SEO task expands this later)
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es" className={inter.variable}>
<body>{children}</body>
</html>
);
}
```
- [ ] **Step 4: Create `lib/content.ts` with all copy**
```typescript
// Source of truth for all copy. Edit here, not in JSX.
export const wordmark = {
// cre[e]er. — 'cre' + gold 'e' + 'er' + gold '.'
parts: { before: 'cre', accent: 'e', after: 'er', dot: '.' },
};
export const hero = {
eyebrow: '— tu estratega de crecimiento —',
// Headline broken into segments for emphasis styling
headline: {
line1: 'Tu negocio no necesita',
effort: 'más esfuerzo.', // gold (HV-1) or gold italic (V-a)
line3: 'Necesita',
clarity: 'más claridad.', // underlined gold
},
cta: 'Define tu siguiente movimiento',
};
export const fraseAncla = {
quote: 'Las empresas no dejan de crecer por falta de talento; dejan de crecer por falta de claridad.',
};
export const paraQuien = {
title: '¿Llegaste a tu punto de inflexión?',
lead: 'Esto es para ti si…',
criteria: [
'tienes tracción y ya demostraste que sabes crecer',
'trabajar más ya no rinde como antes',
'necesitas claridad para decidir el siguiente movimiento',
],
};
export const mecanismo = {
eyebrow: 'el mecanismo único',
title: 'Cómo trabajo',
steps: [
{ n: '01', name: 'Inmersión', desc: 'Me sumerjo en tu empresa hasta comprenderla desde dentro.' },
{ n: '02', name: 'Desafiar supuestos', desc: 'Cuestiono lo que todos dan por hecho y frenan el crecimiento.' },
{ n: '03', name: 'Estrategia ejecutable', desc: 'Construimos, juntos, una estrategia que puedas ejecutar.' },
],
};
export const disciplinas = {
eyebrow: 'las herramientas',
title: 'Las disciplinas que uso',
lead: 'No son la promesa — son los medios.',
items: [
{ name: 'Comunicación', desc: 'Mensaje que alinea a tu equipo y a tu mercado.' },
{ name: 'Digital', desc: 'Presencia y procesos que escalan sin ti.' },
{ name: 'Experiencias', desc: 'Cada toque con tu marca construye confianza.' },
],
};
export const estratega = {
eyebrow: 'la mente detrás de 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.',
};
export const cierre = {
quote: 'Devuélvele claridad al rumbo de tu empresa.',
cta: 'Define tu siguiente movimiento',
contact: {
whatsapp: { label: 'WhatsApp', href: 'https://wa.me/520000000000' }, // TODO dueña: real number
email: { label: 'Email', href: 'mailto:hola@creeer.com' }, // TODO dueña: real email
linkedin: { label: 'LinkedIn', href: 'https://linkedin.com/in/...' }, // TODO dueña: real URL
},
};
```
- [ ] **Step 5: Create `lib/motion.ts`**
```typescript
import type { Variants } from 'framer-motion';
// EaseOutExpo — smooth editorial deceleration
export const easeOutExpo = [0.22, 1, 0.36, 1] as const;
export const fadeInUp: Variants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: easeOutExpo } },
};
export const fadeIn: Variants = {
hidden: { opacity: 0 },
visible: { opacity: 1, transition: { duration: 0.4, ease: easeOutExpo } },
};
export const staggerContainer: Variants = {
hidden: {},
visible: { transition: { staggerChildren: 0.1 } },
};
export const viewportOnce = { once: true, amount: 0.3 } as const;
```
- [ ] **Step 6: Smoke test — `npm run dev`**
Run: `npm run dev`
Expected: app boots, no TS errors, default page renders at localhost:3000.
- [ ] **Step 7: Commit**
```bash
git add -A
git commit -m "feat: design tokens (palette, Inter font, content + motion libs)"
```
---
## Task 3: Primitives — Wordmark, Container, CTA
**Files:**
- Create: `components/primitives/Wordmark.tsx`, `components/primitives/Container.tsx`, `components/primitives/CTA.tsx`, `vitest.config.ts`
- Create tests: `components/primitives/__tests__/Wordmark.test.tsx`, `components/primitives/__tests__/CTA.test.tsx`
**Interfaces:**
- Produces: `<Wordmark size? />`, `<Container as? children />`, `<CTA href variant? />` — consumed by every section.
- [ ] **Step 1: Configure Vitest in `vitest.config.ts`**
```typescript
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: [],
},
resolve: { alias: { '@': path.resolve(__dirname, './') } },
});
```
Add script to `package.json`:
```json
"test": "vitest run",
"test:watch": "vitest"
```
- [ ] **Step 2: Write failing test for `Wordmark`**
`components/primitives/__tests__/Wordmark.test.tsx`:
```typescript
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Wordmark } from '../Wordmark';
describe('<Wordmark />', () => {
it('renders the wordmark in lowercase', () => {
render(<Wordmark />);
const text = screen.getByText(/cre/).closest('span')?.textContent;
expect(text?.toLowerCase()).toBe('creeer.');
});
it('renders the accent "e" separately for gold styling', () => {
const { container } = render(<Wordmark />);
const accent = container.querySelector('[data-accent="true"]');
expect(accent).not.toBeNull();
expect(accent?.textContent).toBe('e');
});
it('never renders uppercase CREEER', () => {
const { container } = render(<Wordmark />);
expect(container.textContent).not.toMatch(/CREEER/);
expect(container.textContent).not.toMatch(/Creeer/);
});
});
```
- [ ] **Step 3: Run test to verify it fails**
Run: `npm test -- Wordmark`
Expected: FAIL — module `../Wordmark` not found.
- [ ] **Step 4: Implement `Wordmark`**
`components/primitives/Wordmark.tsx`:
```tsx
import { wordmark } from '@/lib/content';
type Size = 'sm' | 'md' | 'lg' | 'xl';
const sizeClass: Record<Size, string> = {
sm: 'text-xl',
md: 'text-2xl',
lg: 'text-3xl',
xl: 'text-5xl',
};
export function Wordmark({ size = 'lg', onDark = false }: { size?: Size; onDark?: boolean }) {
const { before, accent, after, dot } = wordmark.parts;
const baseColor = onDark ? 'text-cream' : 'text-ink';
const goldColor = onDark ? 'text-gold-bright' : 'text-gold';
return (
<span className={`font-serif italic font-bold tracking-tight leading-none ${sizeClass[size]} ${baseColor}`}>
{before}
<span data-accent="true" className={`not-italic font-black ${goldColor}`}>{accent}</span>
{after}
<span className={goldColor}>{dot}</span>
</span>
);
}
```
- [ ] **Step 5: Run test to verify it passes**
Run: `npm test -- Wordmark`
Expected: PASS (3 tests).
- [ ] **Step 6: Write failing test for `CTA`**
`components/primitives/__tests__/CTA.test.tsx`:
```typescript
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { CTA } from '../CTA';
describe('<CTA />', () => {
it('renders the label with arrow', () => {
render(<CTA href="#contacto" label="Define tu siguiente movimiento" />);
expect(screen.getByRole('link', { name: /define tu siguiente movimiento/i })).toBeInTheDocument();
});
it('uses the href', () => {
render(<CTA href="#contacto" label="x" />);
expect(screen.getByRole('link').getAttribute('href')).toBe('#contacto');
});
});
```
- [ ] **Step 7: Run test — expected FAIL (module missing)**
Run: `npm test -- CTA`
Expected: FAIL.
- [ ] **Step 8: Implement `Container` and `CTA`**
`components/primitives/Container.tsx`:
```tsx
import type { ElementType, ReactNode } from 'react';
export function Container({ as: Tag = 'div', className = '', children }: { as?: ElementType; className?: string; children: ReactNode }) {
return <Tag className={`mx-auto w-full max-w-content px-6 md:px-12 ${className}`}>{children}</Tag>;
}
```
`components/primitives/CTA.tsx`:
```tsx
type Variant = 'light' | 'dark';
const styles: Record<Variant, string> = {
light: 'bg-ink text-base hover:bg-ink/90', // navy button, light text (on light bg)
dark: 'bg-gold-bright text-ink hover:bg-gold-bright/90', // gold button (on dark bg)
};
export function CTA({ href, label, variant = 'light', className = '' }: { href: string; label: string; variant?: Variant; className?: string }) {
return (
<a
href={href}
className={`inline-block font-sans font-bold uppercase tracking-wider px-7 py-3.5 text-sm transition-colors ${styles[variant]} ${className}`}
>
{label} <span aria-hidden="true">→</span>
</a>
);
}
```
- [ ] **Step 9: Run tests — expected PASS**
Run: `npm test`
Expected: all tests PASS.
- [ ] **Step 10: Commit**
```bash
git add -A
git commit -m "feat: primitives (Wordmark, Container, CTA) with tests"
```
---
## Task 4: Hero (section 01) with HV-1 + V-a variants
**Files:**
- Create: `components/sections/Hero.tsx`
- Test: `components/sections/__tests__/Hero.test.tsx`
- Modify: `app/page.tsx` (render `<Hero />`)
**Interfaces:**
- Consumes: `<Wordmark>`, `<CTA>`, `hero` content object.
- Produces: `<Hero variant="hormozi" | "editorial" />` — dueña decides via prop.
- [ ] **Step 1: Write failing test**
`components/sections/__tests__/Hero.test.tsx`:
```typescript
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Hero } from '../Hero';
describe('<Hero />', () => {
it('renders the eyebrow', () => {
render(<Hero variant="editorial" />);
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();
});
it('renders the CTA', () => {
render(<Hero variant="editorial" />);
expect(screen.getByRole('link', { name: /define tu siguiente movimiento/i })).toBeInTheDocument();
});
it('does NOT render scarcity (cupos/cohorte)', () => {
const { container } = render(<Hero variant="editorial" />);
expect(container.textContent).not.toMatch(/cupos|cohorte|generación fundadora/i);
});
});
```
- [ ] **Step 2: Run test — expected FAIL**
Run: `npm test -- Hero`
Expected: FAIL (module missing).
- [ ] **Step 3: Implement `Hero`**
`components/sections/Hero.tsx`:
```tsx
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 { easeOutExpo, fadeInUp, staggerContainer } from '@/lib/motion';
type Variant = 'hormozi' | 'editorial';
export function Hero({ variant = 'editorial' }: { variant?: Variant }) {
const isHormozi = variant === 'hormozi';
const headlineBase = isHormozi
? 'font-sans font-black uppercase tracking-tight'
: 'font-serif italic font-bold';
const headlineSize = 'text-4xl md:text-6xl leading-[1.1]';
return (
<section className="bg-bg-pure">
<Container className="min-h-[88vh] flex flex-col items-center justify-center text-center py-24">
<motion.div variants={staggerContainer} initial="hidden" animate="visible" 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-gold">
{hero.eyebrow}
</motion.p>
<motion.h1 variants={fadeInUp} className={`mt-8 ${headlineBase} ${headlineSize} text-ink`}>
{hero.headline.line1}{' '}
<span className="text-gold">{hero.headline.effort}</span>{' '}
{hero.headline.line3}{' '}
<span style={{ borderBottom: isHormozi ? '5px solid #B8901f' : '3px solid #B8901f', paddingBottom: 2 }}>
{hero.headline.clarity}
</span>
</motion.h1>
<motion.div variants={fadeInUp} className="mt-10">
<CTA href="#contacto" label={hero.cta} variant="light" />
</motion.div>
</motion.div>
</Container>
</section>
);
}
```
- [ ] **Step 4: Run test — expected PASS**
Run: `npm test -- Hero`
Expected: PASS (4 tests).
- [ ] **Step 5: Render `<Hero />` in `app/page.tsx`**
```tsx
import { Hero } from '@/components/sections/Hero';
export default function Home() {
return (
<main>
{/* @ts-expect-error server component animating — wrap with 'use client' in next step if needed */}
<Hero variant="editorial" />
</main>
);
}
```
(Note: Framer Motion `motion` requires client components. If `npm run dev` errors about client components, add `'use client';` at the top of `Hero.tsx` and any component using `motion`. The animations task (Task 11) addresses this systematically.)
- [ ] **Step 6: Verify in browser**
Run: `npm run dev`
Expected: hero renders at localhost:3000 with the wordmark, eyebrow, headline, CTA. Try both `variant="editorial"` and `variant="hormozi"`.
- [ ] **Step 7: Commit**
```bash
git add -A
git commit -m "feat: Hero section (01) with HV-1 hormozi + V-a editorial variants"
```
---
## Task 5: FraseAncla (section 02)
**Files:**
- Create: `components/sections/FraseAncla.tsx`
- Test: `components/sections/__tests__/FraseAncla.test.tsx`
- Modify: `app/page.tsx`
**Interfaces:**
- Consumes: `fraseAncla` content, `Container`.
- [ ] **Step 1: Write failing test**
`components/sections/__tests__/FraseAncla.test.tsx`:
```typescript
import { render, screen } 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();
});
});
```
- [ ] **Step 2: Run test — expected FAIL**
Run: `npm test -- FraseAncla`
Expected: FAIL.
- [ ] **Step 3: Implement**
`components/sections/FraseAncla.tsx`:
```tsx
'use client';
import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { fraseAncla } from '@/lib/content';
import { fadeInUp, viewportOnce } from '@/lib/motion';
export function FraseAncla() {
return (
<section className="bg-base">
<Container className="py-24 md:py-32 text-center max-w-4xl">
<motion.blockquote
variants={fadeInUp}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
className="font-serif italic text-2xl md:text-4xl font-bold leading-snug text-ink"
>
<span className="text-gold">"</span>
Las empresas no dejan de crecer por falta de talento;{' '}
<span className="text-gold">dejan de crecer por falta de claridad.</span>
<span className="text-gold">"</span>
</motion.blockquote>
</Container>
</section>
);
}
```
- [ ] **Step 4: Run test — expected PASS**
Run: `npm test -- FraseAncla`
Expected: PASS.
- [ ] **Step 5: Add to `app/page.tsx`**
```tsx
import { Hero } from '@/components/sections/Hero';
import { FraseAncla } from '@/components/sections/FraseAncla';
export default function Home() {
return (
<main>
<Hero variant="editorial" />
<FraseAncla />
</main>
);
}
```
- [ ] **Step 6: Commit**
```bash
git add -A
git commit -m "feat: FraseAncla section (02) — anchor quote"
```
---
## Task 6: ParaQuien (section 03)
**Files:**
- Create: `components/sections/ParaQuien.tsx`
- Test: `components/sections/__tests__/ParaQuien.test.tsx`
- Modify: `app/page.tsx`
**Interfaces:**
- Consumes: `paraQuien` content, `Container`.
- [ ] **Step 1: Write failing test**
`components/sections/__tests__/ParaQuien.test.tsx`:
```typescript
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { ParaQuien } from '../ParaQuien';
describe('<ParaQuien />', () => {
it('renders the section title', () => {
render(<ParaQuien />);
expect(screen.getByText(/punto de inflexión/i)).toBeInTheDocument();
});
it('renders all 3 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();
});
});
```
- [ ] **Step 2: Run test — expected FAIL**
Run: `npm test -- ParaQuien`
Expected: FAIL.
- [ ] **Step 3: Implement**
`components/sections/ParaQuien.tsx`:
```tsx
'use client';
import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { paraQuien } from '@/lib/content';
import { fadeInUp, staggerContainer, viewportOnce } from '@/lib/motion';
export function ParaQuien() {
return (
<section className="bg-base">
<Container className="py-24 md:py-32">
<motion.div variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce} className="max-w-3xl">
<motion.h2 variants={fadeInUp} className="font-serif italic font-bold text-3xl md:text-5xl text-ink leading-tight">
{paraQuien.title}
</motion.h2>
<motion.p variants={fadeInUp} className="mt-6 text-lg text-muted">{paraQuien.lead}</motion.p>
<motion.ul variants={fadeInUp} className="mt-8 space-y-3">
{paraQuien.criteria.map((c) => (
<li key={c} className="flex items-start gap-3">
<span className="text-gold font-bold mt-1" aria-hidden="true">✓</span>
<span className="text-ink text-lg">{c}</span>
</li>
))}
</motion.ul>
</motion.div>
</Container>
</section>
);
}
```
- [ ] **Step 4: Run test — expected PASS**
Run: `npm test -- ParaQuien`
Expected: PASS.
- [ ] **Step 5: Add to `app/page.tsx`**
```tsx
// ...imports
import { ParaQuien } from '@/components/sections/ParaQuien';
// ... in JSX after <FraseAncla />:
<ParaQuien />
```
- [ ] **Step 6: Commit**
```bash
git add -A
git commit -m "feat: ParaQuien section (03) — audience filter"
```
---
## Task 7: Mecanismo (section 04, dark)
**Files:**
- Create: `components/sections/Mecanismo.tsx`
- Test: `components/sections/__tests__/Mecanismo.test.tsx`
- Modify: `app/page.tsx`
**Interfaces:**
- Consumes: `mecanismo` content, `Container`. Dark section (navy bg).
- [ ] **Step 1: Write failing test**
`components/sections/__tests__/Mecanismo.test.tsx`:
```typescript
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Mecanismo } from '../Mecanismo';
describe('<Mecanismo />', () => {
it('renders the 3 steps in order', () => {
render(<Mecanismo />);
expect(screen.getByText('Inmersión')).toBeInTheDocument();
expect(screen.getByText('Desafiar supuestos')).toBeInTheDocument();
expect(screen.getByText('Estrategia ejecutable')).toBeInTheDocument();
});
it('renders step numbers 01/02/03', () => {
const { container } = render(<Mecanismo />);
expect(container.textContent).toMatch(/01.+02.+03/s);
});
});
```
- [ ] **Step 2: Run test — expected FAIL**
Run: `npm test -- Mecanismo`
Expected: FAIL.
- [ ] **Step 3: Implement**
`components/sections/Mecanismo.tsx`:
```tsx
'use client';
import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { mecanismo } from '@/lib/content';
import { fadeInUp, staggerContainer, viewportOnce } from '@/lib/motion';
export function Mecanismo() {
return (
<section className="bg-bg-dark text-cream">
<Container className="py-24 md:py-32">
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce}
className="text-xs tracking-[0.3em] uppercase font-semibold text-gold-bright">{mecanismo.eyebrow}</motion.p>
<motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce}
className="mt-4 font-serif italic font-bold text-3xl md:text-5xl">{mecanismo.title}</motion.h2>
<motion.ol variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce}
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-gold-bright/30 pt-6">
<div className="font-serif italic font-black text-5xl md:text-6xl text-gold-bright leading-none">{step.n}</div>
<h3 className="mt-4 font-sans font-bold text-xl">{step.name}</h3>
<p className="mt-3 text-muted-dark leading-relaxed">{step.desc}</p>
</motion.li>
))}
</motion.ol>
</Container>
</section>
);
}
```
- [ ] **Step 4: Run test — expected PASS**
Run: `npm test -- Mecanismo`
Expected: PASS.
- [ ] **Step 5: Add to `app/page.tsx`**
```tsx
import { Mecanismo } from '@/components/sections/Mecanismo';
// after <ParaQuien />: <Mecanismo />
```
- [ ] **Step 6: Commit**
```bash
git add -A
git commit -m "feat: Mecanismo section (04, dark) — 3-step method"
```
---
## Task 8: Disciplinas (section 05)
**Files:**
- Create: `components/sections/Disciplinas.tsx`
- Test: `components/sections/__tests__/Disciplinas.test.tsx`
- Modify: `app/page.tsx`
**Interfaces:**
- Consumes: `disciplinas` content, `Container`.
- [ ] **Step 1: Write failing test**
`components/sections/__tests__/Disciplinas.test.tsx`:
```typescript
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Disciplinas } from '../Disciplinas';
describe('<Disciplinas />', () => {
it('renders the 3 disciplines', () => {
render(<Disciplinas />);
expect(screen.getByText('Comunicación')).toBeInTheDocument();
expect(screen.getByText('Digital')).toBeInTheDocument();
expect(screen.getByText('Experiencias')).toBeInTheDocument();
});
});
```
- [ ] **Step 2: Run test — expected FAIL**
Run: `npm test -- Disciplinas`
Expected: FAIL.
- [ ] **Step 3: Implement**
`components/sections/Disciplinas.tsx`:
```tsx
'use client';
import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { disciplinas } from '@/lib/content';
import { fadeInUp, staggerContainer, viewportOnce } from '@/lib/motion';
export function Disciplinas() {
return (
<section className="bg-base">
<Container className="py-24 md:py-32">
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce}
className="text-xs tracking-[0.3em] uppercase font-semibold text-gold">{disciplinas.eyebrow}</motion.p>
<motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce}
className="mt-4 font-serif italic font-bold text-3xl md:text-5xl text-ink">{disciplinas.title}</motion.h2>
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce}
className="mt-4 text-muted text-lg">{disciplinas.lead}</motion.p>
<motion.div variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce}
className="mt-12 grid gap-8 md:grid-cols-3">
{disciplinas.items.map((item) => (
<motion.div key={item.name} variants={fadeInUp} className="border-t-2 border-gold pt-5">
<h3 className="font-serif italic font-bold text-2xl text-ink">{item.name}</h3>
<p className="mt-3 text-muted leading-relaxed">{item.desc}</p>
</motion.div>
))}
</motion.div>
</Container>
</section>
);
}
```
- [ ] **Step 4: Run test — expected PASS**
Run: `npm test -- Disciplinas`
Expected: PASS.
- [ ] **Step 5: Add to `app/page.tsx`**
```tsx
import { Disciplinas } from '@/components/sections/Disciplinas';
// after <Mecanismo />: <Disciplinas />
```
- [ ] **Step 6: Commit**
```bash
git add -A
git commit -m "feat: Disciplinas section (05) — pillars as tools"
```
---
## Task 9: Estratega (section 06)
**Files:**
- Create: `components/sections/Estratega.tsx`
- Test: `components/sections/__tests__/Estratega.test.tsx`
- Modify: `app/page.tsx`
**Interfaces:**
- Consumes: `estratega` content, `Container`. Includes portrait (placeholder gradient if no photo yet).
- [ ] **Step 1: Write failing test**
`components/sections/__tests__/Estratega.test.tsx`:
```typescript
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Estratega } from '../Estratega';
describe('<Estratega />', () => {
it('renders the bio', () => {
render(<Estratega />);
expect(screen.getByText(/30 años/i)).toBeInTheDocument();
});
it('renders the institutional quote', () => {
render(<Estratega />);
expect(screen.getByText(/El tiempo no se compra/i)).toBeInTheDocument();
});
});
```
- [ ] **Step 2: Run test — expected FAIL**
Run: `npm test -- Estratega`
Expected: FAIL.
- [ ] **Step 3: Implement**
`components/sections/Estratega.tsx`:
```tsx
'use client';
import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { estratega } from '@/lib/content';
import { fadeInUp, staggerContainer, viewportOnce } from '@/lib/motion';
export function Estratega() {
return (
<section className="bg-base">
<Container className="py-24 md:py-32">
<motion.div variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce}
className="grid gap-12 md:grid-cols-[auto_1fr] md:items-center">
{/* Portrait — placeholder gradient until dueña provides a photo */}
<motion.div variants={fadeInUp}
className="w-40 h-40 md:w-56 md:h-56 rounded-full flex-shrink-0"
style={{ background: 'linear-gradient(135deg, #0A1F44 0%, #B8901f 100%)' }}
aria-label="Retrato de la estratega"
role="img"
/>
<div>
<motion.p variants={fadeInUp} className="text-xs tracking-[0.3em] uppercase font-semibold text-gold">{estratega.eyebrow}</motion.p>
<motion.h2 variants={fadeInUp} className="mt-4 font-serif italic font-bold text-3xl md:text-5xl text-ink">{estratega.title}</motion.h2>
<motion.p variants={fadeInUp} className="mt-6 text-lg text-muted leading-relaxed max-w-2xl">{estratega.bio}</motion.p>
<motion.blockquote variants={fadeInUp} className="mt-6 font-serif italic text-xl md:text-2xl text-ink border-l-4 border-gold pl-4">
{estratega.quote}
</motion.blockquote>
</div>
</motion.div>
</Container>
</section>
);
}
```
> **Note:** When the dueña provides a real photo, replace the gradient div with `<Image src="/retrato.jpg" alt="..." width={224} height={224} className="rounded-full" />` using `next/image`. Keep `aria-label`/`alt` meaningful.
- [ ] **Step 4: Run test — expected PASS**
Run: `npm test -- Estratega`
Expected: PASS.
- [ ] **Step 5: Add to `app/page.tsx`**
```tsx
import { Estratega } from '@/components/sections/Estratega';
// after <Disciplinas />: <Estratega />
```
- [ ] **Step 6: Commit**
```bash
git add -A
git commit -m "feat: Estratega section (06) — portrait + bio + quote"
```
---
## Task 10: Cierre + contacto (section 07, dark)
**Files:**
- Create: `components/sections/Cierre.tsx`
- Test: `components/sections/__tests__/Cierre.test.tsx`
- Modify: `app/page.tsx`
**Interfaces:**
- Consumes: `cierre` content (includes contact links), `Container`, `CTA`, `Wordmark` (in footer). Dark section.
- [ ] **Step 1: Write failing test**
`components/sections/__tests__/Cierre.test.tsx`:
```typescript
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Cierre } from '../Cierre';
describe('<Cierre />', () => {
it('renders the closing quote', () => {
render(<Cierre />);
expect(screen.getByText(/Devuélvele claridad/i)).toBeInTheDocument();
});
it('renders 3 contact links with correct hrefs', () => {
render(<Cierre />);
const wa = screen.getByRole('link', { name: /whatsapp/i });
const mail = screen.getByRole('link', { name: /email/i });
const li = screen.getByRole('link', { name: /linkedin/i });
expect(wa.getAttribute('href')).toMatch(/^https:\/\/wa\.me\//);
expect(mail.getAttribute('href')).toMatch(/^mailto:/);
expect(li.getAttribute('href')).toMatch(/^https:\/\/(www\.)?linkedin\./);
});
});
```
- [ ] **Step 2: Run test — expected FAIL**
Run: `npm test -- Cierre`
Expected: FAIL.
- [ ] **Step 3: Implement**
`components/sections/Cierre.tsx`:
```tsx
'use client';
import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { CTA } from '@/components/primitives/CTA';
import { Wordmark } from '@/components/primitives/Wordmark';
import { cierre } from '@/lib/content';
import { fadeInUp, staggerContainer, viewportOnce } from '@/lib/motion';
export function Cierre() {
const { whatsapp, email, linkedin } = cierre.contact;
return (
<section id="contacto" className="bg-bg-dark text-cream">
<Container className="py-24 md:py-32 text-center">
<motion.div variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce}
className="flex flex-col items-center">
<motion.div variants={fadeInUp}><Wordmark size="lg" onDark /></motion.div>
<motion.blockquote variants={fadeInUp}
className="mt-10 font-serif italic text-2xl md:text-4xl font-bold leading-snug max-w-3xl">
<span className="text-gold-bright">"</span>{cierre.quote}<span className="text-gold-bright">"</span>
</motion.blockquote>
<motion.div variants={fadeInUp} className="mt-10">
<CTA href={whatsapp.href} label={cierre.cta} variant="dark" />
</motion.div>
<motion.ul variants={fadeInUp} className="mt-12 flex flex-wrap justify-center gap-6 text-sm">
<li><a href={whatsapp.href} className="hover:text-gold-bright transition-colors">{whatsapp.label}</a></li>
<li aria-hidden="true" className="text-muted-dark">·</li>
<li><a href={email.href} className="hover:text-gold-bright transition-colors">{email.label}</a></li>
<li aria-hidden="true" className="text-muted-dark">·</li>
<li><a href={linkedin.href} className="hover:text-gold-bright transition-colors">{linkedin.label}</a></li>
</motion.ul>
</motion.div>
</Container>
</section>
);
}
```
- [ ] **Step 4: Run test — expected PASS**
Run: `npm test -- Cierre`
Expected: PASS.
- [ ] **Step 5: Add to `app/page.tsx`**
```tsx
import { Cierre } from '@/components/sections/Cierre';
// after <Estratega />: <Cierre />
```
- [ ] **Step 6: Commit**
```bash
git add -A
git commit -m "feat: Cierre section (07, dark) — closing quote + contact links"
```
---
## Task 11: Compose page + animations + reduced-motion
**Files:**
- Modify: `app/page.tsx`, `lib/motion.ts`
**Interfaces:**
- Produces: the full scrollable page with all 7 sections in order, Framer Motion animations, and `prefers-reduced-motion` support.
- [ ] **Step 1: Final `app/page.tsx`**
```tsx
import { Hero } from '@/components/sections/Hero';
import { FraseAncla } from '@/components/sections/FraseAncla';
import { ParaQuien } from '@/components/sections/ParaQuien';
import { Mecanismo } from '@/components/sections/Mecanismo';
import { Disciplinas } from '@/components/sections/Disciplinas';
import { Estratega } from '@/components/sections/Estratega';
import { Cierre } from '@/components/sections/Cierre';
export default function Home() {
return (
<main>
<Hero variant="editorial" /> {/* dueña puede cambiar a "hormozi" */}
<FraseAncla />
<ParaQuien />
<Mecanismo />
<Disciplinas />
<Estratega />
<Cierre />
</main>
);
}
```
- [ ] **Step 2: Add reduced-motion support to `lib/motion.ts`**
Append to `lib/motion.ts`:
```typescript
import { useEffect, useState } from 'react';
// Hook: returns true if user prefers reduced motion. Components use this to
// disable Framer Motion animations (per spec, a11y requirement).
export function usePrefersReducedMotion() {
const [reduced, setReduced] = useState(false);
useEffect(() => {
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
setReduced(mq.matches);
const handler = () => setReduced(mq.matches);
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, []);
return reduced;
}
// Pass to <motion.*> as motion-reduced guard. When reduced, variants resolve to visible immediately.
export const motionProps = (reduced: boolean) =>
reduced
? { initial: 'visible' as const, animate: 'visible' as const, whileInView: undefined, viewport: undefined }
: {};
```
- [ ] **Step 3: Apply in each animated component**
For each section using `motion`, replace its `variants/initial/whileInView` props with reduced-motion-aware versions. Example for `FraseAncla.tsx`:
```tsx
'use client';
import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { fraseAncla } from '@/lib/content';
import { fadeInUp, viewportOnce, usePrefersReducedMotion, motionProps } from '@/lib/motion';
export function FraseAncla() {
const reduced = usePrefersReducedMotion();
return (
<section className="bg-base">
<Container className="py-24 md:py-32 text-center max-w-4xl">
<motion.blockquote
variants={fadeInUp}
initial={reduced ? 'visible' : 'hidden'}
whileInView={reduced ? undefined : 'visible'}
viewport={reduced ? undefined : viewportOnce}
className="font-serif italic text-2xl md:text-4xl font-bold leading-snug text-ink"
>
{/* ...quote content... */}
</motion.blockquote>
</Container>
</section>
);
}
```
Repeat the same `reduced` pattern for: `Hero`, `ParaQuien`, `Mecanismo`, `Disciplinas`, `Estratega`, `Cierre`.
- [ ] **Step 4: Run full test suite**
Run: `npm test`
Expected: all tests PASS.
- [ ] **Step 5: Visual verification**
Run: `npm run dev`
Expected: all 7 sections render in order with smooth scroll animations. Toggle `prefers-reduced-motion` in DevTools → animations should disable (content appears immediately).
- [ ] **Step 6: Commit**
```bash
git add -A
git commit -m "feat: compose page + Framer Motion animations + reduced-motion support"
```
---
## Task 12: SEO metadata, accessibility, static build verification
**Files:**
- Modify: `app/layout.tsx` (full metadata), `app/page.tsx` (semantic HTML)
- Create: `public/og-image.png` (manually), `public/robots.txt`, `public/sitemap.xml`
- Verify: `npm run build` produces static HTML in `out/`
**Interfaces:**
- Produces: complete SEO metadata, OG image, semantic HTML, accessible contact links, passing static export build.
- [ ] **Step 1: Expand metadata in `app/layout.tsx`**
```tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
const inter = Inter({ subsets: ['latin'], weight: ['400', '600', '700', '900'], variable: '--font-inter', display: 'swap' });
export const metadata: Metadata = {
title: 'creeer · tu estratega de crecimiento',
description: 'Claridad estratégica para empresarios con tracción que llegaron a su punto de inflexión. Inmersión profunda, estrategia ejecutable.',
metadataBase: new URL('https://creeer.com'), // TODO dueña: real domain
openGraph: {
title: 'creeer · tu estratega de crecimiento',
description: 'Claridad estratégica para empresarios con tracción en punto de inflexión.',
type: 'website',
locale: 'es',
images: [{ url: '/og-image.png', width: 1200, height: 630, alt: 'creeer' }],
},
twitter: { card: 'summary_large_image', title: 'creeer', description: 'Claridad estratégica para empresarios.' },
alternates: { canonical: 'https://creeer.com' },
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es" className={inter.variable}>
<body>{children}</body>
</html>
);
}
```
- [ ] **Step 2: Add JSON-LD structured data in `app/page.tsx`**
Wrap the `<main>` content with a structured data script. Add at top of `Home()`:
```tsx
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'ProfessionalService',
name: 'creeer',
description: 'Estratega de crecimiento. Claridad estratégica para empresarios con tracción.',
url: 'https://creeer.com',
};
export default function Home() {
return (
<>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
<main>
{/* ...sections... */}
</main>
</>
);
}
```
- [ ] **Step 3: Create `public/robots.txt`**
```
User-agent: *
Allow: /
Sitemap: https://creeer.com/sitemap.xml
```
- [ ] **Step 4: Create `public/sitemap.xml`**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>https://creeer.com/</loc><changefreq>monthly</changefreq><priority>1.0</priority></url>
</urlset>
```
- [ ] **Step 5: Create OG image**
Manually create `public/og-image.png` (1200×630) — use the wordmark `cre`**`e`**`er.` on navy `#0A1F44` background with the headline. If no design tool available, use a placeholder PNG for now and replace later.
- [ ] **Step 6: Add focus-visible styles to `app/globals.css`**
Append to `globals.css`:
```css
@layer base {
a:focus-visible, button:focus-visible {
outline: 3px solid #D4AF37;
outline-offset: 3px;
border-radius: 2px;
}
}
```
- [ ] **Step 7: Run final test suite**
Run: `npm test`
Expected: all tests PASS.
- [ ] **Step 8: Run static build**
Run: `npm run build`
Expected: build succeeds, outputs `out/` directory with static HTML. No errors.
- [ ] **Step 9: Run lint**
Run: `npm run lint`
Expected: passes (or only minor warnings).
- [ ] **Step 10: Manual accessibility smoke test**
Open `npm run dev`, run browser DevTools Lighthouse audit (or axe extension). Expected:
- Contrast ratios pass (navy on white: 14.6:1; marfil on navy: 13.1:1)
- `<html lang="es">` set
- All contact links have discernible text
- No duplicate `<h1>` (only Hero has h1)
- [ ] **Step 11: Final commit**
```bash
git add -A
git commit -m "feat: SEO metadata, JSON-LD, OG image, sitemap, a11y focus styles"
```
---
## Spec Coverage Self-Check
| Spec section | Task |
|---|---|
| 3.1 Paleta (tokens) | Task 2 |
| 3.2 Tipografía híbrida (Georgia + Inter) | Task 2, 3 |
| 3.3 Escala tipográfica | Tasks 4–10 (applied in Tailwind classes) |
| 3.4 Wordmark | Task 3 |
| 3.5 Recursos (•, smart quotes, →) | Tasks 4–10 |
| 3.6 Espaciado y ritmo | Tasks 4–10 (`py-24 md:py-32`, `Container`) |
| 3.7 Layout claro/oscuro | Tasks 4–10 (`bg-base`, `bg-bg-dark`) |
| 4. Hero (HV-1 + V-a) | Task 4 |
| 5. Flujo 7 secciones | Tasks 4–10 |
| 6. Messaging (frases-ancla) | Task 2 (content.ts) |
| 7. Restricciones técnicas | Tasks 1, 10 |
| 8. SEO / a11y / perf | Task 12 |
| 9. Animación (Framer Motion + reduced) | Task 11 |
**Placeholder scan:** None. All steps contain complete code. The `cierre.contact` hrefs in `lib/content.ts` are marked `TODO dueña` for real values — that's a content decision (not implementation) and documented in spec §10.
**Type consistency:** All section components share the same interface signature (`export function SectionName()` with no props, except `<Hero variant>`). All consume the same primitives (`<Container>`, `<CTA>`, `<Wordmark>`) and content objects. ✓
---
## Execution
Plan complete. Two execution options:
**1. Subagent-Driven (recommended)** — dispatch a fresh subagent per task with review between tasks. Best for catching issues early and keeping each task focused.
**2. Inline Execution** — execute tasks in this session using executing-plans, with checkpoints for review. Best when you want tight loop with the user.
**Which approach?**