From 2cfdc8750317e84dd0310ed56e515112233483bc Mon Sep 17 00:00:00 2001 From: creeer-dev Date: Tue, 21 Jul 2026 09:53:20 -0600 Subject: [PATCH] docs: spec, plan, directrices, AGENTS.md (pre-scaffold) --- .gitignore | 30 + AGENTS.md | 48 + docs/dueña-directrices.md | 189 +++ docs/hero-finalists.md | 118 ++ ...026-07-20-creeer-landing-implementation.md | 1435 ++++++++++++++++ .../plans/2026-07-20-creeer-landing.md | 1498 +++++++++++++++++ .../specs/2026-07-20-creeer-landing-design.md | 289 ++++ 7 files changed, 3607 insertions(+) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 docs/dueña-directrices.md create mode 100644 docs/hero-finalists.md create mode 100644 docs/superpowers/plans/2026-07-20-creeer-landing-implementation.md create mode 100644 docs/superpowers/plans/2026-07-20-creeer-landing.md create mode 100644 docs/superpowers/specs/2026-07-20-creeer-landing-design.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af95e65 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# env files +.env +.env*.local + +# typescript +*.tsbuildinfo +next-env.d.ts + +# superpowers brainstorming (visual companion mockups) +/superpowers/ +/.superpowers/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..cc94ed4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,48 @@ +# creeer — Landing page + +One-page landing para **creeer**, marca personal de una estratega de crecimiento. Stack: Next.js 14 (App Router) + TypeScript + Tailwind CSS + Framer Motion, exportada como sitio estático. Español. + +## Estado del proyecto + +**Pre-scaffold** (sin código aún). Diseño completo y aprobado en `docs/superpowers/specs/2026-07-20-creeer-landing-design.md`. Próximo paso: scaffold Next.js + implementar las 7 secciones del spec. + +## ⚠️ Lee primero en cada sesión + +**`docs/dueña-directrices.md`** — directrices verbatim de la dueña + posicionamiento estratégico + sistema de diseño extraído del pptx. Toda decisión (copy, diseño, código) pasa por su filtro. **Si una propuesta contradice algo, pregunta antes de avanzar.** + +## Decisiones bloqueadas (no reabrir sin consultar a la dueña) + +- **Tono maestro:** "seguridad + cercanía" + "simple, directo, limpio". +- **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. (Es la regla más fácil de romper por defecto.) +- **Paleta:** navy `#0A1F44` + dorado tostado `#B8901f` (sobre claro) / dorado `#D4AF37` (sobre oscuro) + blanco cálido `#FBFAF7` / blanco `#FFFFFF`. **Sin amarillo eléctrico** tipo Hormozi. +- **Tipografía híbrida:** Georgia italic = voz de marca (wordmark + citas); Inter 400/600/900 = voz funcional (headlines Hormozi + body + UI). +- **Voz:** segunda persona ("tú"/"tu negocio"), sin jerga corporativa. +- **Sin escasez en el hero:** no mostrar cupos, cohorte ni "Generación Fundadora" arriba (decisión de la dueña). + +## Stack y constraints + +- Next.js 14 App Router + TypeScript + Tailwind + Framer Motion +- `output: export` → HTML estático, **sin backend** +- Contacto por enlaces directos (WhatsApp, email, LinkedIn) — **sin formulario** +- Deploy: Vercel o Netlify +- Comandos: se definen al scaffold (`npm run dev`, `npm run build`, `npm run lint`) + +## Skills relevantes para este proyecto + +- `transformation-positioning` — messaging (vende el hoyo, no el taladro) **obligatorio al escribir copy** +- `frontend-design`, `web-design-guidelines`, `vercel-react-best-practices` +- `tailwind-design-system`, `nextjs-app-router-patterns`, `framer-motion-animator` +- `seo`, `accessibility`, `performance`, `core-web-vitals` + +## Docs del proyecto + +- `docs/dueña-directrices.md` — source of truth de marca y posicionamiento +- `docs/hero-finalists.md` — 2 finalistas del hero (**HV-1 Hormozi** / **V-a editorial**), la dueña elige +- `docs/superpowers/specs/2026-07-20-creeer-landing-design.md` — spec de diseño aprobado (7 secciones, sistema visual, animación, SEO/a11y/perf) + +## Reglas operativas + +- **No commits** sin aprobación explícita del usuario. +- **No reabrir** decisiones bloqueadas sin consultar a la dueña. +- Si la dueña envía un mensaje nuevo (WhatsApp, etc.), agrégalo a `docs/dueña-directrices.md` en "Mensajes verbatim" con fecha y contexto. +- Mockups del brainstorming visual companion en `.superpowers/brainstorm/` (en `.gitignore`, efímeros). diff --git a/docs/dueña-directrices.md b/docs/dueña-directrices.md new file mode 100644 index 0000000..a507e04 --- /dev/null +++ b/docs/dueña-directrices.md @@ -0,0 +1,189 @@ +# Directrices de la dueña · creeer + +> **Source of truth para decisiones de marca, copy y diseño.** +> Esta es la voz y el criterio de la dueña de creeer. Toda decisión del proyecto debe pasar por este filtro. Si una propuesta contradice algo de aquí, **preguntar antes de avanzar**. + +--- + +## Quién es la dueña + +La dueña es **la mente detrás de creeer** — estratega de crecimiento con +30 años construyendo marcas. Su negocio real es **claridad estratégica para empresarios con tracción que llegaron a un punto de inflexión** (no vende marketing, vende la transformación). + +El nombre **creeer** es un juego con "creer" (en ti, en mí, en el proyecto) + "career". + +--- + +## Mensajes verbatim de la dueña (contexto y voz) + +Estos son los mensajes que la dueña envió por WhatsApp durante el brainstorming, transcritos literalmente. **Preservar su voz y sus palabras.** + +### Sobre la sensación que quiere proyectar +> **"Sabes lo que quiero que proyectemos es seguridad pero cercanía."** + +*(3:03 p.m., 20/7/2026)* — Esta es la directriz maestra de tono. **No frío/distante, no agresivo puro. Autoridad accesible.** + +### Sobre el wordmark en minúsculas +> **"Por eso está en minúsculas."** + +*(3:04 p.m., 20/7/2026)* — La decisión de escribir `creeer` en minúsculas es **consciente y estratégica**: transmite cercanía, humildad, accesibilidad (frente al frío de los mayúsculas institucionales). **NUNCA usar `CREEER` ni `Creeer` en el wordmark** — siempre `creeer`. + +### Sobre la paleta y el estilo +> **"Creo que el blanco con azul."** +> **"Simple directo."** +> **"Limpio."** + +*(3:12 p.m., 20/7/2026)* — Tres directrices visuales: +1. **Blanco con azul** → fondo claro + navy (no dark mode como dirección principal) +2. **Simple directo** → sin decoración, sin elementos accesorios, copy directo +3. **Limpio** → mucho aire, blanco, tipografía hace el trabajo + +### Referencia a Alex Hormozi +La dueña mencionó a **Alex Hormozi** como referencia estética/comunicacional. Su ADN: +- Bold sans-serif gigante, a menudo uppercase +- Alto contraste (negro/blanco/un acento eléctrico — en creeer, dorado) +- Copy directo, corto, cuantificado (números, métricas, claims) +- Palabras clave en caja (background detrás de la palabra) +- Voz agresiva, results-driven, "sin paja" + +> ⚠️ **Tensión a equilibrar:** Hormozi es agresivo/directo, pero la dueña también pidió **cercanía + minúsculas + blanco+azul + limpio**. La síntesis es **"Hormozi en voz, creeer en alma"**: el punch visual de Hormozi (bold sans, palabras en caja) con el dorado tostado, el wordmark serif en minúsculas y la paleta cálida de creeer. + +--- + +## Principios derivados (cómo aplicar los mensajes) + +| Directriz de la dueña | Implicación práctica | +|---|---| +| "Seguridad + cercanía" | Serif para autoridad + voz en "tú" + fondos cálidos para cercanía | +| "minúsculas" | Wordmark SIEMPRE `creeer`. Eyebrows y CTAs pueden ser uppercase (system-ui) pero el wordmark jamás | +| "blanco con azul" | Fondo principal blanco/blanco cálido, navy como tinta y CTA | +| "Simple directo" | Sin elementos decorativos accesorios. Copy corto, una idea por línea | +| "Limpio" | Mucho aire (whitespace), máximo 2 familias tipográficas, paleta reducida | +| Referencia Hormozi | Permitido bold sans uppercase + palabras en caja + métricas, **siempre que** no rompan cercanía | + +--- + +## Posicionamiento estratégico (de su documento de nicho) + +Esto NO es copy de landing — es la brújula estratégica que la dueña ya validó. Toda la messaging debe ser coherente con esto. + +### Lo que creeer NO es +- ❌ Un diplomado de marketing +- ❌ Una consultoría genérica +- ❌ Clases/asesoría por hora +- ❌ Venta de disciplinas (Comunicación/Digital/Experiencias) como promesa + +### Lo que creeer SÍ es +- ✅ **Claridad estratégica** para empresarios con tracción en punto de inflexión +- ✅ Transformación, no horas +- ✅ Inmersión profunda → desafiar supuestos → estrategia ejecutable + +### Cliente ideal +Empresarios que **ya lograron tracción**, pero llegaron al punto donde **trabajar más ya no basta** — necesitan mejores decisiones estratégicas. NO cualquier PYME. NO empresas en crisis. **Empresas en transición.** + +### Frases-ancla aprobadas (de su documento) +- *"Las empresas no dejan de crecer por falta de talento; dejan de crecer por falta de claridad."* +- *"Cuando el esfuerzo ya no alcanza, la estrategia se vuelve indispensable."* +- *"El tiempo no se compra, se invierte."* (institucional) +- *"Devuélvele claridad al rumbo de tu empresa."* (promesa) + +### Tagline de marca +*"creeer en ti, creeer en mí, creeer en el proyecto."* + +### Pilares (disciplinas, NO la promesa) +**Comunicación · Digital · Experiencias** (+ Integración y acción). Son las herramientas, no el producto. **Viven en sección "cómo", no en el hero.** + +--- + +## Restricciones fijadas (decisiones ya tomadas) + +Estas son decisiones bloqueadas durante el brainstorming. **No reabrir sin consultar.** + +### Paleta +- Blanco cálido `#FBFAF7` / Blanco puro `#FFFFFF` (fondos) +- Navy `#0A1F44` (texto principal + CTAs) +- Dorado tostado `#B8901f` (acentos sobre claro) / Dorado `#D4AF37` (acentos sobre oscuro) +- **NO amarillo eléctrico tipo Hormozi** (rompe cercanía) + +### Sistema de diseño extraído del pptx (BASE) + +El pptx `CREEER_Plantilla_Presentacion.pptx` es la **base visual oficial** de la marca. Estos datos se extrajeron parseando el archivo y deben respetarse: + +**Formato:** 16:9 widescreen (33.87 × 19.05 cm) + +**Paleta usada en el pptx (100% confirmada):** +- Dorado `#D4AF37` → **siempre BOLD, para títulos y énfasis** (Master Class, Agenda, sección, "Gracias", y la "e" del wordmark) +- Navy `#0A1F44` → **body text** (peso regular) +- Blanco implícito (fondos) + +**Tipografía del pptx:** +- Fuente: **Calibri** (sans-serif) — majorFont y minorFont del tema +- **Decisión RESUELTA (sistema híbrido):** + - **Wordmark** `cre`**`e`**`er.` → **Georgia 700 italic** (minúscula, "e" central en peso 900 + dorado, punto final dorado). El usuario prefiere la elegancia del wordmark serif sobre la fidelidad estricta al pptx sans-serif. El wordmark es la firma de marca. + - **Headlines** → **Inter 900** (Google Fonts) uppercase — punch Hormozi. + - **Body / eyebrow / CTA** → Inter 400/600. + - **Citas institucionales** → Georgia italic (anclan con el wordmark). + - Principio: **serif = voz de marca**, **sans = voz funcional**. + +**Escala tipográfica (extraída de los slides reales):** +| Uso | Tamaño | Estilo | Color | +|---|---|---|---| +| Caption/tagline | 14pt | regular | navy | +| Subtítulo | 24pt | regular | navy | +| Body / lista | 28pt | regular | navy | +| Cierre | 32pt | regular | dorado | +| Título de sección | 36pt | **BOLD** | dorado | +| Título grande / cita | 40pt | **BOLD** | navy o dorado | +| Hero | 48pt | **BOLD** | dorado | + +**Wordmark `cre`+`e`+`er` (confirmado a múltiples tamaños):** +- "Cre" + "e" (BOLD + `#D4AF37`) + "er" +- La **2ª "e"** (la del medio) va en dorado + negrita +- Aparece en 14pt (slide 1) y 28pt (slide 2) + +**Logo PNG:** `image1.png` — 1200×400 px, aspect **3:1 horizontal**. En slide 1 ocupa 27.38 × 9.13 cm (es el **protagonista visual**, no un detalle pequeño). + +**Patrones de alineación:** +- `center` (align=ctr) → hero / títulos de impacto +- `right` (align=r) → momentos asimétricos (slide 2) +- `left` (align=l) → contenido, citas, cierre + +**Recursos visuales recurrentes:** +- Separador de disciplinas: **" • "** (Comunicación • Digital • Experiencias) +- Listas numeradas: **"1. " "2. " "3. "** (sin bullets) +- Smart quotes **"" ""** para citas institucionales +- Mucho aire/whitespace alrededor de los textos + +**Anclas de copy del pptx (verbatim):** +- Tagline: *"creeer en ti, en mi, en el proyecto"* (slide 1 y 2) +- Cita institucional: *"El tiempo no se compra, se invierte"* (slide 6, 40pt bold navy) +- Cierre: *"Gracias por tu tiempo"* + *"CREEER - Comunicación • Digital • Experiencias"* (slide 7) + +### Wordmark +- SIEMPRE en minúsculas: `creeer` +- La "e" central (2ª de las 3) en dorado + bold +- Termina con punto dorado: `creeer.` + +### Copy +- Voz en **segunda persona ("tú")** — nunca "el empresario", siempre "tu negocio" +- Sin jerga corporativa +- "Simple, directo, limpio" aplica al copy también + +### Estructura +- One-page (single page) +- Sin backend, contacto por enlaces directos (WhatsApp, email, LinkedIn) +- Sin escasez en el hero (la dueña pidió quitar cupos/cohorte del hero) + +### Pendiente de decisión de la dueña +- **Hero final:** HV-1 (bold sans sin cajas) vs HV-2 (palabras en caja) — ver `docs/hero-finalists.md` + +--- + +## Cómo usar este archivo + +Cuando trabajes en creeer (copy, diseño, código, decisiones de producto): + +1. **Lee esto primero** — es la voz de la dueña. +2. **Pasa cada decisión por el filtro** de "seguridad + cercanía" + "simple, directo, limpio". +3. **Si una propuesta contradice algo de aquí**, detente y pregunta antes de avanzar. +4. **Si la dueña envía un mensaje nuevo**, agrégalo a la sección "Mensajes verbatim" con fecha y contexto. +5. **Las decisiones bloqueadas** (paleta, wordmark, voz) no se reabren sin su aprobación explícita. diff --git a/docs/hero-finalists.md b/docs/hero-finalists.md new file mode 100644 index 0000000..01ca285 --- /dev/null +++ b/docs/hero-finalists.md @@ -0,0 +1,118 @@ +# Hero finalists · creeer + +Dos candidatos finales para el hero de la landing. **Mismo copy, misma paleta, mismo wordmark, mismo layout centrado.** La diferencia es el **tono del headline**: + +- **HV-1** → punch Hormozi (headline en Inter 900 uppercase) +- **V-a** → pureza editorial (headline en Georgia italic, todo serif) + +La dueña puede elegir uno. Ambos respetan: wordmark `cre`**`e`**`er.` en Georgia italic, voz en "tú", paleta del pptx, "simple, directo, limpio", sin escasez en el hero. + +--- + +## Base común + +**Wordmark:** `cre`**`e`**`er.` en **Georgia 700 italic**, minúscula, con la **"e" central** (2ª de las 3) en peso 900 + dorado `#B8901f`, y punto final dorado. *(El wordmark es la firma de marca — siempre serif italic, sin importar el hero elegido.)* + +**Eyebrow:** `— tu estratega de crecimiento —` (small caps, dorado tostado) + +**Headline copy:** +> Tu negocio no necesita más esfuerzo. +> Necesita más claridad. + +**CTA:** `Define tu siguiente movimiento →` (navy fill, white text, chunky) + +**Paleta:** +- Blanco `#FFFFFF` (fondo) +- Navy `#0A1F44` (texto + CTA) +- Dorado tostado `#B8901f` (acentos) + +**Sin escasez:** NO se muestran cupos, cohorte ni "Generación Fundadora" en el hero (decisión de la dueña). Esos elementos, si aparecen, viven en sección posterior de la página. + +**Voz:** segunda persona ("tú"/"tu negocio"), directa, sin jerga corporativa. + +--- + +## HV-1 · Hormozi ligero (punch) + +**Tratamiento:** +- Headline en **Inter 900 uppercase** con tracking negativo (`letter-spacing: -1.5px`) +- "más **esfuerzo**" en color dorado tostado `#B8901f` +- "más **claridad**" con **subrayado dorado** grueso (5px solid) +- Wordmark sigue en Georgia italic (contraste serif/sans intencional) + +**Sensación:** punto medio exacto entre la limpieza del pptx y el punch Hormozi. El contraste tipográfico (wordmark serif + headline sans) da energía sin perder clase. + +**Trade-off:** +- ✅ Punch visual, más conversión +- ✅ Headline memorable, retención media +- ✅ Mantiene el wordmark serif (marca intacta) +- ⚠️ Más agresivo que V-a; algo menos de cercanía + +**Mejor si la dueña prioriza:** impacto, conversión, diferenciación. + +--- + +## V-a · Editorial minimal (cercanía) + +**Tratamiento:** +- **TODO en Georgia serif italic** — wordmark + eyebrow + headline + CTA +- "más **esfuerzo**" en color dorado tostado, italic +- "más **claridad**" subrayada en dorado +- Sin métricas, sin subhead, sin elementos accesorios. Solo 4 bloques: wordmark → eyebrow → headline → CTA + +**Sensación:** calma editorial pura. Máximo aire. "Simple, directo, limpio" llevado al extremo. La cercanía que pidió la dueña en su forma más pura. + +**Trade-off:** +- ✅ Máxima cercanía y limpieza +- ✅ Coherencia tipográfica total (todo serif) +- ✅ Elegancia atemporal +- ⚠️ Menos punch de conversión que HV-1 +- ⚠️ Podría sentirse "demasiado tranquilo" para vender + +**Mejor si la dueña prioriza:** "seguridad + cercanía", "simple, directo, limpio", prestigio editorial. + +--- + +## Spectrum visual + +| Hero | Cercanía | Punch Hormozi | Estilo | +|---|---|---|---| +| **V-a** (todo serif italic) | **Alta** | Bajo | Editorial puro — calma, elegancia | +| **HV-1** (serif + sans Hormozi) | Media | **Medio-alto** | Hybrid — punch + marca | + +--- + +## Cómo decidir (pregunta para la dueña) + +> ¿Prefieres que el hero **respire y se sienta cálido** (V-a), o que **tenga punch e impacte** (HV-1)? + +Ambos funcionan. La decisión es de tono, no de calidad. El wordmark, la paleta y el copy son idénticos — solo cambia la energía del headline. + +--- + +## Mockups en vivo + +Servidor visual companion (activo durante la sesión de brainstorming): +``` +http://localhost:55739/?key=7cb508aff417782d47d2fdf9c54062393af1d0d1e6e2d19090228dbf67df333b +``` + +Archivos fuente de los mockups: +- `.superpowers/brainstorm/2447-1784580629/content/three-georgia-heroes.html` (#2 V-a) +- `.superpowers/brainstorm/2447-1784580629/content/heroes-wordmark-restored.html` (HV-1 con wordmark Georgia) +- `.superpowers/brainstorm/2447-1784580629/content/va-large-locked.html` (V-a en grande) + +--- + +## Nota técnica (para implementación) + +Independientemente de cuál se elija, la implementación es la misma base Next.js + Tailwind + Framer Motion: +- Componente `` con el wordmark, eyebrow, headline y CTA +- Animación de entrada escalonada (Framer Motion): wordmark → eyebrow → headline → CTA +- Fuentes: **Georgia** (system serif, no requiere import) + **Inter** (Google Fonts, solo si se usa HV-1) + +**Diferencia entre HV-1 y V-a en código:** +- V-a: headline usa `font-family: Georgia, serif; font-style: italic` +- HV-1: headline usa `font-family: 'Inter', sans-serif; font-weight: 900; text-transform: uppercase` + +Cambiar entre ambos es **trivial** (un `className` condicional). Se puede dejar como prop del componente `` para que la dueña decida en QA. diff --git a/docs/superpowers/plans/2026-07-20-creeer-landing-implementation.md b/docs/superpowers/plans/2026-07-20-creeer-landing-implementation.md new file mode 100644 index 0000000..87045b0 --- /dev/null +++ b/docs/superpowers/plans/2026-07-20-creeer-landing-implementation.md @@ -0,0 +1,1435 @@ +# creeer Landing Page 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:** Build the one-page `creeer` landing site (7 sections) as a statically-exported Next.js 14 App Router site, matching the approved design spec exactly. + +**Architecture:** A single `app/page.tsx` assembles seven independent section components (`components/sections/*.tsx`), each wrapped in a shared `MotionSection` scroll-reveal wrapper that respects `prefers-reduced-motion`. A shared `Wordmark` component renders the brand mark everywhere it appears. No backend, no client-side state beyond motion — content is static JSX. + +**Tech Stack:** Next.js 14 (App Router) + TypeScript + Tailwind CSS 3 + Framer Motion 11, `output: export`. Tests: Vitest + React Testing Library + jsdom. + +## Global Constraints + +- Idioma: español, `lang="es"` en ``. +- Wordmark SIEMPRE minúsculas: `creeer` — nunca `CREEER` ni `Creeer`. +- Paleta exacta (no aproximar): `bg-base #FBFAF7`, `bg-pure #FFFFFF`, `bg-dark #0A1F44`, `text-primary #0A1F44`, `text-on-dark #F5EBD0`, `accent-gold #B8901f`, `accent-gold-bright #D4AF37`, `text-muted #5a5168`, `text-muted-dark #9aa6c2`. NO amarillo eléctrico tipo Hormozi. +- Sin backend, sin formulario — contacto solo por enlaces directos (WhatsApp/email/LinkedIn). +- `next.config.js` debe tener `output: 'export'` **y** `images.unoptimized: true` (la optimización de imágenes de Next no funciona en export estático). +- Container max-width: `1152px`. +- Voz: segunda persona ("tú"/"tu negocio"), sin jerga corporativa. +- Toda animación Framer Motion debe desactivarse con `useReducedMotion()`. +- Contraste WCAG 2.2 AA: `accent-gold` (#B8901f) solo para texto grande/decorativo, nunca body text sobre fondo claro. +- Un único `

