docs(recorrido): spec y plan del recorrido de diálogo

Sale de la llamada del 31/jul: "te va contestando y te va como llevando
de uno a otro". Los seis puntos de inflexión dejan de ser tarjetas mudas
y pasan a ser una conversación.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
erikghandClaude Opus 5 committed 2026-08-03 11:11:36 -06:00
1 parent 2383e63bc5
commit 170482a32b
2 files changed
+577

No files matched your search

@@ -0,0 +1,467 @@
# El recorrido de diálogo — 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:** Convertir `PuntosInflexion` de una rejilla de seis tarjetas mudas en una conversación: cada punto de inflexión enuncia la situación del lector y creeer contesta qué hace al respecto.
**Architecture:** Nace un primitivo `ParDialogo` que encapsula un intercambio (situación + respuesta + su separación visual) y no sabe nada de la sección que lo monta. `lib/content.ts` cambia la forma de `puntosInflexion.items` de `{ name, desc }` a `{ name, situacion, respuesta }`. `PuntosInflexion` conserva su cabecera y sustituye el cuerpo por una `<ul>` de seis `ParDialogo`. El carrusel móvil desaparece y con él el primitivo `Carrusel`, que se queda sin consumidores.
**Tech Stack:** Next.js 14 App Router (`output: 'export'`), React 18, TypeScript, Tailwind, Framer Motion 11, Vitest + jsdom + Testing Library.
**Spec:** `docs/superpowers/specs/2026-08-03-recorrido-dialogo-design.md`
## Global Constraints
- **Gestor de paquetes: `pnpm`** (`pnpm@11.7.0` vía corepack). No usar npm.
- **Rama nueva por cada cambio.** Este trabajo va en `feat/recorrido-dialogo`. Nunca trabajar directo en `main`.
- **No hacer commits sin aprobación explícita del usuario.** Los pasos de commit de este plan están escritos, pero **hay que pedir el visto bueno antes de ejecutarlos**.
- **El copy vive en `lib/content.ts`, no en el JSX.** Las comillas angulares que envuelven la situación son puntuación del componente, no copy.
- **El wordmark nunca se escribe a mano en el JSX.** Solo `<Wordmark />` dibuja `cre≡er`.
- **Proyecto en español** — copy, docs y commits. **Los comentarios de código, en inglés.**
- **Toda sección animada** combina `usePrefersReducedMotion()` con `motionProps(reduced)`. Es requisito de accesibilidad del spec.
- **Tokens de color de `tailwind.config.ts`** (`azul-profundo`, `dorado`, `gris-claro`, `gris-medio`), nunca hex sueltos.
- **Sin escasez:** ningún texto puede contener `cupos`, `cohorte` ni `generación fundadora`.
- **Ningún conector puede depender de `pathLength`** sobre un SVG con `preserveAspectRatio="none"`. Chromium recorre el guion en espacio de pantalla y normaliza la unidad desde el espacio de usuario, así que el trazo llega partido. Usar bordes o formas rellenas.
## File Structure
| Archivo | Responsabilidad |
|---------|-----------------|
| `components/primitives/ParDialogo.tsx` | **Nuevo.** Un intercambio: la situación del lector y la respuesta de creeer, con las dos voces distinguibles sin depender del color |
| `components/primitives/__tests__/ParDialogo.test.tsx` | **Nuevo.** Contrato del primitivo |
| `lib/content.ts` | `puntosInflexion.items` pasa a `{ name, situacion, respuesta }`; se añade `lead` |
| `components/sections/PuntosInflexion.tsx` | Cabecera intacta; el cuerpo pasa de rejilla + carrusel a `<ul>` de `ParDialogo` |
| `components/sections/__tests__/PuntosInflexion.test.tsx` | Se actualiza: ya no hay tarjetas ni duplicado por carrusel |
| `components/primitives/Carrusel.tsx` | **Se retira.** Sin consumidores tras el cambio |
| `components/primitives/__tests__/Carrusel.test.tsx` | **Se retira** con él |
---
### Task 0: Abrir la rama
**Files:** ninguno.
- [ ] **Step 1: Crear la rama de trabajo**
El repositorio arrastra la retirada del hilo conductor sin commitear en `feat/hilo-conductor-visual`. Antes de empezar, confirmar con el usuario qué hacer con esos cambios (commitearlos allí o arrastrarlos). Después:
```bash
git checkout -b feat/recorrido-dialogo
git status --short
```
Expected: la rama nueva existe y `git status` muestra solo lo que el usuario haya decidido arrastrar.
---
### Task 1: `ParDialogo`, el intercambio
**Files:**
- Create: `components/primitives/ParDialogo.tsx`
- Test: `components/primitives/__tests__/ParDialogo.test.tsx`
**Interfaces:**
- Consumes: `Wordmark` de `@/components/primitives/Wordmark` (props `size?: 'sm'|'md'|'lg'|'xl'`, `onDark?: boolean`, `showTagline?: boolean`); `fadeInUp` de `@/lib/motion`.
- Produces: `ParDialogo({ n, name, situacion, respuesta }: { n: number; name: string; situacion: string; respuesta: string })`. Renderiza un `<li>`, así que **el que lo monta debe darle una `<ul>` o `<ol>` como padre**. La Task 2 depende de esa firma exacta.
- [ ] **Step 1: Escribir el test que falla**
Crear `components/primitives/__tests__/ParDialogo.test.tsx`:
```tsx
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { ParDialogo } from '../ParDialogo';
const par = {
n: 1,
name: 'Crecer',
situacion: 'El siguiente salto no tiene mapa.',
respuesta: 'Me sumerjo hasta entender tu empresa desde dentro.',
};
describe('<ParDialogo />', () => {
it('renders both voices of the exchange', () => {
render(<ul><ParDialogo {...par} /></ul>);
expect(screen.getByText(/El siguiente salto no tiene mapa/)).toBeInTheDocument();
expect(screen.getByText(/Me sumerjo hasta entender tu empresa desde dentro/)).toBeInTheDocument();
});
it('attributes the answer to creeer, not to the reader', () => {
render(<ul><ParDialogo {...par} /></ul>);
// Without a speaker label the answer reads as more description of the
// problem. The wordmark is that label — and <Wordmark /> is the only
// thing allowed to draw cre≡er, never hand-typed in the JSX.
expect(screen.getByLabelText('creeer')).toBeInTheDocument();
});
it('labels the situation with its inflection point', () => {
render(<ul><ParDialogo {...par} /></ul>);
expect(screen.getByText(/01 · Crecer/)).toBeInTheDocument();
});
it('tells the two voices apart by more than colour', () => {
const { container } = render(<ul><ParDialogo {...par} /></ul>);
// A reader who cannot separate gold from navy still has the speaker
// label, the column, and the rule hanging off each block. Colour alone
// would fail the spec's accessibility criterion.
const bloques = container.querySelectorAll('li > div');
expect(bloques).toHaveLength(2);
bloques.forEach((b) => expect(b.className).toMatch(/border-l-2/));
});
it('is a list item, so the section can mount six of them as a list', () => {
render(<ul><ParDialogo {...par} /></ul>);
expect(screen.getAllByRole('listitem')).toHaveLength(1);
});
});
```
- [ ] **Step 2: Correr el test y verificar que falla**
Run: `pnpm test components/primitives/__tests__/ParDialogo.test.tsx`
Expected: FAIL — `Failed to resolve import "../ParDialogo"`.
- [ ] **Step 3: Escribir el componente**
Crear `components/primitives/ParDialogo.tsx`:
```tsx
'use client';
import { motion } from 'framer-motion';
import { Wordmark } from '@/components/primitives/Wordmark';
import { fadeInUp } from '@/lib/motion';
// One exchange of the page's conversation: the reader's situation, then what
// creeer does about it.
//
// The sides never swap from one pair to the next. Each voice keeps its own
// margin, the way a conversation reads on a page; what alternates going down
// is situación → respuesta, not left → right. Flipping sides per pair would
// cost the reader the one cue that tells them who is speaking.
//
// Three cues carry that distinction, never colour alone: the speaker label,
// the column, and the rule hanging off each block. The answer also drops half
// a step below the situation on wide screens, so the eye reads it as a reply
// rather than as a second column of the same thought.
//
// No motion.li here on purpose: Framer propagates variants through React
// context, so the two motion.div children still inherit the stagger from the
// <ul> the section wraps them in, even across this plain <li>.
export function ParDialogo({
n,
name,
situacion,
respuesta,
}: {
n: number;
name: string;
situacion: string;
respuesta: string;
}) {
return (
<li className="grid gap-5 md:grid-cols-2 md:gap-12">
<motion.div variants={fadeInUp} className="border-l-2 border-azul-profundo/25 pl-5">
<p className="text-xs font-black uppercase tracking-[0.2em] text-dorado">
{String(n).padStart(2, '0')} · {name}
</p>
<p className="mt-3 font-sans font-bold text-xl md:text-2xl text-azul-profundo leading-snug">
«{situacion}»
</p>
</motion.div>
<motion.div variants={fadeInUp} className="border-l-2 border-dorado pl-5 md:mt-10">
<Wordmark size="sm" />
<p className="mt-3 text-gris-medio leading-relaxed">{respuesta}</p>
</motion.div>
</li>
);
}
```
- [ ] **Step 4: Correr el test y verificar que pasa**
Run: `pnpm test components/primitives/__tests__/ParDialogo.test.tsx`
Expected: PASS (5 tests).
- [ ] **Step 5: Typecheck**
Run: `npx tsc --noEmit`
Expected: sin salida.
- [ ] **Step 6: Commit** *(pedir aprobación primero — ver Global Constraints)*
```bash
git add components/primitives/ParDialogo.tsx components/primitives/__tests__/ParDialogo.test.tsx
git commit -m "feat(recorrido): ParDialogo — la situación y la respuesta como un intercambio"
```
---
### Task 2: El copy y la sección
**Files:**
- Modify: `lib/content.ts:35-48` (el bloque `puntosInflexion`)
- Modify: `components/sections/PuntosInflexion.tsx`
- Test: `components/sections/__tests__/PuntosInflexion.test.tsx`
**Interfaces:**
- Consumes: `ParDialogo({ n, name, situacion, respuesta })` de la Task 1.
- Produces: `puntosInflexion` con forma `{ eyebrow: string; title: string; lead: string; items: { name: string; situacion: string; respuesta: string }[] }`. **`desc` desaparece**: ningún otro archivo lo lee (verificado — `PuntosInflexion.tsx` era su único consumidor).
- [ ] **Step 1: Escribir los tests que fallan**
Reemplazar el contenido de `components/sections/__tests__/PuntosInflexion.test.tsx`:
```tsx
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { PuntosInflexion } from '../PuntosInflexion';
import { puntosInflexion } from '@/lib/content';
describe('<PuntosInflexion />', () => {
it('renders the section title', () => {
render(<PuntosInflexion />);
expect(screen.getByText(/llegan a un punto de inflexión/i)).toBeInTheDocument();
});
it('renders the six inflection points from the dueña deck', () => {
render(<PuntosInflexion />);
// Each point now renders once, not twice: the mobile carousel is gone and
// the conversation stacks vertically at every breakpoint.
for (const name of ['Crecer', 'Consolidarse', 'Recuperar rentabilidad', 'Simplificar', 'Expandirse', 'Replantear el portafolio']) {
expect(screen.getByText(new RegExp(name))).toBeInTheDocument();
}
expect(screen.getAllByRole('listitem')).toHaveLength(6);
});
it('answers every situation instead of only naming it', () => {
render(<PuntosInflexion />);
// This is the whole point of the section, and the one thing that would
// silently regress back into the old mute grid: six situations, six
// answers, each spoken by creeer.
expect(screen.getAllByLabelText('creeer')).toHaveLength(6);
for (const { respuesta } of puntosInflexion.items) {
expect(screen.getByText(respuesta)).toBeInTheDocument();
}
});
it('states each situation in the second person, as the dueña reads them', () => {
render(<PuntosInflexion />);
// Her own phrasing in the 31/jul call: "estás atrapado en tu compañía",
// "creciste muy rápido pero quieres expandirte". The deck's infinitives
// ("Dar el siguiente salto") would put the reader outside their own story.
for (const { situacion } of puntosInflexion.items) {
expect(situacion).toMatch(/\b(tu|tus|te|ya|creciste|construiste|sabes|puedes|quieres|dejó|volvió|abiertos|mismo)\b/i);
}
});
it('does NOT introduce scarcity', () => {
const { container } = render(<PuntosInflexion />);
expect(container.textContent).not.toMatch(/cupos|cohorte|generación fundadora/i);
});
});
```
- [ ] **Step 2: Correr los tests y verificar que fallan**
Run: `pnpm test components/sections/__tests__/PuntosInflexion.test.tsx`
Expected: FAIL — `getAllByLabelText('creeer')` no encuentra nada y `puntosInflexion.items[0].respuesta` es `undefined`.
- [ ] **Step 3: Reescribir el copy en `lib/content.ts`**
Sustituir el bloque `puntosInflexion` entero (líneas 35–48) por:
```ts
// The six inflection points, from the dueña's deck "Rumbo Estratégico ·
// Propuesta Socio" (slide 2). The deck states each one in the infinitive
// ("Dar el siguiente salto sin saber por dónde"); here it is stated in the
// second person, the way she reads them aloud herself — "estás atrapado en tu
// compañía", "creciste muy rápido pero quieres expandirte" (call of 31/jul,
// 6:01–7:15). `respuesta` is new copy and still needs her sign-off; 01 and 05
// are the two exchanges she improvised on that call, written out.
//
// `situacion` describes the state of the business. It must not drift into how
// the reader feels — that is `dolores`, four sections down, and the two would
// collapse into each other.
export const puntosInflexion = {
eyebrow: 'el contexto',
title: 'Todos los negocios consolidados llegan a un punto de inflexión',
lead: '¿Te reconoces?',
items: [
{
name: 'Crecer',
situacion: 'Ya demostraste que sabes crecer. El siguiente salto no tiene mapa.',
respuesta: 'Me sumerjo hasta entender tu empresa desde dentro. Sales sabiendo por dónde, no intentándolo por todos lados a la vez.',
},
{
name: 'Consolidarse',
situacion: 'Construiste rápido. Escalar así multiplicaría el desorden.',
respuesta: 'Ordenamos lo que ya existe antes de ponerle más peso encima. Escalas sobre algo que aguanta.',
},
{
name: 'Recuperar rentabilidad',
situacion: 'El esfuerzo dejó de traducirse en resultado.',
respuesta: 'Cuestiono lo que todos dan por hecho en tu operación. El margen no siempre está en vender más.',
},
{
name: 'Simplificar',
situacion: 'La operación se volvió más compleja que el negocio.',
respuesta: 'Separo lo que solo tú puedes decidir de lo que la empresa debería resolver sin ti.',
},
{
name: 'Expandirse',
situacion: 'Mercados nuevos, el mismo mapa de decisión.',
respuesta: 'Reviso tu proceso y ubico la oportunidad real antes de mover una ficha. Expandirte deja de ser una apuesta.',
},
{
name: 'Replantear el portafolio',
situacion: 'Varios frentes abiertos y ninguno con su lugar claro.',
respuesta: 'Construimos, juntos, el criterio para decidir qué sigue y qué se deja ir. Por escrito, no en tu cabeza.',
},
],
};
```
- [ ] **Step 4: Reescribir `PuntosInflexion.tsx`**
Sustituir el archivo entero por:
```tsx
'use client';
import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { FondoPuntos } from '@/components/primitives/FondoPuntos';
import { ParDialogo } from '@/components/primitives/ParDialogo';
import { IlustracionInflexion } from '@/components/primitives/IlustracionInflexion';
import { puntosInflexion } from '@/lib/content';
import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, motionProps } from '@/lib/motion';
export function PuntosInflexion() {
const reduced = usePrefersReducedMotion();
return (
<section className="bg-gris-claro relative overflow-hidden">
<FondoPuntos className="-bottom-6 -right-12 w-80 md:w-[32rem] text-azul-profundo/10" distance={32} />
<Container className="relative py-16 md:py-24">
<div className="flex flex-col md:flex-row md:items-center md:justify-between md:gap-12">
<div>
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="text-xs tracking-[0.3em] uppercase font-black text-dorado">{puntosInflexion.eyebrow}</motion.p>
<motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-4 font-sans font-black text-seccion text-azul-profundo max-w-3xl">{puntosInflexion.title}</motion.h2>
</div>
<motion.div variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-8 md:mt-0 shrink-0">
<IlustracionInflexion className="w-56 md:w-72 h-auto mx-auto" />
</motion.div>
</div>
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-14 text-xs tracking-[0.3em] uppercase font-black text-dorado">{puntosInflexion.lead}</motion.p>
{/* The conversation. One <li> per exchange, read top to bottom — no
carousel at any breakpoint, because "te bajas" is the whole shape
the dueña asked for. The stagger reaches ParDialogo's two halves
through React context, so each pair arrives in two beats: the
situation, then the answer. */}
<motion.ul variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-8 list-none p-0 flex flex-col gap-12 md:gap-16">
{puntosInflexion.items.map((item, i) => (
<ParDialogo key={item.name} n={i + 1} {...item} />
))}
</motion.ul>
</Container>
</section>
);
}
```
- [ ] **Step 5: Correr los tests y verificar que pasan**
Run: `pnpm test components/sections/__tests__/PuntosInflexion.test.tsx`
Expected: PASS (5 tests).
- [ ] **Step 6: Correr la suite entera y el typecheck**
Run: `pnpm test && npx tsc --noEmit`
Expected: todo verde. `Carrusel.test.tsx` sigue pasando por su cuenta — el primitivo aún existe, solo dejó de usarse.
- [ ] **Step 7: Commit** *(pedir aprobación primero)*
```bash
git add lib/content.ts components/sections/PuntosInflexion.tsx components/sections/__tests__/PuntosInflexion.test.tsx
git commit -m "feat(recorrido): los seis puntos dejan de ser tarjetas y pasan a contestar"
```
---
### Task 3: Retirar el carrusel
**Files:**
- Delete: `components/primitives/Carrusel.tsx`
- Delete: `components/primitives/__tests__/Carrusel.test.tsx`
**Interfaces:**
- Consumes: nada.
- Produces: nada. Es una retirada.
- [ ] **Step 1: Verificar que de verdad quedó sin consumidores**
Run: `grep -rn "Carrusel" --include="*.tsx" --include="*.ts" components app lib`
Expected: solo `components/primitives/Carrusel.tsx` y su propio test. Si aparece cualquier otro archivo, **parar**: el primitivo sigue en uso y no se retira.
- [ ] **Step 2: Retirarlo**
```bash
git rm components/primitives/Carrusel.tsx components/primitives/__tests__/Carrusel.test.tsx
```
- [ ] **Step 3: Verificar que nada se rompió**
Run: `pnpm test && npx tsc --noEmit && pnpm lint`
Expected: todo verde, con la suite más corta por los tests del carrusel que se fueron.
- [ ] **Step 4: Commit** *(pedir aprobación primero)*
```bash
git add -A components/primitives
git commit -m "chore: retira Carrusel, sin consumidores desde que el recorrido se apila"
```
---
### Task 4: Verificación en navegador
**Files:** ninguno. Es la comprobación de los criterios de aceptación del spec que jsdom no puede dar, porque **jsdom no hace layout**. Dos de los peores fallos de este proyecto —una banda que alargaba el documento 5.800px y otra que llegaba partida en cuatro— pasaron toda la suite en verde.
- [ ] **Step 1: Build de producción**
Run: `pnpm build`
Expected: export estático sin errores.
- [ ] **Step 2: Levantar el servidor**
Run: `pnpm dev`
Expected: sirve en `http://localhost:3000`. **No correr `pnpm build` a la vez que `pnpm dev`**: comparten `.next` y el servidor acaba sirviendo una página corrupta (se midió un documento de 60.569px por esto).
- [ ] **Step 3: Revisar a 1440×900 y a 390×844**
Comprobar contra los criterios del spec:
- los seis pares se leen de arriba abajo como conversación;
- cada respuesta se distingue como voz de creeer sin depender solo del color;
- en 390px no hay desbordes horizontales ni carrusel;
- la sección no deja huecos verticales desproporcionados frente a sus vecinas (`Dolores` viene justo después).
- [ ] **Step 4: Revisar con `prefers-reduced-motion: reduce`**
Cargar la página con la preferencia activada y confirmar que **ningún par queda invisible** y que todas las reglas laterales se dibujan. Es el fallo clásico de este proyecto: las ramas de movimiento reducido que se quedan en `opacity: 0`.
- [ ] **Step 5: Mirar `Dolores` y `PuntosInflexion` juntas**
El spec lo pide explícitamente. Las situaciones describen el estado del negocio; los dolores, cómo se siente. Si al leerlas seguidas suenan a lo mismo, informarlo — **no reescribir `dolores` por cuenta propia**, es texto verbatim de la dueña.
- [ ] **Step 6: Informar al usuario**
Resumir qué se verificó y qué queda pendiente del visto bueno de la dueña: las seis respuestas son redacción nueva y las situaciones reescriben líneas marcadas como verbatim de su deck.
@@ -0,0 +1,110 @@
# El recorrido: la página que te contesta
**Fecha:** 2026-08-03
**Origen:** llamada de Zoom con la dueña del 31/jul/2026 (`Grabación 2026-07-31 171150.mp4`, minutos 5:30–7:40).
**Rama:** `feat/recorrido-dialogo`.
---
## 1. De dónde sale esto
En esa llamada Jazmín comparte pantalla, recorre `en-ti.com` como referencia y describe una idea para la página de creeer. Transcripción del tramo:
> **[06:01]** Pero también estaba pensando que podríamos hacer esto. O sea, dices: empiezas aquí y dices "estás atrapado en tu compañía y no estás creciendo", y ya te bajas, ¿no? Y entonces te bajas y contestas: "esto es para ti". Aquí desafiamos o cuestionamos absolutamente todo lo que has hecho y lo replanteamos, te generamos… y luego otra vez te regresas, ¿no?
>
> **[06:39]** O la siguiente: vas otra vez de cómo está, y aquí preguntas otra cosa. "No estás creciendo, creciste muy rápido pero quieres expandirte y sientes que no…" Quieres expandirte pero llegaste a un punto de inflexión — y ya contesta creeer y dice "hacemos una inmersión, revisamos tu proceso y vemos las áreas de oportunidad, o cuáles son los siguientes pasos para esa expansión".
>
> **[07:15]** ¿Sí me explico? Y luego te regresas, y luego otro. O sea, como que te va contestando y te va como llevando de uno a otro.
**Advertencia sobre la fuente.** Es transcripción automática (Whisper-small sobre el audio de Zoom), no taquigrafía. Tres pasajes quedan en duda: *"y sientes que no sientes"* está mal transcrito y falta el final de la frase; *"hacemos una inversión"* se lee aquí como **inmersión**, que es el paso 01 de `mecanismo` y encaja con el resto de la frase; y hay una repetición por solape de ventanas alrededor de 07:14. El resto se da por fiable. **Pendiente:** que la dueña o Erik confirmen la fidelidad antes de que esto entre en `docs/dueña-directrices.md`.
## 2. El problema
La página tiene hoy las piezas de lo que ella describe, pero sueltas y mudas:
- `PuntosInflexion` enumera seis situaciones en una rejilla de tarjetas. Ninguna dice qué hace creeer al respecto.
- `Mecanismo` dice qué hace creeer —inmersión, desafiar supuestos, estrategia ejecutable— pero desligado de la situación de quien lee.
- `ParaQuien` dice "esto es para ti si…" como lista de requisitos, no como respuesta.
El lector se reconoce en una tarjeta y ahí se acaba. Nada le contesta. Eso es exactamente lo que ella pide: *"te va contestando y te va como llevando de uno a otro"*.
## 3. Concepto: el diálogo
`PuntosInflexion` deja de ser una rejilla y pasa a ser una **secuencia de seis pares**. Cada par es un intercambio:
- **la situación**, en segunda persona, con la voz del lector;
- **la respuesta de creeer**, en primera persona, con la voz de la estratega.
Alternan al bajar, unidos por un conector, de modo que la sección se lee como una conversación y no como un catálogo. Todo el texto queda en el DOM: nada se esconde tras un clic, ni por SEO ni por accesibilidad.
La segunda persona no es invención: es como los enuncia ella misma en la llamada (*"estás atrapado…", "creciste muy rápido pero quieres expandirte…"*).
## 4. El contenido
Los seis puntos de inflexión se conservan —son de la diapositiva 2 de su deck— y cada uno gana su par.
| # | Punto | Situación (tú) | Respuesta (cre≡er) |
|---|-------|----------------|--------------------|
| 01 | Crecer | Ya demostraste que sabes crecer. El siguiente salto no tiene mapa. | Me sumerjo hasta entender tu empresa desde dentro. Sales sabiendo por dónde, no intentándolo por todos lados a la vez. |
| 02 | Consolidarse | Construiste rápido. Escalar así multiplicaría el desorden. | Ordenamos lo que ya existe antes de ponerle más peso encima. Escalas sobre algo que aguanta. |
| 03 | Recuperar rentabilidad | El esfuerzo dejó de traducirse en resultado. | Cuestiono lo que todos dan por hecho en tu operación. El margen no siempre está en vender más. |
| 04 | Simplificar | La operación se volvió más compleja que el negocio. | Separo lo que solo tú puedes decidir de lo que la empresa debería resolver sin ti. |
| 05 | Expandirse | Mercados nuevos, el mismo mapa de decisión. | Reviso tu proceso y ubico la oportunidad real antes de mover una ficha. Expandirte deja de ser una apuesta. |
| 06 | Replantear el portafolio | Varios frentes abiertos y ninguno con su lugar claro. | Construimos, juntos, el criterio para decidir qué sigue y qué se deja ir. Por escrito, no en tu cabeza. |
Los pares **01** y **05** son los dos ejemplos que ella da en el audio, redactados.
### Frontera con `Dolores`
`Dolores` son cuatro citas en primera persona sobre lo que el lector **siente** ("Trabajo más y gano casi lo mismo que hace dos años"). Las seis situaciones de aquí describen el estado del **negocio**, no el ánimo. Esa frontera es deliberada y hay que sostenerla al redactar: si una situación empieza a sonar a queja, invade `Dolores`.
Las dos secciones van seguidas en la página. Conviene mirarlas juntas una vez implementado esto.
### Riesgo de repetición
Las tres respuestas posibles salen todas del mismo mecanismo. Con seis pares hay riesgo real de que las seis digan lo mismo con distintas palabras. Mitigación: cada respuesta abre por el movimiento concreto de esa situación y cierra por lo que cambia para el lector, no por la actividad. Si aun así sobran, **recortar a cuatro es decisión de la dueña** — los seis son suyos.
## 5. Arquitectura
| Archivo | Cambio |
|---------|--------|
| `lib/content.ts` | `puntosInflexion.items` cambia de `{ name, desc }` a `{ name, situacion, respuesta }` |
| `components/primitives/ParDialogo.tsx` | **Nuevo.** Un par: situación, respuesta y el conector entre ambas |
| `components/primitives/__tests__/ParDialogo.test.tsx` | **Nuevo** |
| `components/sections/PuntosInflexion.tsx` | De rejilla + carrusel a secuencia de `ParDialogo` |
| `components/sections/__tests__/PuntosInflexion.test.tsx` | Actualizar: ya no hay tarjetas ni carrusel |
| `components/primitives/Carrusel.tsx` + su test | **Retirar.** Queda sin consumidores (ver abajo) |
**`ParDialogo`** recibe `n`, `name`, `situacion` y `respuesta`, y no sabe nada de la sección que lo monta. Es la única pieza que conoce la forma del intercambio.
Los lados **no alternan par a par**: la situación va siempre al mismo lado y la respuesta siempre al contrario, como en una conversación, donde cada interlocutor mantiene su margen. Lo que alterna al bajar es situación → respuesta → situación → respuesta. Voltear los lados en cada par rompería la lectura de quién habla.
**`PuntosInflexion`** conserva su cabecera actual —eyebrow, título e `IlustracionInflexion`— y sustituye el cuerpo.
**El carrusel móvil desaparece**: el diálogo se apila en vertical y no necesita deslizamiento — además, *"te bajas"* es literalmente lo que pide la dueña. Esto deja a `Carrusel` **sin ningún consumidor**: hoy `PuntosInflexion` es el único. Se retira junto con su test, como se hizo con `SimboloTresLineas`; el historial de git lo conserva si vuelve a hacer falta. Dejarlo montado sin uso repetiría un huérfano que ya se limpió una vez en este proyecto.
**Movimiento.** Cada par entra con `fadeInUp` dentro de un `staggerContainer`, con `viewportOnce`, y **siempre** combinando `usePrefersReducedMotion()` con `motionProps(reduced)`, como el resto de las secciones.
**Los conectores se dibujan con bordes o con formas rellenas, nunca con `pathLength` sobre un SVG escalado de forma no uniforme.** Esa combinación se rompe en Chromium —el guion se recorre en espacio de pantalla pero se normaliza desde el espacio de usuario, y el trazo llega partido en trozos— y es lo que acaba de retirarse con el hilo conductor.
**Tests.** Verifican copy y contrato de marca, no estilos, como el resto del proyecto: que aparezcan las seis situaciones y las seis respuestas; que cada respuesta esté atribuida a creeer y cada situación al lector; que con `prefers-reduced-motion` no quede nada invisible.
## 6. Decisiones que necesitan el visto bueno de la dueña
1. **Se reescriben líneas marcadas como verbatim de su deck.** Los `desc` actuales ("Dar el siguiente salto sin saber por dónde") pasan a segunda persona y dejan de ser literales de la diapositiva 2. Se hace porque es como ella misma los enuncia en la llamada, pero es su texto.
2. **Ella dice "nosotros", la página dice "yo".** En el audio: *"desafiamos", "cuestionamos", "hacemos una inmersión"*. La web está en primera persona del singular porque es una estratega sola, y así está `mecanismo`. Aquí se mantiene el singular salvo donde el trabajo es conjunto ("Construimos, juntos"). Pasar todo a plural sería un cambio global de voz, no de esta sección.
3. **Las seis respuestas son redacción nueva**, no salidas de ningún material suyo. Hay que validarlas una por una.
## 7. Fuera de alcance
La otra mitad de ese tramo de audio: el claim del hero (*"a la mejor, si ponemos 'oye, damos claridad'"*), la **diana con un punto**, y jugar con los puntitos de la marca. Ahí ella está pensando en voz alta y no cierra nada. Merece su propio spec y su propia decisión.
## 8. Criterios de aceptación
- [ ] Los seis pares se leen de arriba abajo como una conversación: situación, respuesta, situación, respuesta.
- [ ] Todo el texto está en el DOM sin interacción: nada tras un clic, un hover o un deslizamiento.
- [ ] Cada respuesta se distingue visualmente como voz de creeer, y cada situación como voz del lector, sin depender solo del color.
- [ ] En móvil (390px) la secuencia se lee sin desbordes horizontales y sin carrusel.
- [ ] Con `prefers-reduced-motion: reduce` no queda ningún par invisible ni ningún conector sin dibujar.
- [ ] `pnpm test`, `npx tsc --noEmit`, `pnpm lint` y `pnpm build` limpios.
- [ ] Ningún conector depende de `pathLength` sobre un SVG con `preserveAspectRatio="none"`.