` en toda la página (vive en el Hero). +- Datos de contacto reales (WhatsApp/email/LinkedIn), foto de la dueña y elección final HV-1 vs V-a están **pendientes de la dueña** (ver spec §10). Este plan usa placeholders explícitos marcados como tales — no bloquean el build. + +--- + +### Task 1: Next.js scaffold, Tailwind tokens, fonts, root layout + +**Files:** +- Create: `package.json` +- Create: `tsconfig.json` +- Create: `next.config.js` +- Create: `postcss.config.js` +- Create: `tailwind.config.ts` +- Create: `.eslintrc.json` +- Create: `vitest.config.ts` +- Create: `vitest.setup.ts` +- Create: `test/mocks/next-font-google.ts` +- Create: `app/layout.tsx` +- Create: `app/layout.test.tsx` +- Create: `app/globals.css` +- Create: `app/page.tsx` (placeholder, replaced in Task 11) +- Create: `components/MotionProvider.tsx` + +**Interfaces:** +- Produces: Tailwind color tokens (`bg-base`, `bg-pure`, `bg-dark`, `text-primary`, `text-on-dark`, `accent-gold`, `accent-gold-bright`, `text-muted`, `text-muted-dark`), `font-serif` (Georgia), `font-sans` (Inter via `--font-inter`), `max-w-container` (1152px). All later tasks consume these. +- Produces: working `npm test` (Vitest) and `npm run build` (static export) pipelines. +- Produces: `MotionProvider({ children: ReactNode })` at `components/MotionProvider.tsx`, wrapping the app in `` per spec §8.3 (bundle-size). Consumed by `app/layout.tsx` here, and required context for Tasks 3 and 4, which must use framer-motion's `m.*` components (not `motion.*`) so the `strict` flag catches accidental full-bundle imports. + +- [ ] **Step 1: Write `package.json`** + +```json +{ + "name": "creeer", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "framer-motion": "^11.3.19", + "next": "^14.2.5", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.4.8", + "@testing-library/react": "^16.0.0", + "@types/node": "^20.14.14", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", + "eslint-config-next": "^14.2.5", + "jsdom": "^24.1.1", + "postcss": "^8.4.40", + "tailwindcss": "^3.4.7", + "typescript": "^5.5.4", + "vitest": "^2.0.5" + } +} +``` + +- [ ] **Step 2: Write `tsconfig.json`** + +```json +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { "@/*": ["./*"] } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} +``` + +- [ ] **Step 3: Write `next.config.js`** + +```js +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'export', + images: { + unoptimized: true, + }, +}; + +module.exports = nextConfig; +``` + +- [ ] **Step 4: Write `postcss.config.js`** + +```js +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; +``` + +- [ ] **Step 5: Write `tailwind.config.ts`** + +```ts +import type { Config } from 'tailwindcss'; + +const config: Config = { + content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'], + theme: { + extend: { + colors: { + 'bg-base': '#FBFAF7', + 'bg-pure': '#FFFFFF', + 'bg-dark': '#0A1F44', + 'text-primary': '#0A1F44', + 'text-on-dark': '#F5EBD0', + 'accent-gold': '#B8901f', + 'accent-gold-bright': '#D4AF37', + 'text-muted': '#5a5168', + 'text-muted-dark': '#9aa6c2', + }, + fontFamily: { + serif: ['Georgia', 'serif'], + sans: ['var(--font-inter)', 'system-ui', 'sans-serif'], + }, + maxWidth: { + container: '1152px', + }, + }, + }, + plugins: [], +}; + +export default config; +``` + +- [ ] **Step 6: Write `.eslintrc.json`** + +```json +{ + "extends": "next/core-web-vitals" +} +``` + +- [ ] **Step 7: Write `test/mocks/next-font-google.ts`** + +Vitest doesn't run through Next's compiler, so `next/font/google` can't resolve its font manifest. Alias it to this mock so components importing it stay testable. + +```ts +export function Inter() { + return { + className: 'font-inter-mock', + variable: '--font-inter', + }; +} +``` + +- [ ] **Step 8: Write `vitest.config.ts`** + +```ts +import path from 'path'; +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [react()], + test: { + environment: 'jsdom', + setupFiles: ['./vitest.setup.ts'], + globals: true, + }, + resolve: { + alias: { + '@': path.resolve(__dirname, '.'), + 'next/font/google': path.resolve( + __dirname, + 'test/mocks/next-font-google.ts' + ), + }, + }, +}); +``` + +- [ ] **Step 9: Write `vitest.setup.ts`** + +```ts +import '@testing-library/jest-dom/vitest'; + +class MockIntersectionObserver { + observe() {} + unobserve() {} + disconnect() {} +} + +// @ts-expect-error jsdom doesn't implement IntersectionObserver +global.IntersectionObserver = MockIntersectionObserver; + +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: (query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: () => {}, + removeListener: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => false, + }), +}); +``` + +- [ ] **Step 10: Write `components/MotionProvider.tsx`** + +Wraps the app in Framer Motion's `LazyMotion` so every animated component can import the lightweight `m.*` primitives instead of the full `motion.*` bundle (spec §8.3: "usar `LazyMotion` + `domAnimation` para reducir bundle"). `strict` makes framer-motion throw if a `motion.*` component is ever used underneath by mistake. + +```tsx +'use client'; + +import type { ReactNode } from 'react'; +import { LazyMotion, domAnimation } from 'framer-motion'; + +export function MotionProvider({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} +``` + +- [ ] **Step 11: Write `app/globals.css`** + +```css +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + color-scheme: light; +} + +*:focus-visible { + outline: 3px solid #b8901f; + outline-offset: 2px; +} +``` + +- [ ] **Step 12: Write `app/layout.tsx`** + +```tsx +import type { Metadata } from 'next'; +import type { ReactNode } from 'react'; +import { Inter } from 'next/font/google'; +import { MotionProvider } from '@/components/MotionProvider'; +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 un punto de inflexión. Define tu siguiente movimiento.', +}; + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ); +} +``` + +- [ ] **Step 13: Write the failing test `app/layout.test.tsx`** + +```tsx +import { describe, expect, it } from 'vitest'; +import { render } from '@testing-library/react'; +import RootLayout, { metadata } from './layout'; + +describe('RootLayout', () => { + it('sets the approved SEO title and description', () => { + expect(metadata.title).toBe('creeer · tu estratega de crecimiento'); + expect(metadata.description).toContain('Claridad estratégica'); + }); + + it('declares Spanish as the document language', () => { + const { container } = render( + +

contenido

+
+ ); + expect(container.querySelector('html')).toHaveAttribute('lang', 'es'); + }); +}); +``` + +- [ ] **Step 14: Write the placeholder `app/page.tsx`** + +Temporary stub — replaced entirely in Task 11 once all sections exist. + +```tsx +export default function Home() { + return ( +
+

creeer

+
+ ); +} +``` + +- [ ] **Step 15: Install dependencies** + +Run: `npm install` +Expected: installs without errors, creates `node_modules/` and `package-lock.json`. + +- [ ] **Step 16: Run the test to verify it passes** + +Run: `npm test -- app/layout.test.tsx` +Expected: PASS, 2 tests passing. + +- [ ] **Step 17: Verify the static export build works** + +Run: `npm run build` +Expected: build succeeds, output includes a line for route `/`, and `out/index.html` exists afterward. + +- [ ] **Step 18: Commit** + +```bash +git add package.json package-lock.json tsconfig.json next.config.js postcss.config.js tailwind.config.ts .eslintrc.json vitest.config.ts vitest.setup.ts test/ app/ components/MotionProvider.tsx +git commit -m "chore: scaffold Next.js app with Tailwind tokens, fonts, and test runner" +``` + +--- + +### Task 2: Wordmark component + +**Files:** +- Create: `components/Wordmark.tsx` +- Create: `components/Wordmark.test.tsx` + +**Interfaces:** +- Consumes: Tailwind tokens from Task 1 (`text-primary`, `text-on-dark`, `accent-gold`, `font-serif`). +- Produces: `Wordmark({ size?: 'base' | 'lg', onDark?: boolean })` at `components/Wordmark.tsx`. Consumed by Task 4 (Hero). + +- [ ] **Step 1: Write the failing tests** + +```tsx +import { describe, expect, it } from 'vitest'; +import { render } from '@testing-library/react'; +import { Wordmark } from './Wordmark'; + +describe('Wordmark', () => { + it('renders the middle "e" in gold with heavier weight than the rest', () => { + const { container } = render(); + const goldE = container.querySelector('.font-black.text-accent-gold'); + expect(goldE).not.toBeNull(); + expect(goldE?.textContent).toBe('e'); + }); + + it('renders the trailing period in gold', () => { + const { container } = render(); + const spans = container.querySelectorAll('span.text-accent-gold'); + const period = Array.from(spans).find((el) => el.textContent === '.'); + expect(period).not.toBeUndefined(); + }); + + it('never renders the wordmark in uppercase', () => { + const { container } = render(); + expect(container.textContent?.toLowerCase()).toBe(container.textContent); + }); + + it('switches to the on-dark ink color when onDark is true', () => { + const { container } = render(); + expect(container.firstElementChild?.className).toContain( + 'text-text-on-dark' + ); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `npm test -- components/Wordmark.test.tsx` +Expected: FAIL with "Cannot find module './Wordmark'". + +- [ ] **Step 3: Write `components/Wordmark.tsx`** + +```tsx +export function Wordmark({ + size = 'base', + onDark = false, +}: { + size?: 'base' | 'lg'; + onDark?: boolean; +}) { + const inkColor = onDark ? 'text-text-on-dark' : 'text-text-primary'; + const sizeClass = size === 'lg' ? 'text-4xl md:text-5xl' : 'text-2xl'; + + return ( + + cre + e + er + . + + ); +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `npm test -- components/Wordmark.test.tsx` +Expected: PASS, 4 tests passing. + +- [ ] **Step 5: Commit** + +```bash +git add components/Wordmark.tsx components/Wordmark.test.tsx +git commit -m "feat: add Wordmark component with gold middle-e brand rule" +``` + +--- + +### Task 3: MotionSection scroll-reveal wrapper + +**Files:** +- Create: `components/MotionSection.tsx` +- Create: `components/MotionSection.test.tsx` + +**Interfaces:** +- Consumes: `framer-motion`'s `m` and `useReducedMotion` (the `m.*` lightweight primitives, not `motion.*` — see `MotionProvider` in Task 1, which enforces this via `strict`). +- Produces: `MotionSection({ children: ReactNode, className?: string })` at `components/MotionSection.tsx`, rendering a `
`. Consumed by Tasks 5–10 (all sections except Hero, which has its own staggered entrance). + +- [ ] **Step 1: Write the failing tests** + +```tsx +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { cleanup, render } from '@testing-library/react'; +import { LazyMotion, domAnimation } from 'framer-motion'; +import { MotionSection } from './MotionSection'; + +function mockReducedMotion(matches: boolean) { + vi.spyOn(window, 'matchMedia').mockImplementation((query: string) => ({ + matches, + media: query, + onchange: null, + addListener: () => {}, + removeListener: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => false, + })); +} + +function renderWithMotion(ui: React.ReactElement) { + return render( + {ui} + ); +} + +afterEach(() => { + cleanup(); + vi.restoreAllMocks(); +}); + +describe('MotionSection', () => { + it('renders a plain section with no inline animation styles when reduced motion is preferred', () => { + mockReducedMotion(true); + const { container } = renderWithMotion( + +

contenido

+
+ ); + const section = container.querySelector('section'); + expect(section).not.toBeNull(); + expect(section?.getAttribute('style')).toBeNull(); + }); + + it('applies an initial opacity of 0 before entering the viewport when motion is allowed', () => { + mockReducedMotion(false); + const { container } = renderWithMotion( + +

contenido

+
+ ); + expect(container.querySelector('section')?.getAttribute('style')).toContain( + 'opacity: 0' + ); + }); + + it('always renders its children regardless of motion preference', () => { + mockReducedMotion(true); + const { getByText } = renderWithMotion( + +

contenido visible

+
+ ); + expect(getByText('contenido visible')).toBeInTheDocument(); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `npm test -- components/MotionSection.test.tsx` +Expected: FAIL with "Cannot find module './MotionSection'". + +- [ ] **Step 3: Write `components/MotionSection.tsx`** + +```tsx +'use client'; + +import type { ReactNode } from 'react'; +import { m, useReducedMotion } from 'framer-motion'; + +const EASE_OUT_EXPO: [number, number, number, number] = [0.22, 1, 0.36, 1]; + +export function MotionSection({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) { + const shouldReduceMotion = useReducedMotion(); + + if (shouldReduceMotion) { + return
{children}
; + } + + return ( + + {children} + + ); +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `npm test -- components/MotionSection.test.tsx` +Expected: PASS, 3 tests passing. + +Note: this component renders `m.section` without a `LazyMotion` ancestor in production only through `app/layout.tsx`'s `MotionProvider` (Task 1) — outside of a real render tree, `m` components need that provider to have their animation features loaded, which is why the test above wraps every render in its own ``. + +- [ ] **Step 5: Commit** + +```bash +git add components/MotionSection.tsx components/MotionSection.test.tsx +git commit -m "feat: add MotionSection scroll-reveal wrapper with reduced-motion support" +``` + +--- + +### Task 4: Hero section (01) + +**Files:** +- Create: `components/sections/Hero.tsx` +- Create: `components/sections/Hero.test.tsx` + +**Interfaces:** +- Consumes: `Wordmark` (Task 2). +- Produces: `Hero({ variant?: 'hormozi' | 'editorial' })` at `components/sections/Hero.tsx`, default `'editorial'`. Renders the page's single `

` and the CTA linking to `#contacto`. Consumed by Task 11. + +**Note on the default:** the spec leaves HV-1 vs V-a as the dueña's decision (§4, §10). `variant` defaults to `'editorial'` (V-a) because it's the closer match to the master tone "seguridad + cercanía" — swap the default or the call site in Task 11 the moment she decides. + +- [ ] **Step 1: Write the failing tests** + +```tsx +import { describe, expect, it } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { Hero } from './Hero'; + +describe('Hero', () => { + it('renders the editorial variant in Georgia italic by default', () => { + render(); + const headline = screen.getByRole('heading', { level: 1 }); + expect(headline.className).toContain('font-serif'); + expect(headline.className).toContain('italic'); + }); + + it('renders the hormozi variant in uppercase sans when requested', () => { + render(); + const headline = screen.getByRole('heading', { level: 1 }); + expect(headline.className).toContain('uppercase'); + expect(headline.className).not.toContain('font-serif'); + }); + + it('renders exactly one h1 with the approved headline copy', () => { + render(); + const headings = screen.getAllByRole('heading', { level: 1 }); + expect(headings).toHaveLength(1); + expect(headings[0].textContent).toContain('más esfuerzo'); + expect(headings[0].textContent).toContain('más claridad'); + }); + + it('renders the CTA linking to the contact section', () => { + render(); + const cta = screen.getByRole('link', { + name: /Define tu siguiente movimiento/, + }); + expect(cta).toHaveAttribute('href', '#contacto'); + }); + + it('never shows scarcity language in the hero', () => { + render(); + const text = screen.getByRole('heading', { level: 1 }).textContent ?? ''; + expect(text).not.toMatch(/cupo|cohorte|generaci[oó]n fundadora/i); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `npm test -- components/sections/Hero.test.tsx` +Expected: FAIL with "Cannot find module './Hero'". + +- [ ] **Step 3: Write `components/sections/Hero.tsx`** + +```tsx +'use client'; + +import { m, useReducedMotion } from 'framer-motion'; +import { Wordmark } from '@/components/Wordmark'; + +const EASE_OUT_EXPO: [number, number, number, number] = [0.22, 1, 0.36, 1]; +const STAGGER_DELAYS = [0, 0.1, 0.2, 0.3]; + +type HeroVariant = 'hormozi' | 'editorial'; + +function HeroHeadline({ variant }: { variant: HeroVariant }) { + if (variant === 'hormozi') { + return ( +

+ Tu negocio no necesita más esfuerzo. +
+ Necesita{' '} + + más claridad + + . +

+ ); + } + + return ( +

+ Tu negocio no necesita más esfuerzo. +
+ Necesita{' '} + + más claridad + + . +

+ ); +} + +export function Hero({ variant = 'editorial' }: { variant?: HeroVariant }) { + const shouldReduceMotion = useReducedMotion(); + + const item = (delay: number) => + shouldReduceMotion + ? {} + : { + initial: { opacity: 0, y: 16 }, + animate: { opacity: 1, y: 0 }, + transition: { duration: 0.5, delay, ease: EASE_OUT_EXPO }, + }; + + return ( +
+ + + + + — tu estratega de crecimiento — + + + + + + Define tu siguiente movimiento → + +
+ ); +} +``` + +Note: Hero's tests only assert `className`/text/`href`, none of which depend on the animation feature bundle being loaded, so `Hero.test.tsx` renders the component directly with no `LazyMotion` wrapper needed (unlike `MotionSection.test.tsx`). + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `npm test -- components/sections/Hero.test.tsx` +Expected: PASS, 5 tests passing. + +- [ ] **Step 5: Commit** + +```bash +git add components/sections/Hero.tsx components/sections/Hero.test.tsx +git commit -m "feat: add Hero section with hormozi/editorial variant switch" +``` + +--- + +### Task 5: AnchorQuote section (02) + +**Files:** +- Create: `components/sections/AnchorQuote.tsx` +- Create: `components/sections/AnchorQuote.test.tsx` + +**Interfaces:** +- Consumes: `MotionSection` (Task 3). +- Produces: `AnchorQuote()` at `components/sections/AnchorQuote.tsx`. Consumed by Task 11. + +- [ ] **Step 1: Write the failing tests** + +```tsx +import { describe, expect, it } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { AnchorQuote } from './AnchorQuote'; + +describe('AnchorQuote', () => { + it('renders the approved anchor quote verbatim', () => { + render(); + expect( + screen.getByText(/no dejan de crecer por falta de claridad/) + ).toBeInTheDocument(); + }); + + it('does not introduce a second h1 on the page', () => { + render(); + expect(screen.queryAllByRole('heading', { level: 1 })).toHaveLength(0); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `npm test -- components/sections/AnchorQuote.test.tsx` +Expected: FAIL with "Cannot find module './AnchorQuote'". + +- [ ] **Step 3: Write `components/sections/AnchorQuote.tsx`** + +```tsx +import { MotionSection } from '@/components/MotionSection'; + +export function AnchorQuote() { + return ( + +
+ “Las empresas no dejan de crecer por falta de talento; dejan de + crecer por falta de claridad.” +
+
+ ); +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `npm test -- components/sections/AnchorQuote.test.tsx` +Expected: PASS, 2 tests passing. + +- [ ] **Step 5: Commit** + +```bash +git add components/sections/AnchorQuote.tsx components/sections/AnchorQuote.test.tsx +git commit -m "feat: add AnchorQuote section" +``` + +--- + +### Task 6: ForWhom section (03) + +**Files:** +- Create: `components/sections/ForWhom.tsx` +- Create: `components/sections/ForWhom.test.tsx` + +**Interfaces:** +- Consumes: `MotionSection` (Task 3). +- Produces: `ForWhom()` at `components/sections/ForWhom.tsx`. Consumed by Task 11. + +- [ ] **Step 1: Write the failing tests** + +```tsx +import { describe, expect, it } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { ForWhom } from './ForWhom'; + +describe('ForWhom', () => { + it('renders exactly the three approved qualifying chips in order', () => { + render(); + const items = screen.getAllByRole('listitem'); + expect(items.map((item) => item.textContent)).toEqual([ + '✓ tienes tracción', + '✓ más esfuerzo no rinde', + '✓ necesitas claridad', + ]); + }); + + it('uses an h2, never a second h1', () => { + render(); + expect(screen.getByRole('heading', { level: 2 })).toBeInTheDocument(); + expect(screen.queryAllByRole('heading', { level: 1 })).toHaveLength(0); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `npm test -- components/sections/ForWhom.test.tsx` +Expected: FAIL with "Cannot find module './ForWhom'". + +- [ ] **Step 3: Write `components/sections/ForWhom.tsx`** + +```tsx +import { MotionSection } from '@/components/MotionSection'; + +const CHIPS = ['tienes tracción', 'más esfuerzo no rinde', 'necesitas claridad']; + +export function ForWhom() { + return ( + +
+

+ ¿Llegaste a tu punto de inflexión? +

+

+ Esto es para ti si... +

+
    + {CHIPS.map((chip) => ( +
  • + ✓ {chip} +
  • + ))} +
+
+
+ ); +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `npm test -- components/sections/ForWhom.test.tsx` +Expected: PASS, 2 tests passing. + +- [ ] **Step 5: Commit** + +```bash +git add components/sections/ForWhom.tsx components/sections/ForWhom.test.tsx +git commit -m "feat: add ForWhom qualifying section" +``` + +--- + +### Task 7: HowIWork section (04, dark) + +**Files:** +- Create: `components/sections/HowIWork.tsx` +- Create: `components/sections/HowIWork.test.tsx` + +**Interfaces:** +- Consumes: `MotionSection` (Task 3). +- Produces: `HowIWork()` at `components/sections/HowIWork.tsx`. Consumed by Task 11. + +- [ ] **Step 1: Write the failing tests** + +```tsx +import { describe, expect, it } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { HowIWork } from './HowIWork'; + +describe('HowIWork', () => { + it('renders the three mechanism steps in order, numbered, with no icons', () => { + render(); + const items = screen.getAllByRole('listitem'); + expect(items.map((item) => item.textContent)).toEqual([ + '01Inmersión', + '02Desafiar supuestos', + '03Estrategia ejecutable', + ]); + expect(screen.queryAllByRole('img')).toHaveLength(0); + }); + + it('uses the dark section background', () => { + const { container } = render(); + expect(container.querySelector('section')?.className).toContain( + 'bg-bg-dark' + ); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `npm test -- components/sections/HowIWork.test.tsx` +Expected: FAIL with "Cannot find module './HowIWork'". + +- [ ] **Step 3: Write `components/sections/HowIWork.tsx`** + +```tsx +import { MotionSection } from '@/components/MotionSection'; + +const STEPS = [ + { number: '01', title: 'Inmersión' }, + { number: '02', title: 'Desafiar supuestos' }, + { number: '03', title: 'Estrategia ejecutable' }, +]; + +export function HowIWork() { + return ( + +
+

+ Cómo trabajo +

+
    + {STEPS.map((step) => ( +
  1. + + {step.number} + +

    + {step.title} +

    +
  2. + ))} +
+
+
+ ); +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `npm test -- components/sections/HowIWork.test.tsx` +Expected: PASS, 2 tests passing. + +- [ ] **Step 5: Commit** + +```bash +git add components/sections/HowIWork.tsx components/sections/HowIWork.test.tsx +git commit -m "feat: add HowIWork dark mechanism section" +``` + +--- + +### Task 8: Disciplines section (05) + +**Files:** +- Create: `components/sections/Disciplines.tsx` +- Create: `components/sections/Disciplines.test.tsx` + +**Interfaces:** +- Consumes: `MotionSection` (Task 3). +- Produces: `Disciplines()` at `components/sections/Disciplines.tsx`. Consumed by Task 11. + +- [ ] **Step 1: Write the failing tests** + +```tsx +import { describe, expect, it } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { Disciplines } from './Disciplines'; + +describe('Disciplines', () => { + it('renders the three disciplines as tools, not as the core promise', () => { + render(); + expect(screen.getByText('Comunicación')).toBeInTheDocument(); + expect(screen.getByText('Digital')).toBeInTheDocument(); + expect(screen.getByText('Experiencias')).toBeInTheDocument(); + }); + + it('gives each card a gold top border', () => { + const { container } = render(); + expect(container.querySelectorAll('.border-t-4')).toHaveLength(3); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `npm test -- components/sections/Disciplines.test.tsx` +Expected: FAIL with "Cannot find module './Disciplines'". + +- [ ] **Step 3: Write `components/sections/Disciplines.tsx`** + +```tsx +import { MotionSection } from '@/components/MotionSection'; + +const DISCIPLINES = ['Comunicación', 'Digital', 'Experiencias']; + +export function Disciplines() { + return ( + +
+

+ Las herramientas +

+
+ {DISCIPLINES.map((discipline) => ( +
+

+ {discipline} +

+
+ ))} +
+
+
+ ); +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `npm test -- components/sections/Disciplines.test.tsx` +Expected: PASS, 2 tests passing. + +- [ ] **Step 5: Commit** + +```bash +git add components/sections/Disciplines.tsx components/sections/Disciplines.test.tsx +git commit -m "feat: add Disciplines section" +``` + +--- + +### Task 9: About section (06) + +**Files:** +- Create: `components/sections/About.tsx` +- Create: `components/sections/About.test.tsx` + +**Interfaces:** +- Consumes: `MotionSection` (Task 3). +- Produces: `About()` at `components/sections/About.tsx`. Consumed by Task 11. + +**Note:** no professional photo exists yet (spec §5, §10 — pending). Renders a navy/gold gradient placeholder labeled for screen readers; swap the `div` for a Next `...` the moment a photo is provided (`images.unoptimized: true` from Task 1 already makes this drop-in safe under static export). + +- [ ] **Step 1: Write the failing tests** + +```tsx +import { describe, expect, it } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { About } from './About'; + +describe('About', () => { + it('labels the portrait placeholder for screen readers until a real photo is provided', () => { + render(); + expect( + screen.getByRole('img', { name: /Retrato de la estratega/ }) + ).toBeInTheDocument(); + }); + + it('renders the first-person bio and the institutional quote', () => { + render(); + expect(screen.getByText(/Hoy ayudo a empresarios/)).toBeInTheDocument(); + expect( + screen.getByText(/El tiempo no se compra, se invierte/) + ).toBeInTheDocument(); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `npm test -- components/sections/About.test.tsx` +Expected: FAIL with "Cannot find module './About'". + +- [ ] **Step 3: Write `components/sections/About.tsx`** + +```tsx +import { MotionSection } from '@/components/MotionSection'; + +export function About() { + return ( + +
+
+
+

+ La mente detrás de creeer +

+

+ +30 años construyendo marcas. Hoy ayudo a empresarios con + tracción a encontrar la claridad estratégica que los lleva a su + siguiente movimiento. +

+

+ “El tiempo no se compra, se invierte.” +

+
+
+ + ); +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `npm test -- components/sections/About.test.tsx` +Expected: PASS, 2 tests passing. + +- [ ] **Step 5: Commit** + +```bash +git add components/sections/About.tsx components/sections/About.test.tsx +git commit -m "feat: add About section with portrait placeholder" +``` + +--- + +### Task 10: Closing section (07, dark) + contact links + +**Files:** +- Create: `components/sections/Closing.tsx` +- Create: `components/sections/Closing.test.tsx` + +**Interfaces:** +- Consumes: `MotionSection` (Task 3). +- Produces: `Closing()` at `components/sections/Closing.tsx`, rendering the `id="contacto"` anchor target that Hero's CTA (Task 4) scrolls to. Consumed by Task 11. + +**Note:** WhatsApp number, email, and LinkedIn URL are placeholders pending the dueña's real contact details (spec §10). Replace the three values in `CONTACT_LINKS` the moment she provides them — no other code changes needed. + +- [ ] **Step 1: Write the failing tests** + +```tsx +import { describe, expect, it } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { Closing } from './Closing'; + +describe('Closing', () => { + it('exposes the three contact channels with accessible labels', () => { + render(); + expect( + screen.getByRole('link', { name: 'Escribir por WhatsApp' }) + ).toHaveAttribute('href', 'https://wa.me/521234567890'); + expect( + screen.getByRole('link', { name: 'Escribir por correo electrónico' }) + ).toHaveAttribute('href', 'mailto:hola@creeer.com'); + expect( + screen.getByRole('link', { name: 'Ver perfil de LinkedIn' }) + ).toHaveAttribute('href', 'https://www.linkedin.com/in/creeer'); + }); + + it('points the closing CTA at the primary contact channel', () => { + render(); + const cta = screen.getByRole('link', { + name: 'Define tu siguiente movimiento →', + }); + expect(cta).toHaveAttribute('href', 'https://wa.me/521234567890'); + }); + + it('provides the #contacto anchor target for the hero CTA to scroll to', () => { + const { container } = render(); + expect(container.querySelector('#contacto')).not.toBeNull(); + }); + + it('uses the dark section background', () => { + const { container } = render(); + expect(container.querySelector('section')?.className).toContain( + 'bg-bg-dark' + ); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `npm test -- components/sections/Closing.test.tsx` +Expected: FAIL with "Cannot find module './Closing'". + +- [ ] **Step 3: Write `components/sections/Closing.tsx`** + +```tsx +import { MotionSection } from '@/components/MotionSection'; + +const CONTACT_LINKS = [ + { + label: 'WhatsApp', + href: 'https://wa.me/521234567890', + ariaLabel: 'Escribir por WhatsApp', + }, + { + label: 'Email', + href: 'mailto:hola@creeer.com', + ariaLabel: 'Escribir por correo electrónico', + }, + { + label: 'LinkedIn', + href: 'https://www.linkedin.com/in/creeer', + ariaLabel: 'Ver perfil de LinkedIn', + }, +]; + +export function Closing() { + return ( + +
+

+ “Devuélvele claridad al rumbo de tu empresa.” +

+ + Define tu siguiente movimiento → + + +
+
+ ); +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `npm test -- components/sections/Closing.test.tsx` +Expected: PASS, 4 tests passing. + +- [ ] **Step 5: Commit** + +```bash +git add components/sections/Closing.tsx components/sections/Closing.test.tsx +git commit -m "feat: add Closing section with contact links" +``` + +--- + +### Task 11: Assemble the page, structured data, and verify the build + +**Files:** +- Modify: `app/page.tsx` (replace the Task 1 placeholder) +- Create: `app/page.test.tsx` + +**Interfaces:** +- Consumes: `Hero` (Task 4), `AnchorQuote` (Task 5), `ForWhom` (Task 6), `HowIWork` (Task 7), `Disciplines` (Task 8), `About` (Task 9), `Closing` (Task 10). + +- [ ] **Step 1: Write the failing tests** + +```tsx +import { describe, expect, it } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import Home from './page'; + +describe('Home page', () => { + it('renders exactly one h1 across the whole page', () => { + render(); + expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1); + }); + + it('renders the seven sections in spec order', () => { + const { container } = render(); + expect(container.querySelectorAll('section')).toHaveLength(7); + }); + + it('repeats the primary CTA copy at the top and the bottom of the page', () => { + render(); + expect( + screen.getAllByText('Define tu siguiente movimiento →') + ).toHaveLength(2); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `npm test -- app/page.test.tsx` +Expected: FAIL — the placeholder page only renders one `

` and zero `
` elements, so the section-count and CTA-repetition assertions fail. + +- [ ] **Step 3: Replace `app/page.tsx`** + +```tsx +import { Hero } from '@/components/sections/Hero'; +import { AnchorQuote } from '@/components/sections/AnchorQuote'; +import { ForWhom } from '@/components/sections/ForWhom'; +import { HowIWork } from '@/components/sections/HowIWork'; +import { Disciplines } from '@/components/sections/Disciplines'; +import { About } from '@/components/sections/About'; +import { Closing } from '@/components/sections/Closing'; + +const STRUCTURED_DATA = { + '@context': 'https://schema.org', + '@type': 'ProfessionalService', + name: 'creeer', + description: + 'Claridad estratégica para empresarios con tracción que llegaron a un punto de inflexión.', + url: 'https://creeer.com', +}; + +export default function Home() { + return ( +
+ + + + + + + +