diff --git a/AGENTS.md b/AGENTS.md index 153a4d7..cf364f3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # 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. +One-page landing para **creeer**, firma de claridad estratégica. Stack: Next.js 14 (App Router) + TypeScript + Tailwind CSS + Framer Motion, exportada como sitio estático. Español. ## Estado del proyecto @@ -17,7 +17,8 @@ One-page landing para **creeer**, marca personal de una estratega de crecimiento - **Paleta:** Azul Profundo `#0A1D3D` + Dorado Estratégico `#C4942E` + Blanco `#FFFFFF` + neutros (Azul Claro `#C8EEF5`, Gris `#F2F2F2`, Gris Medio `#AAAAAA`). **Sin amarillo eléctrico** tipo Hormozi. - **Tipografía:** **Open Sans** (vía `next/font/google`). Es lo más cercano a Aptos en open-source (mismo diseñador: Steve Matteson). **Aptos descartada** en web: la EULA de Microsoft prohíbe servirla públicamente. Decisión de la dueña, 28/jul/2026. - **Tagline de marca:** "CLARIDAD · DIRECCIÓN · CRECIMIENTO". Eslogan: "Estrategia que transforma." -- **Voz:** segunda persona ("tú"/"tu negocio"), sin jerga corporativa. +- **Voz:** segunda persona ("tú"/"tu negocio"), sin jerga corporativa. **creeer habla en primera persona del plural** ("nos sumergimos", "cuestionamos") — decisión de Erik del 6/ago/2026, **visto bueno de la dueña el 19/ago/2026**; antes era singular. Única excepción: las citas de `dolores`, donde el "yo" es el cliente. Hay un test que lo blinda en `lib/__tests__/content.test.ts`. +- **Cifra de trayectoria: 25+** años construyendo marcas (confirmado por la dueña, 19/ago/2026). No 30+. - **Sin escasez en el hero:** no mostrar cupos, cohorte ni "Generación Fundadora" arriba (decisión de la dueña). ## Stack y constraints diff --git a/README.md b/README.md index 72fd62f..82e9ff5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # creeer -Landing page one-page para **creeer**, marca personal de una estratega de crecimiento. Next.js 14 (App Router) + TypeScript + Tailwind CSS + Framer Motion, exportada como sitio estático. +Landing page one-page para **creeer**, firma de claridad estratégica. Next.js 14 (App Router) + TypeScript + Tailwind CSS + Framer Motion, exportada como sitio estático. ## Requisitos @@ -23,7 +23,7 @@ pnpm lint # ESLint app/ # App Router (layout, page, globals.css) components/ primitives/ # Wordmark, Container, CTA (reutilizables) - sections/ # Hero, FraseAncla, ParaQuien, Mecanismo, Disciplinas, Estratega, Cierre, ComoEmpezar + sections/ # Hero, PuntosInflexion, FraseAncla, Dolores, Proceso, Disciplinas, ParaQuien, Estratega, ComoEmpezar, Cierre layout/ # Header, Footer lib/ content.ts # todo el copy (source of truth) diff --git a/app/globals.css b/app/globals.css index 18c262f..050b757 100644 --- a/app/globals.css +++ b/app/globals.css @@ -18,9 +18,43 @@ } @layer base { - a:focus-visible, button:focus-visible { - outline: 3px solid #C4942E; + /* The ring needs to contrast against the background it appears on, not + against the brand. The global gold gave 2.76:1 on white, below the 3:1 + WCAG 2.2 requires for a focus indicator. Deep blue on white gives 16.7:1; + and on the two navy sections, where the navy would disappear, gold takes + over, which gives 6.07:1 there. */ + a:focus-visible, + button:focus-visible { + outline: 3px solid #0a1d3d; outline-offset: 3px; border-radius: 2px; } + + .bg-azul-profundo a:focus-visible, + .bg-azul-profundo button:focus-visible { + outline-color: #c4942e; + } +} + +/* The page's only motion gesture. The hidden state depends on `.js`, which an + inline script in sets before first paint: without JavaScript the + class never lands, nothing is hidden, and the page reads in full. Anyone + who asked for less motion is also excluded here — the media query leaves + them out entirely rather than neutralising it afterwards. */ +@media (prefers-reduced-motion: no-preference) { + .js [data-revelar] { + opacity: 0; + transform: translateY(12px); + } + .js [data-revelar][data-visible] { + animation: revelar 400ms cubic-bezier(0.22, 1, 0.36, 1) both; + animation-delay: calc(var(--i, 0) * 60ms); + } +} + +@keyframes revelar { + to { + opacity: 1; + transform: none; + } } diff --git a/app/layout.tsx b/app/layout.tsx index b386bbd..f793c5d 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -5,23 +5,29 @@ import './globals.css'; const openSans = Open_Sans({ subsets: ['latin'], weight: ['400', '600', '700', '800'], style: ['normal', 'italic'], variable: '--font-open-sans', 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.', + title: 'creeer · claridad estratégica para tu negocio', + description: 'Tu negocio no necesita más esfuerzo — necesita más claridad. Estrategia ejecutable para empresarios con tracción en su punto de inflexión.', 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.', + title: 'creeer · claridad estratégica para tu negocio', + description: 'Tu negocio no necesita más esfuerzo — necesita más claridad. Estrategia ejecutable para empresarios con tracció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.' }, + twitter: { card: 'summary_large_image', title: 'creeer', description: 'Tu negocio no necesita más esfuerzo — necesita más claridad.' }, alternates: { canonical: 'https://creeer.com' }, }; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( + + {/* Blocking and before first paint on purpose: it decides whether the + CSS can hide anything at all. If this doesn't run, there is no + hidden state to reveal. */} + ', '', html, flags=re.S) +texto = re.sub(r'<[^>]+>', ' ', texto) +for frase in ['Tu negocio no necesita', 'Crecer', 'One Year Plan', + 'Devuélvele claridad', 'Agenda tu sesión']: + assert frase in texto, f'FALTA en el HTML: {frase}' +print('las cinco frases clave están en el HTML servido') +PY +``` +Expected: imprime la confirmación. + +- [ ] **Step 4: Comprobar que framer-motion salió del bundle** + +Run: `grep -rl "framer-motion" out/_next/static/chunks/ | head; grep -c framer-motion package.json` +Expected: sin resultados en `chunks/`, y `0` en `package.json`. + +- [ ] **Step 5: Comprobar la escala y el ritmo** + +Run: +```bash +echo "text-display -> $(grep -o 'text-display' out/index.html | wc -l) (debe ser 1)" +echo "secciones navy -> $(grep -o ']*bg-azul-profundo' out/index.html | wc -l) (debe ser 2)" +``` +Expected: 1 y 2. + +- [ ] **Step 6: Cerrar la verificación** + +Run: `pnpm test && npx tsc --noEmit && pnpm lint` +Expected: todo verde. + +Si algo de los pasos 1–5 falla, arreglarlo antes de dar el rediseño por terminado. Reportar los números reales, no la intención. + +- [ ] **Step 7: Commit** + +```bash +git commit --allow-empty -m "chore: verificados los criterios de aceptación del rediseño" +``` + +--- + +## Lo que este plan NO hace + +- **No pide la aprobación de la dueña.** El spec (§1, §15) deja escrito que esta dirección va contra su encargo del 30/jul de "más gráfico y movimiento", y que su doc dice de los dolores y los puntos "usarlos, no reescribirlos" — este plan los conserva verbatim, pero reorganiza todo lo demás. Esa conversación es del usuario con ella. +- **No trae el retrato ni los contactos reales.** Siguen como `TODO dueña`. +- **No decide** si la escasez ("Generación Fundadora") puede vivir fuera del hero. diff --git a/docs/superpowers/plans/2026-08-21-animacion-tres-lineas.md b/docs/superpowers/plans/2026-08-21-animacion-tres-lineas.md new file mode 100644 index 0000000..e5cb834 --- /dev/null +++ b/docs/superpowers/plans/2026-08-21-animacion-tres-lineas.md @@ -0,0 +1,320 @@ +# «Las Tres Líneas» — 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:** Reemplazar la escena Three.js del hero ("Astrolabio") por «Las Tres Líneas»: tres barras doradas que pasan de caos → ≡ → escalera → crecimiento, con reposo vivo. + +**Architecture:** Solo se reescribe el interior de `components/primitives/EscenaClaridad.tsx` (misma ruta, misma API `className`, mismo export). El wrapper (IntersectionObserver, `prefers-reduced-motion`, `SiHayWebGL`, props del `Canvas`) queda idéntico. `ClaridadHero.tsx`, `Hero.tsx` y las 4 pruebas existentes no se tocan. + +**Tech Stack:** Next.js 14 (App Router, `output: export`), `three@0.185`, `@react-three/fiber@8`, TypeScript, Vitest. + +**Spec:** `docs/superpowers/specs/2026-08-21-animacion-tres-lineas-design.md` + +## Global Constraints + +- Gestor de paquetes: **pnpm** (nunca npm). Comandos: `pnpm test`, `pnpm lint`, `npx tsc --noEmit`, `pnpm build`. +- **Comentarios de código en inglés**; copy/docs/commits en español. +- Paleta: dorado `#C4942E` (la escena no usa azul — ya no hay anillos ni sombra). +- **Staging del repo tiene cambios ajenos staged** (deletes de `FormaTrabajar`/`Mecanismo`, rename `CLAUDE.md`). Al commitear usa **rutas explícitas** (`git add `), nunca `git add -A` / `git add .`. +- Commits de esta implementación **aprobados** por el usuario en la rama `animacion-tres-lineas`. +- `prefers-reduced-motion` debe montar la pose final directamente (patrón `estatico` existente). + +## Fuente de verdad de la coreografía + +Los parámetros aprobados son los del **preview del visual companion** (elección de Erik, concepto A). Nota: el geometry es **centrado** (`BoxGeometry(2.3, 0.3, 0.3)`) y crece simétricamente en X — así se ve en el preview aprobado; ignora la mención a "origen en extremo" heredada del código viejo en una frase del spec. + +--- + +### Task 1: Reescribir `EscenaClaridad.tsx` con «Las Tres Líneas» + +**Files:** +- Modify: `components/primitives/EscenaClaridad.tsx` (reescritura completa del interior) +- Test (sin edits): `components/primitives/__tests__/EscenaClaridad.test.tsx` + +**Interfaces:** +- Consumes: `usePrefersReducedMotion` de `@/lib/motion` (ya usada), `Canvas`/`useFrame`/`useThree` de `@react-three/fiber`. +- Produces: `export function EscenaClaridad({ className = '' }: { className?: string })` — firma idéntica a la actual. `ClaridadHero` y los tests la consumen sin cambios. + +- [ ] **Step 1: Baseline — las 4 pruebas pasan hoy** + +Run: `pnpm test components/primitives/__tests__/EscenaClaridad.test.tsx` +Expected: 4 passed (aria-hidden, tamaño del caller, frameloop always, frameloop demand). + +- [ ] **Step 2: Reemplazar el archivo completo por la nueva escena** + +Escribir en `components/primitives/EscenaClaridad.tsx`: + +```tsx +'use client'; + +import { Component, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'; +import { Canvas, useFrame, useThree } from '@react-three/fiber'; +import * as THREE from 'three'; +import { usePrefersReducedMotion } from '@/lib/motion'; + +// VISUAL CONCEPT: "Las Tres Líneas" — the brand symbol tells the story. +// 1. Caos: three golden bars drift tangled, each on its own frequencies. +// 2. Claridad: they settle into the "≡" of cre≡er — level, exact, still. +// 3. Dirección: together they tilt ~9° and stagger into a staircase. +// 4. Crecimiento: each line extends forward (the top one the most) with a +// rising golden glow. The scene then rests, barely alive: a slow camera +// drift and an almost imperceptible emissive pulse. No loops, no resets. + +const DORADO = new THREE.Color('#C4942E'); + +const suave = (t: number) => 1 - Math.pow(1 - t, 3); +const tramo = (t: number, desde: number, dura: number) => + Math.min(1, Math.max(0, (t - desde) / dura)); + +// Phase timings (seconds), verbatim from the approved preview. +const CAOS_DURA = 3.0; +const CLARIDAD = { desde: 3.0, dura: 2.2 }; +const DIRECCION = { desde: 5.2, dura: 2.0 }; +const CRECIMIENTO = { desde: 7.2, dura: 2.4 }; + +// ── Las Tres Líneas ───────────────────────────────────────────────────────── + +function TresLineas({ estatico }: { estatico: boolean }) { + const time = useRef(estatico ? 10 : 0); + const m1 = useRef(null); + const m2 = useRef(null); + const m3 = useRef(null); + const meshes = [m1, m2, m3]; + + // Decoupled drift frequencies per bar, so the chaos never syncs. + const deriva = useMemo( + () => [ + { fx: 1.1, fy: 0.9, fz: 0.7, rx: 0.8, ry: 1.1 }, + { fx: 1.47, fy: 1.4, fz: 1.4, rx: 1.1, ry: 0.9 }, + { fx: 1.84, fy: 1.9, fz: 0.7, rx: 1.4, ry: 0.7 }, + ], + [], + ); + + const geometria = useMemo(() => new THREE.BoxGeometry(2.3, 0.3, 0.3), []); + const materiales = useMemo( + () => + [0, 1, 2].map( + () => + new THREE.MeshStandardMaterial({ + color: DORADO, + metalness: 0.8, + roughness: 0.25, + emissive: DORADO, + emissiveIntensity: 0, + }), + ), + [], + ); + + // Final poses: the "≡" (identity rotation) and the tilted staircase. + const qSimbolo = useMemo(() => new THREE.Quaternion().identity(), []); + const qEscalera = useMemo( + () => new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, -0.16)), + [], + ); + + useFrame((_, delta) => { + if (!estatico) time.current += delta; + const t = time.current; + + const claridad = suave(tramo(t, CLARIDAD.desde, CLARIDAD.dura)); + const direccion = suave(tramo(t, DIRECCION.desde, DIRECCION.dura)); + const crecimiento = suave(tramo(t, CRECIMIENTO.desde, CRECIMIENTO.dura)); + // "Reposo vivo": a slow emissive breathing once the story is told. + const pulso = Math.sin(t * 0.8) * 0.03 * crecimiento; + + meshes.forEach((ref, i) => { + const mesh = ref.current; + if (!mesh) return; + const d = deriva[i]; + + // Caos: free drift around the frame. + const posCaos = new THREE.Vector3( + Math.sin(t * d.fx + i * 2.1) * 1.6, + Math.cos(t * d.fy + i) * 1.1 + 0.7, + Math.sin(t * d.fz + i * 3) * 0.9, + ); + const rotCaos = new THREE.Quaternion().setFromEuler( + new THREE.Euler(t * d.rx, t * (d.ry - i * 0.2), t * 0.5), + ); + + // Claridad: the "≡" of the wordmark. + const posSimbolo = new THREE.Vector3(0, 0.7 - i * 0.7, 0); + // Dirección: the ascending staircase. + const posEscalera = new THREE.Vector3(i * 1.15 - 1.15, 0.35 + i * 0.8, 0); + + mesh.position.copy( + posCaos.lerp(posSimbolo, claridad).lerp(posEscalera, direccion), + ); + mesh.quaternion.slerpQuaternions(rotCaos, qSimbolo, claridad); + mesh.quaternion.slerp(qEscalera, direccion); + + // Crecimiento: each line extends, the top one the most. + mesh.scale.set( + 1 + direccion * 0.1 + crecimiento * (0.35 + i * 0.35), + 1, + 1, + ); + + materiales[i].emissiveIntensity = crecimiento * (0.1 + i * 0.06) + pulso; + }); + }); + + return ( + + {meshes.map((ref, i) => ( + + ))} + + ); +} + +// ── Cámara — frontal, with the preview's gentle drift ─────────────────────── + +const CAMARA: [number, number, number] = [0, 0.9, 8]; +const MIRA: [number, number, number] = [0, 0.7, 0]; + +function Camara({ estatico }: { estatico: boolean }) { + const { camera, invalidate } = useThree(); + const time = useRef(estatico ? 10 : 0); + + useEffect(() => { + camera.position.set(...CAMARA); + camera.lookAt(...MIRA); + invalidate(); + }, [camera, invalidate]); + + useFrame((_, delta) => { + if (estatico) return; + time.current += delta; + camera.position.set( + CAMARA[0] + Math.sin(time.current * 0.25) * 0.5, + CAMARA[1], + CAMARA[2], + ); + camera.lookAt(...MIRA); + }); + + return null; +} + +// ── Iluminación — the three lights of the approved preview ────────────────── + +function Luces() { + return ( + <> + + + + + ); +} + +// ── WebGL Guard ───────────────────────────────────────────────────────────── + +class SiHayWebGL extends Component<{ children: ReactNode }, { falla: boolean }> { + state = { falla: false }; + static getDerivedStateFromError() { return { falla: true }; } + render() { return this.state.falla ? null : this.props.children; } +} + +// ── El Componente ─────────────────────────────────────────────────────────── + +export function EscenaClaridad({ className = '' }: { className?: string }) { + const reducido = usePrefersReducedMotion(); + const contenedor = useRef(null); + const [enVista, setEnVista] = useState(true); + + useEffect(() => { + const el = contenedor.current; + if (!el || typeof IntersectionObserver === 'undefined') return; + const obs = new IntersectionObserver(([entrada]) => setEnVista(entrada.isIntersecting)); + obs.observe(el); + return () => obs.disconnect(); + }, []); + + const animando = enVista && !reducido; + + return ( + + ); +} +``` + +Diferencias clave vs. el archivo viejo: fuera `Anillos`, `Simbolo`, `Suelo`/`texturaSombra` y la `pointLight` azul; el `fov` pasa de 40 a 38 y `far` de 60 a 100 (los del preview); `Camara` recibe `estatico` para congelar la deriva en reduced-motion. + +- [ ] **Step 3: Correr las pruebas del componente** + +Run: `pnpm test components/primitives/__tests__/EscenaClaridad.test.tsx` +Expected: 4 passed — el wrapper no cambió de contrato. + +- [ ] **Step 4: Commit** + +```bash +git add components/primitives/EscenaClaridad.tsx +git commit -m "feat(hero): el astrolabio deja paso a «Las Tres Líneas» — caos, ≡, escalera, crecimiento" +``` + +--- + +### Task 2: Verificación completa y ajuste de luces + +**Files:** +- Modify (solo si hace falta): `components/primitives/EscenaClaridad.tsx` + +**Interfaces:** +- Consumes: Task 1 completo. +- Produces: nada nuevo — verificación. + +- [ ] **Step 1: Typecheck y lint** + +Run: `npx tsc --noEmit && pnpm lint` +Expected: sin errores. Si `three@0.185` tipa distinto algún prop de luces, ajusta el valor, no el tipo. + +- [ ] **Step 2: Suite completa** + +Run: `pnpm test` +Expected: todos pasan (incl. content/contraste/escala, intactos). + +- [ ] **Step 3: Build de export estático** + +Run: `pnpm build` +Expected: export exitoso a `out/` sin errores de prerender. + +- [ ] **Step 4: Humo visual** + +Run: `pnpm dev` → abrir `http://localhost:3000`, dejar correr el relato completo (~10 s) y verificar: (a) las tres barras forman el ≡, (b) se vuelven escalera ascendente, (c) crecen con brillo, (d) reposo con deriva sutil sin reiniciar, (e) con DevTools → rendering → emulate prefers-reduced-motion, la escena monta directo en la escalera, quieta. + +Nota de luces: el preview corrió en three 0.160; en 0.185 la caída física de la `pointLight` es igual pero si el dorado se ve apagado o quemado, ajusta `intensity` (rango sano 6–16) en `Luces` — es calibración, no cambio de concepto. + +- [ ] **Step 5: Commit (solo si hubo ajuste)** + +```bash +git add components/primitives/EscenaClaridad.tsx +git commit -m "fix(hero): calibración de luces de «Las Tres Líneas» para three 0.185" +``` + +Si no hubo ajuste, este paso se omite. + +--- + +## Self-Review + +- **Cobertura del spec:** concepto/tabla de fases → Task 1 Step 2 (`CAOS_DURA`, `CLARIDAD`, `DIRECCION`, `CRECIMIENTO`, `pulso`, deriva de cámara); reposo vivo → `pulso` + drift de `Camara`; reduced-motion → `estatico` en `Camara`/`TresLineas` + `frameloop: demand`; fuera de vista → IntersectionObserver intacto; eliminaciones (Anillos/Suelo/pointLight azul) → código nuevo no los contiene; pruebas intactas → Steps 1/3; commit en rama → aprobado. ✔ +- **Placeholders:** ninguno — todo paso tiene código o comando exacto. ✔ +- **Consistencia de tipos:** `EscenaClaridad({ className }: { className?: string })` idéntico al consumido por `ClaridadHero.tsx:54` y por los tests. `Camara`/`TresLineas` comparten la prop `estatico: boolean`. ✔ diff --git a/docs/superpowers/specs/2026-08-19-creeer-rediseno-documento-design.md b/docs/superpowers/specs/2026-08-19-creeer-rediseno-documento-design.md new file mode 100644 index 0000000..d0add7e --- /dev/null +++ b/docs/superpowers/specs/2026-08-19-creeer-rediseno-documento-design.md @@ -0,0 +1,247 @@ +# Rediseño completo · «El documento» + +**Fecha:** 19/ago/2026 +**Rama:** a crear desde `fix/contraste-y-hero-visible` +**Estado:** aprobado por el usuario (dirección y alcance), pendiente de la dueña + +--- + +## 1. Encargo + +Rediseñar la landing por completo. El usuario fijó qué sobrevive: + +| Se conserva | Queda libre | +|---|---| +| La paleta entera (tokens de `tailwind.config.ts`) | Estructura y orden de secciones | +| El wordmark `cre≡er` (SVG trazado del manual) | Copy | +| Open Sans | Layout, retícula, escala tipográfica | +| | Sistema gráfico, ilustraciones, fotografía | +| | Movimiento | +| | Arquitectura de componentes | + +**Conflicto declarado y aceptado:** `docs/dueña-directrices.md` registra que la dueña pidió el 30/jul "algo de gráfico y movimiento". Esta dirección reduce ambos. Y su doc dice de los dolores y los puntos de inflexión: "ya están escritos en su voz — usarlos, no reescribirlos"; este spec los conserva verbatim (§10), pero reorganiza todo lo demás. Ambas cosas necesitan el visto bueno de la dueña antes de publicar. + +## 2. Concepto rector + +creeer vende un documento. Su propio deck lo dice: cada etapa "deja algo por escrito", y la etapa 08 *es* un One Year Plan. + +**La página se comporta como el entregable en vez de describirlo.** Un documento estratégico escrito sobre el negocio del lector: secciones numeradas, columna de margen, reglas finas, medida de texto disciplinada, cero decoración ambiental. La página demuestra claridad en lugar de prometerla. + +De ahí sale todo lo demás. Cuando una decisión no esté en este spec, la pregunta es: *¿esto lo haría un documento estratégico bien compuesto?* + +## 3. Estructura: 6 secciones + +| # | Sección | Fondo | Origen | +|---|---|---|---| +| 01 | Apertura | blanco | `Hero` | +| 02 | Dónde estás | blanco | `PuntosInflexion` + `Dolores` | +| 03 | Qué cambia | **navy** | `FraseAncla` + lead de `Disciplinas` | +| 04 | El trabajo | blanco | `Proceso` | +| 05 | Para quién · Quién | blanco | `ParaQuien` + `Estratega` | +| 06 | Empezar | **navy** | `ComoEmpezar` + `Cierre` | + +### 01 · Apertura +Wordmark (`size="xl"`, con tagline), antetítulo, titular en `display`, una línea de quién, dos CTAs. Sin ilustración, sin campos de puntos. Altura natural — se acabó el `min-h-[80vh]`, que en móvil pelea con la barra del navegador. + +Su columna de margen lleva el número `01` y su `MarcaSeccion` en `paso={0}`: las líneas más dispersas de toda la página. El sistema queda establecido desde la primera pantalla. + +### 02 · Dónde estás +Los seis puntos de inflexión como tabla de dos columnas: nombre (700) | situación (`cuerpo`), separados por filete `gris-claro`. Se lee entera en segundos. + +Debajo, un aparte enmarcado con los cuatro dolores en primera persona, a `sumario` en cursiva, uno por línea con filete entre ellos. Cierra el remate de la dueña ("Esto no se resuelve con más esfuerzo…") a `titulo`, con la resolución en dorado. + +**Desaparece el escenario fijado de 600vh**, con su `IntersectionObserver` manual, sus seis fotografías, sus velos, su raíl y su barra de progreso. + +### 03 · Qué cambia — navy +El centro emocional y el primer navy. La frase ancla va a `titulo` con la segunda cláusula en dorado — no a `display`, que se gasta una sola vez en toda la página y es del titular de 01 (§4). Debajo, el insight que hoy vive en `Disciplinas`: "No vendemos marketing. Vendemos claridad estratégica". Las tres disciplinas quedan como una línea de texto, no como tres tarjetas con icono. + +### 04 · El trabajo — blanco +Las ocho etapas como índice numerado, no como ocho tarjetas con foto. Cada etapa: número tabular, nombre a `sumario`/700, y los entregables como chips `azul-claro` con texto navy. Las tres fases aprobadas (Inmersión → Desafiar supuestos → Estrategia ejecutable) como tira encima del índice. + +La etapa 08 se distingue por ser la última y por llevar su nombre a `titulo` en vez de a `sumario`, no por cambiar de color de fondo ni de tarjeta. + +**Desaparecen las ocho fotografías** y los 27 iconos de entregable (a 18px con trazo efectivo de 1,2px no se distinguen entre sí). + +### 05 · Para quién · Quién — blanco +Dos columnas separadas por filete vertical: "Sí es para ti" / "No es para ti", cuatro criterios cada una. Sin losa navy — el navy está reservado a 03 y 06. + +Debajo, quién está detrás: hueco de retrato (`aspect-[4/5]`), el 30+ como cifra estática, bio y la cita institucional. **La cifra ya no cuenta hacia arriba.** + +### 06 · Empezar — navy +Los tres pasos como lista numerada, el CTA principal a WhatsApp, y los tres enlaces de contacto. Cierra con el wordmark y el legal. Absorbe el footer. + +## 4. Sistema tipográfico + +Open Sans, cinco roles. Reemplazan a `ancla` y `seccion`, que se eliminan. + +```ts +fontSize: { + display: ['clamp(3.5rem, 9vw, 7rem)', { lineHeight: '0.94', letterSpacing: '-0.03em' }], + titulo: ['clamp(2.25rem, 4.5vw, 3.5rem)', { lineHeight: '1', letterSpacing: '-0.02em' }], + sumario: ['clamp(1.375rem, 2vw, 1.75rem)', { lineHeight: '1.35', letterSpacing: '-0.01em' }], + cuerpo: ['1.0625rem', { lineHeight: '1.7' }], + rotulo: ['0.75rem', { lineHeight: '1.2', letterSpacing: '0.3em' }], +} +``` + +**Reglas duras:** + +- `display` aparece **exactamente una vez** en toda la página: el titular de 01. Hoy hay cinco elementos empatados en 4,5rem, y por eso ninguno es el más grande. +- Medida máxima de texto corrido: **68 caracteres** (`max-w-[68ch]`). +- Pesos: 400 cuerpo · 600 énfasis y rótulos · 700 títulos · 800 display. **Prohibido `font-black`**: Open Sans llega a 800 y `layout.tsx` carga hasta 800, así que los 34 usos actuales caen a 800 de todos modos. Se elimina el peso 900 del vocabulario. +- Números de sección y de etapa con `font-variant-numeric: tabular-nums`. +- `text-balance` en titulares, `text-pretty` en párrafos. + +## 5. Retícula + +``` +│ 02 │ DÓNDE ESTÁS │ +│ ≡ │ │ +│ │ Seis situaciones. Reconoce la tuya. │ +│ │ │ +│ │ Crecer El siguiente salto │ +│ │ no tiene mapa. │ +``` + +- Contenedor: `max-w-content` sube de 1152px a **1280px**, con `px-6 md:px-12`. +- `Seccion` monta `grid md:grid-cols-[7rem_minmax(0,1fr)] gap-x-10`. Columna izquierda: número tabular + `MarcaSeccion`. Columna derecha: todo el contenido. +- En móvil colapsa a una columna; el número y la marca van en línea sobre el antetítulo. +- La tensión entre reglas anchas y texto angosto (68ch dentro de 1280px) es deliberada: es lo que hace que la composición se lea editorial. + +**Ritmo vertical**, tres niveles — hoy ocho secciones comparten `py-16 md:py-24` y la página va como un metrónomo: + +| Nivel | Padding | Dónde | +|---|---|---| +| `compacta` | `py-20 md:py-28` | 04, 05 | +| `normal` | `py-28 md:py-40` | 02 | +| `momento` | `py-36 md:py-56` | 01, 03, 06 | + +01 va en `momento` y sin `min-height`: la apertura respira por padding, no por alto de viewport. + +## 6. Color + +Los tokens no cambian. Cambia la disciplina de uso. + +- **Un solo blanco.** Se eliminan `hondo-blanco` y `hondo-claro`: sus gradientes se solapan (`hondo-claro` empieza en `#FFFFFF`) y hacían indistinguibles cuatro secciones seguidas. Fondo claro = `#FFFFFF` plano. +- **`gris-claro`** deja de ser fondo de sección: solo filetes y rellenos de ficha. +- **Navy exactamente dos veces**, en 03 y 06. Se conserva `hondo-navy` para darles relieve. +- **Dorado, tres trabajos y ninguno más:** el `≡` del wordmark, `MarcaSeccion`, y una palabra de acento por declaración. Se elimina el dorado como relleno de icono y como campo ambiental. Se eliminan `velo-dorado`, `velo-frio` y `dorado-metal`. +- **`azul-claro #C8EEF5`** — hoy declarado y sin usar — pasa a ser el fondo de los chips de entregables en 04, con texto navy: **13,5:1**. +- Se conserva `dorado-texto #8A6520` para texto pequeño dorado sobre claro. + +**Contraste mínimo exigido:** 4,5:1 texto normal, 3:1 texto grande y gráficos con significado. Esto cierra los pendientes que quedaron abiertos en la revisión: el acento del titular, el 30+ de Estratega, la flecha entre fases y los iconos dorados sobre blanco — todos desaparecen o se recolorean. + +## 7. `MarcaSeccion`: el ≡ como sistema + +La única idea gráfica de la página, y la respuesta a "le falta gráfico". + +Cada sección lleva su marca en la columna de margen, y **la marca progresa al bajar**: muchas líneas desordenadas al principio, tres barras alineadas al final. La filosofía del manual (Complejidad → Enfoque → Claridad) deja de estar ilustrada en un rincón y pasa a ser el mobiliario del documento. + +**Implementación:** reutiliza la geometría ya probada de `lib/simbolo.ts` (`VIEWBOX_SIMBOLO`, `LINEAS_SIMBOLO`, `BARRAS_SIMBOLO`) y las posiciones dispersas de `SecuenciaClaridad`. `MarcaSeccion({ paso })` con `paso` de 0 a 5 interpola **estáticamente** cada línea entre su posición dispersa y su barra destino con factor `paso/5`; las líneas no supervivientes bajan de opacidad con el mismo factor. En `paso === 5` quedan las tres barras del `≡` exactas. + +Sin animación, sin estado, sin JS: es un SVG calculado en render. Server component. + +**Sustituye a todo el aparato gráfico actual.** Se eliminan diecisiete primitivas: `FondoPuntos`, `PatronPuntos`, `Resplandor`, `IlustracionNudo`, `IlustracionBifurcacion`, `IlustracionInflexion`, `IlustracionConvergencia`, `SecuenciaClaridad`, `SimboloMarca`, `TarjetaSpotlight`, `MarcoRetrato`, `EscenaDialogo`, `ParDialogo`, `FondoFoto`, `TextoDesenfoque`, `ContadorNumero`, `ProgresoLectura`, y el sistema de iconos `Icono` con sus 41 glifos. + +## 8. Movimiento + +**Un solo gesto: `revelar`.** 12px de subida + fade, 400ms, `cubic-bezier(0.22, 1, 0.36, 1)`, una vez, al entrar en viewport, escalonado 60ms entre hermanos. + +Se eliminan: parallax, scroll fijado, contadores, spotlight, hover-lift, blur palabra a palabra, barra de progreso de lectura, compactación del header, dibujado de ilustraciones y puntos reactivos al cursor. + +**Montado al revés de como está hoy — este es el punto crítico.** El export actual manda 112 elementos en `opacity: 0`; si el JS no llega, la página no se lee. + +1. `app/layout.tsx` incluye un script inline bloqueante en ``: `document.documentElement.classList.add('js')`. +2. `globals.css`: + ```css + @media (prefers-reduced-motion: no-preference) { + .js [data-revelar] { opacity: 0; transform: translateY(12px); } + .js [data-revelar][data-visible] { animation: revelar 400ms cubic-bezier(.22,1,.36,1) both; + animation-delay: calc(var(--i, 0) * 60ms); } + } + @keyframes revelar { to { opacity: 1; transform: none; } } + ``` +3. `` (client) pinta un envoltorio con `data-revelar` y `style={{ '--i': indice }}`. Un único `IntersectionObserver` a nivel de módulo, compartido por todas las instancias, marca `data-visible` al entrar y deja de observar. `indice` lo pasa la sección: es la posición del bloque entre sus hermanos, y sirve solo para escalonar el retardo. Cuando se omite, vale 0. + +**Consecuencias:** sin JS, todo visible. Con `prefers-reduced-motion: reduce`, el estado oculto nunca llega a aplicarse — no hace falta el hook `usePrefersReducedMotion` ni el spread `motionProps` en ninguna sección. + +## 9. Arquitectura + +- **Fuera `framer-motion`** (53,6 kB del bundle compartido) y **fuera `embla-carousel`** + `embla-carousel-autoplay`, que ya no usa nadie. +- **Las secciones vuelven a ser server components.** Hoy las diez son `'use client'`. El único componente cliente será `Revelar`. +- **Siete primitivas**, frente a las ~20 actuales: + + | Primitiva | Papel | + |---|---| + | `Container` | ancho y padding horizontal | + | `Seccion` | la retícula de margen + el ritmo vertical + fondo | + | `MarcaSeccion` | la progresión del ≡ | + | `Antetitulo` | el rótulo dorado (ya existe) | + | `CTA` | los botones (ya existe) | + | `Wordmark` | el logotipo (ya existe, intacto) | + | `Revelar` | el único gesto de movimiento | + +- `lib/content.ts` sigue siendo el source of truth del copy. +- `lib/motion.ts` se elimina. + +## 10. Copy + +**Verbatim, sin tocar** (son palabras de la dueña, de su deck): + +- Los seis puntos de inflexión (nombre + situación). +- Los cuatro dolores en primera persona. +- El remate: "Esto no se resuelve con más esfuerzo. Se resuelve con una estrategia clara." +- Las frases-ancla aprobadas. +- Los nombres de las ocho etapas y sus entregables. +- El insight: "No vendemos marketing. Vendemos claridad estratégica." + +**Se escribe nuevo** solo el tejido conectivo que pide la estructura nueva: antetítulos y títulos de las seis secciones, y los sumarios de una línea bajo cada uno. Se recortan las respuestas largas del recorrido, que existían para llenar seis pantallas fijadas. + +**Se mantiene la prohibición de escasez** en 01 (`cupos|cohorte|generación fundadora`), decisión explícita de la dueña. + +Los placeholders `TODO dueña` (WhatsApp, email, LinkedIn, dominio, retrato) siguen siendo placeholders. No se inventan valores. + +## 11. Accesibilidad + +- Skip link al `
` — hoy no hay. +- Un solo `

` (01); cada sección con su `

`. +- `100svh`/`100dvh` en vez de `100vh` donde haga falta. +- Foco visible con contraste ≥ 3:1 **contra el fondo sobre el que aparece**: anillo navy sobre claro, dorado sobre navy. El dorado global de hoy da 2,76:1 sobre blanco. +- Nada de contenido oculto tras estado de scroll. + +## 12. Tests + +Se mantiene la convención del repo: **copy y contrato de marca, no estilos**, salvo cuando el estilo *es* el contrato (contraste, contenido oculto). + +Se conservan intactos: `simbolo.test.ts`, `Wordmark.test.tsx`, `Antetitulo.test.tsx`, `CTA.test.tsx`, `Container.test.tsx`, `contraste.test.ts` y `content.test.ts`. + +Se reescriben, no se conservan, los tests de sección: las seis secciones nuevas no se corresponden una a una con las diez viejas. Cada una estrena su test de copy, y el test negativo de escasez viaja de `Hero.test.tsx` al de la sección 01. + +Se añaden: + +1. **Guarda de contenido oculto**, generalizada a las seis secciones: ninguna renderiza elementos con `style.opacity === '0'` ni `style.filter` con blur. Es el fallo que ya nos mordió una vez. +2. **`MarcaSeccion`**: `paso={5}` produce exactamente las tres barras de `LINEAS_SIMBOLO`; `paso={0}` produce las nueve líneas dispersas. +3. **Guarda de escala**: la página monta exactamente un elemento con `text-display`. + +Se eliminan los tests de los componentes eliminados. + +## 13. Criterios de aceptación + +1. `pnpm test`, `npx tsc --noEmit` y `pnpm lint` limpios. +2. `pnpm build` correcto, y `out/index.html` con **cero** ocurrencias de `opacity:0` y de `filter:blur`. +3. `framer-motion` y `embla-carousel` no aparecen en `package.json` ni en el bundle. +4. Exactamente **dos** secciones navy y **un** elemento a escala `display`. +5. Todo par texto/fondo cumple 4,5:1 (normal) o 3:1 (grande y gráficos). +6. Con JavaScript desactivado, la página se lee completa. + +## 14. Fuera de alcance + +- El retrato real de la estratega y los datos de contacto: siguen pendientes de la dueña. +- Los dos casos de proyectos reales que la dueña prometió el 29/jul. +- Cualquier decisión sobre si la escasez ("Generación Fundadora") puede vivir fuera del hero. + +## 15. Riesgos + +1. **Va contra el último encargo de la dueña.** Pidió "algo de gráfico y movimiento"; esto reduce ambos. El argumento a favor es que el ≡ progresivo es más gráfico que diez adornos ambientales, y que un solo gesto de movimiento se nota más que doce repartidos. Es su marca: necesita aprobarlo. +2. **Se retira trabajo real.** El recorrido fijado, cinco ilustraciones SVG, catorce fotografías acreditadas y el sistema de iconos son semanas de trabajo. Queda todo en git, pero sale de la página. +3. **Con una sola familia tipográfica, la distinción depende entera de la composición.** Si el resultado se siente genérico, la salida natural sería una segunda familia para display — y eso choca con la decisión de tipografía que el usuario marcó como intocable. diff --git a/lib/__tests__/content.test.ts b/lib/__tests__/content.test.ts new file mode 100644 index 0000000..afd2c3c --- /dev/null +++ b/lib/__tests__/content.test.ts @@ -0,0 +1,101 @@ +import { describe, it, expect } from 'vitest'; +import * as content from '../content'; +import * as contenido from '../content'; +import { + apertura, dondeEstas, queCambia, elTrabajo, paraQuienQuien, empezar, + puntosInflexion, dolores, fraseAncla, +} from '../content'; + +// The landing speaks in the first person plural (Erik's decision, 6/ago/2026, +// recorded in docs/superpowers/specs/2026-08-06-proceso-ocho-etapas-design.md). +// Before that it was singular, and the change touched copy in six different +// objects — easy to reintroduce by accident when editing one of them. +// +// The reader stays "tú": that half is the dueña's locked decision and is not +// what this file guards. + +// Unambiguous first-person-singular markers: conjugated verbs and imperatives +// with the enclitic "-me". Deliberately excludes anything that doubles as a +// noun — "trabajo" is one ("el trabajo completo"), which is why the whole +// word list is verbs and never bare stems. +const SINGULAR = /\b(me sumerjo|sumerjo|cuestiono|separo|reviso|ubico|acompaño|escríbeme|cuéntame|conmigo)\b/i; + +describe('voz de la marca en lib/content.ts', () => { + it('speaks in the plural everywhere creeer is the one speaking', () => { + // `dolores` is the one exception and it is not an oversight: those four + // quotes are in the first person singular because the CLIENT is talking, + // not creeer ("Trabajo más y gano casi lo mismo que hace dos años"). + // They are verbatim from slide 3 of the dueña's deck and must not move. + const { dolores, ...resto } = content; + + for (const [nombre, valor] of Object.entries(resto)) { + expect(JSON.stringify(valor), `«${nombre}» quedó en singular`).not.toMatch(SINGULAR); + } + + // The eyebrow is where the old singular voice survived the plural + // migration — locked now that the dueña confirmed it (19/ago/2026). + expect(apertura.eyebrow).not.toMatch(/\btu estratega\b/i); + + // Guard the guard: if the quotes are ever rewritten, this fails and forces + // a decision instead of silently weakening the check above. + expect(JSON.stringify(dolores)).toMatch(/Trabajo más y gano casi lo mismo/); + }); + + it('keeps the reader in the second person', () => { + expect(JSON.stringify(content.paraQuien)).toMatch(/\btienes\b/i); + expect(JSON.stringify(content.proceso)).toMatch(/\btu\b/i); + }); +}); + +describe('la estructura de seis secciones', () => { + it('numera el documento del 01 al 06 sin saltos', () => { + expect( + [apertura, dondeEstas, queCambia, elTrabajo, paraQuienQuien, empezar].map((s) => s.numero), + ).toEqual(['01', '02', '03', '04', '05', '06']); + }); + + it('conserva verbatim las palabras de la dueña', () => { + // Her doc is explicit: "they are already written in her voice — use them, + // don't rewrite them." These tests fail if someone rewrites them. + expect(dondeEstas.dolores).toContain('Trabajo más y gano casi lo mismo que hace dos años.'); + expect(dondeEstas.remate.accent).toBe('Se resuelve con una estrategia clara.'); + expect(queCambia.ancla.accent).toBe('dejan de crecer por falta de claridad.'); + expect(dondeEstas.puntos.map((p) => p.name)).toEqual([ + 'Crecer', 'Consolidarse', 'Recuperar rentabilidad', + 'Simplificar', 'Expandirse', 'Replantear el portafolio', + ]); + }); + + it('los alias viejos siguen leyendo de la misma fuente, no de una copia', () => { + // While the two structures coexist, a silent divergence between them + // would mean different copy on the same page. + expect(puntosInflexion.items[0].situacion).toBe(dondeEstas.puntos[0].situacion); + expect(dolores.quotes).toEqual(dondeEstas.dolores); + expect(fraseAncla.quote).toBe(queCambia.ancla); + }); + + it('mantiene en pie todo lo que las secciones viejas importan', () => { + // The ten old sections stay mounted until Task 13, and a single missing + // name here breaks the whole build. This is the real list of names the + // code imports today from `@/lib/content`. + const modulo = contenido as Record; + [ + 'brandPillars', 'cierre', 'comoEmpezar', 'creditosFotos', 'disciplinas', + 'dolores', 'estratega', 'footer', 'fraseAncla', 'header', 'hero', + 'paraQuien', 'proceso', 'puntosInflexion', + ].forEach((nombre) => expect(modulo[nombre], nombre).toBeDefined()); + }); + + it('no promete escasez en la apertura', () => { + expect(JSON.stringify(apertura)).not.toMatch(/cupos|cohorte|generación fundadora/i); + }); + + it('arma los tres contactos como enlaces que abren algo', () => { + // An email without `mailto:` is a relative URL: the browser navigates to + // /nombre@dominio and lands on a 404 instead of opening the mail client. + const { whatsapp, email, linkedin } = empezar.contacto; + expect(whatsapp.href).toMatch(/^https:\/\/wa\.me\/\d{10,15}$/); + expect(email.href).toMatch(/^mailto:[^\s@]+@[^\s@]+\.[^\s@]+$/); + expect(linkedin.href).toMatch(/^https:\/\/(www\.)?linkedin\.com\/in\/[\w-]+\/?$/); + }); +}); diff --git a/lib/__tests__/contraste.test.ts b/lib/__tests__/contraste.test.ts new file mode 100644 index 0000000..561a911 --- /dev/null +++ b/lib/__tests__/contraste.test.ts @@ -0,0 +1,41 @@ +import { readFileSync } from 'node:fs'; +import { describe, it, expect } from 'vitest'; + +// Guardas de contraste, leídas del código fuente porque son una regla de +// sistema, no el comportamiento de un componente suelto: lo que hay que impedir +// es que el token vuelva a aparecer donde no se lee. +// +// `gris-medio` (#AAAAAA) da 7,2:1 sobre azul profundo — ahí es el token +// correcto, y en Disciplinas y el footer se queda. Sobre blanco da 2,3:1 y +// sobre gris-claro 2,1:1: en fondo claro es texto que no se lee. +// +// `dorado` (#C4942E) da 6,07:1 sobre azul profundo y 2,76:1 / 2,46:1 sobre los +// dos fondos claros. Sigue siendo el color de rellenos, reglas, iconos y el ≡; +// lo que no puede ser en claro es texto pequeño, y para eso está `dorado-texto`. + +const SOBRE_CLARO = [ + 'components/sections/Hero.tsx', + 'components/sections/Dolores.tsx', + 'components/sections/Proceso.tsx', + 'components/sections/ParaQuien.tsx', + 'components/sections/Estratega.tsx', + 'components/sections/ComoEmpezar.tsx', + // La rama de movimiento reducido del recorrido va sobre gris-claro, y es + // justo la que sirve a quien más necesita que el texto se lea. + 'components/primitives/ParDialogo.tsx', +]; + +describe('tokens de texto sobre fondo claro', () => { + it.each(SOBRE_CLARO)('%s no pinta texto en gris-medio', (archivo) => { + expect(readFileSync(archivo, 'utf8')).not.toMatch(/text-gris-medio/); + }); + + it('ParDialogo etiqueta al hablante en el dorado legible', () => { + // La regla vertical sigue siendo `bg-dorado` — el dorado del manual como + // relleno no tiene problema. Lo que no puede quedarse en #C4942E es la + // etiqueta, que es texto de 12px sobre gris-claro. + const fuente = readFileSync('components/primitives/ParDialogo.tsx', 'utf8'); + expect(fuente).toMatch(/text-dorado-texto/); + expect(fuente).not.toMatch(/text-dorado(?!-)/); + }); +}); diff --git a/lib/__tests__/enlaces.test.ts b/lib/__tests__/enlaces.test.ts new file mode 100644 index 0000000..e849960 --- /dev/null +++ b/lib/__tests__/enlaces.test.ts @@ -0,0 +1,15 @@ +import { describe, it, expect } from 'vitest'; +import { esExterno, propsEnlace } from '../enlaces'; + +describe('propsEnlace', () => { + it('opens outside links in a new tab without handing over window.opener', () => { + expect(propsEnlace('https://wa.me/525558185802')).toEqual({ target: '_blank', rel: 'noopener noreferrer' }); + expect(propsEnlace('https://www.linkedin.com/in/x')).toEqual({ target: '_blank', rel: 'noopener noreferrer' }); + }); + + it('leaves in-page anchors and mailto: in the same tab', () => { + expect(propsEnlace('#contacto')).toEqual({}); + expect(propsEnlace('mailto:hola@creeer.com')).toEqual({}); + expect(esExterno('mailto:hola@creeer.com')).toBe(false); + }); +}); diff --git a/lib/__tests__/escala.test.ts b/lib/__tests__/escala.test.ts new file mode 100644 index 0000000..6ecc816 --- /dev/null +++ b/lib/__tests__/escala.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from 'vitest'; +import config from '../../tailwind.config'; + +// The scale is a contract, not a preference: the problem this redesign fixes is +// that five elements were tied for largest on the page, and because of that, +// none of them read as largest. Five roles: only one reaches above 4rem. +const escala = () => (config.theme?.extend?.fontSize ?? {}) as Record; + +describe('escala tipográfica', () => { + it('define los cinco roles del documento', () => { + expect(Object.keys(escala())).toEqual( + expect.arrayContaining(['display', 'titulo', 'sumario', 'cuerpo', 'rotulo']), + ); + }); + + it('display es el único que llega a 7rem', () => { + const conSieteRem = Object.entries(escala()).filter(([, v]) => v[0].includes('7rem')); + expect(conSieteRem.map(([k]) => k)).toEqual(['display']); + }); + + it('el cuerpo se compone holgado y el display apretado', () => { + const lh = (rol: string) => (escala()[rol][1] as { lineHeight: string }).lineHeight; + expect(Number(lh('display'))).toBeLessThan(1); + expect(Number(lh('cuerpo'))).toBeGreaterThan(1.6); + }); + + it('reserva una medida de 68 caracteres para el texto corrido', () => { + const anchos = (config.theme?.extend?.maxWidth ?? {}) as Record; + expect(anchos.medida).toBe('68ch'); + }); +}); diff --git a/lib/content.ts b/lib/content.ts index 9e8a68f..0be835a 100644 --- a/lib/content.ts +++ b/lib/content.ts @@ -11,160 +11,199 @@ export const brandPillars = { pillar3: "Crecimiento", }; -export const hero = { - eyebrow: '— tu estratega de crecimiento —', - // Headline broken into segments for emphasis styling +// ── 01 · Apertura ────────────────────────────────────────────────────────── +export const apertura = { + numero: '01', + eyebrow: 'claridad estratégica para tu negocio', headline: { line1: 'Tu negocio no necesita', - effort: 'más esfuerzo.', // gold (HV-1) or gold italic (V-a) + acento: 'más esfuerzo.', line3: 'Necesita', - clarity: 'más claridad.', // underlined gold + cierre: 'más claridad.', }, - cta: 'Define tu siguiente movimiento', - ctaSecundario: 'Ver cómo trabajo', + quien: 'Claridad estratégica para empresarios con tracción que llegaron a su punto de inflexión.', + // Hormozi: Dream Outcome — the hero names the problem; this line names the + // result the reader walks away with. Concrete, time-bound, personal. + dreamOutcome: 'Un plan a 12 meses para que tu negocio crezca sin depender de ti en cada decisión.', + cta: 'Agenda una sesión de claridad — 30 min, sin costo', + ctaSecundario: 'Ver cómo trabajamos', }; -export const fraseAncla = { - // Quote split into parts so the second clause can render in gold. - quote: { - before: 'Las empresas no dejan de crecer por falta de talento;', - accent: 'dejan de crecer por falta de claridad.', // gold - }, -}; +// ── 02 · Dónde estás ─────────────────────────────────────────────────────── +// Los seis puntos y los cuatro dolores son de las diapositivas 2 y 3 del deck +// de la dueña. Verbatim. `respuesta` desaparece: existía para llenar seis +// pantallas fijadas y el recorrido ya no existe. +const PUNTOS = [ + { name: 'Crecer', situacion: 'Ya demostraste que sabes crecer. El siguiente salto no tiene mapa.' }, + { name: 'Consolidarse', situacion: 'Construiste rápido. Escalar así multiplicaría el desorden.' }, + { name: 'Recuperar rentabilidad', situacion: 'Tu esfuerzo dejó de traducirse en resultado.' }, + { name: 'Simplificar', situacion: 'Tu operación se volvió más compleja que tu negocio.' }, + { name: 'Expandirse', situacion: 'Vas a mercados nuevos con el mismo mapa de decisión.' }, + { name: 'Replantear el portafolio', situacion: 'Tienes varios frentes abiertos y ninguno con su lugar claro.' }, +]; -// 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`, one section down, in the client's own -// first-person words, 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: 'Tu 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: 'Tu operación se volvió más compleja que tu negocio.', - respuesta: 'Separo lo que solo tú puedes decidir de lo que la empresa debería resolver sin ti.', - }, - { - name: 'Expandirse', - situacion: 'Vas a mercados nuevos con 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: 'Tienes 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.', - }, - ], -}; - -// Slide 3 of the same deck. The four quotes are the client's own words — -// keep them verbatim, in first person. -export const dolores = { - eyebrow: 'lo que realmente sientes', - title: 'Los dolores detrás del punto de inflexión', - quotes: [ +export const dondeEstas = { + numero: '02', + eyebrow: 'dónde estás', + title: 'Tu negocio llegó a un punto de inflexión', + sumario: 'Seis situaciones. Reconoce la tuya.', + puntos: PUNTOS, + etiquetaDolores: 'Y por dentro suena así', + dolores: [ 'Trabajo más y gano casi lo mismo que hace dos años.', 'Tengo varios proyectos y no sé en cuál enfocar mi tiempo y mi dinero.', 'Mi empresa no puede crecer si yo no estoy en cada decisión.', 'Lo que me trajo hasta aquí ya no me sirve para el siguiente nivel.', ], - // Split so the resolution renders in gold. remate: { before: 'Esto no se resuelve con más esfuerzo.', accent: 'Se resuelve con una estrategia clara.', }, }; -export const paraQuien = { +// ── 03 · Qué cambia ──────────────────────────────────────────────────────── +export const queCambia = { + numero: '03', + eyebrow: 'qué cambia', + ancla: { + before: 'Las empresas no dejan de crecer por falta de talento;', + accent: 'dejan de crecer por falta de claridad.', + }, + // El "después": los dolores nombran el problema; esto pinta el resultado. + // Paga directo el tercer dolor — "Mi empresa no puede crecer si yo no estoy + // en cada decisión". + despues: 'Sabes exactamente dónde invertir, tu equipo ejecuta sin que estés encima, y cada decisión tiene un plan detrás.', + insight: 'No vendemos marketing. Vendemos claridad estratégica.', + // Las tres disciplinas, degradadas de tres tarjetas con icono a una línea de + // texto: el propio deck dice que son los medios, no la promesa. + medios: 'Comunicación, digital y experiencias son las herramientas que usamos. No son lo que vendemos.', +}; + +// ── 04 · El trabajo ──────────────────────────────────────────────────────── +export const elTrabajo = { + numero: '04', + eyebrow: 'el trabajo', + // Hormozi: Name your mechanism. A named process is a system; an unnamed + // process is a list. Systems sell. Lists don't. + metodo: 'Ruta de Claridad', + title: 'La Ruta de Claridad: de tu operación de hoy a tu plan a un año', + sumario: 'Ocho etapas. Cada una deja algo por escrito.', + fases: ['Inmersión', 'Desafiar supuestos', 'Estrategia ejecutable'], + etiquetaEntregables: 'Entregables', + etapas: [ + { n: '01', name: 'Análisis de compañía y FODA', entregables: ['FODA', 'Diagnóstico ejecutivo', 'Prioridades', 'Quick wins'] }, + { n: '02', name: 'Análisis interno', entregables: ['Mapa de capacidades', 'Restricciones del negocio', 'Oportunidades de crecimiento'] }, + { n: '03', name: 'Conocimiento del consumidor', entregables: ['Segmentación', 'Buyer persona', 'Customer journey', 'Necesidades no cubiertas'] }, + { n: '04', name: 'Estrategia de marca', entregables: ['Posicionamiento', 'Arquitectura de marcas', 'Manual estratégico'] }, + { n: '05', name: 'Comunicación y branding', entregables: ['Plataforma de comunicación', 'Campaña anual', 'Estrategia de medios', 'Plan de contenido'] }, + { n: '06', name: 'Marketing y ventas', entregables: ['Dashboard CEO', 'Dashboard comercial', 'Comité de crecimiento'] }, + { n: '07', name: 'Innovación comercial', entregables: ['Roadmap de innovación', 'Go to market', 'Prioridades de innovación'] }, + { n: '08', name: 'One Year Plan', entregables: ['Roadmap a 12 meses', 'Iniciativas prioritarias', 'Gobernanza del crecimiento'], remate: true }, + ], +}; + +// ── 05 · Para quién · Quién ──────────────────────────────────────────────── +export const paraQuienQuien = { + numero: '05', + eyebrow: 'para quién', 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', - 'quieres profesionalizar sin perder tu esencia', - ], - // Deck slide 9, "NO ES PARA". Kept impersonal on purpose: this disqualifies - // a profile, it does not accuse the reader. - leadNo: 'Esto no es para ti si…', - criteriaNo: [ - 'el negocio está en fase inicial o sin tracción', - 'buscas una solución rápida de venta', - 'buscas un curso, no una transformación', - 'la empresa está en crisis de supervivencia, no en transición', - ], + si: { + lead: 'Esto es para ti si…', + criterios: [ + 'tienes tracción y ya demostraste que sabes crecer', + 'trabajar más ya no rinde como antes', + 'necesitas claridad para decidir el siguiente movimiento', + 'quieres profesionalizar sin perder tu esencia', + ], + }, + no: { + lead: 'Esto no es para ti si…', + criterios: [ + 'el negocio está en fase inicial o sin tracción', + 'buscas una solución rápida, no un cambio de fondo', + 'buscas un curso, no una transformación', + 'la empresa está en crisis de supervivencia, no en transición', + ], + }, + quien: { + eyebrow: 'quiénes estamos detrás', + title: 'Quiénes somos', + cifra: '25+', + cifraPie: 'años construyendo marcas', + bio: '25 años de estrategia detrás de marcas que pasaron de locales a líderes de categoría. Hemos acompañado a más de 40 empresas en retail, servicios y manufactura a encontrar su siguiente movimiento.', // TODO dueña: confirmar cifra y lista de industrias + quote: 'Tu negocio ya demostró que funciona. Ahora necesita un plan que lo lleve más lejos.', + }, }; -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.' }, +// ── 06 · Empezar ─────────────────────────────────────────────────────────── +export const empezar = { + numero: '06', + eyebrow: 'empezar', + title: 'Tu siguiente movimiento empieza con una conversación.', + pasos: [ + { n: '01', name: 'Escríbenos', desc: 'Cuéntanos dónde está tu negocio hoy.' }, + { n: '02', name: 'Sesión de claridad', desc: 'Una conversación para ubicar tu punto de inflexión.' }, + { n: '03', name: 'Plan en mano', desc: 'Sales con claridad y tu siguiente movimiento definido.' }, ], + cta: 'Agenda tu sesión de claridad', + whatsappText: 'Hola, quiero agendar una sesión de claridad', + quote: 'Devuélvele claridad al rumbo de tu empresa.', + contacto: { + whatsapp: { label: 'WhatsApp', href: 'https://wa.me/525558185802' }, + email: { label: 'Email', href: 'mailto:jazmin.rodgon@gmail.com' }, + linkedin: { label: 'LinkedIn', href: 'https://www.linkedin.com/in/jazminrodriguez-jazinta' }, + }, + legal: '© 2026 creeer', }; -// Practical details of the engagement — what the reader can expect when -// working with creeer. Sits between Mecanismo (the high-level HOW) and -// Disciplinas (the tools). All copy derived from approved sources: -// mecanismo steps, puntosInflexion respuestas, comoEmpezar. -// ⚠️ «Duración» left deliberately open — the dueña hasn't specified a -// timeline. The card invites a conversation instead of committing a number. -export const formaTrabajar = { - eyebrow: 'qué esperar', - title: 'Así es trabajar conmigo', - items: [ - { - name: 'Metodología', - desc: 'Inmersión en tu empresa, desafío de lo que todos dan por hecho y construcción conjunta de tu estrategia.', - }, - { - name: 'Quién participa', - desc: 'Tú y yo. Sin intermediarios. Trabajo directo con quien toma las decisiones.', - }, - { - name: 'De dónde partimos', - desc: 'Tu operación real: números, procesos, equipo. Me sumerjo hasta comprenderla desde dentro.', - }, - { - name: 'Qué te llevas', - desc: 'Un diagnóstico claro, tus prioridades ordenadas y una estrategia ejecutable. Por escrito, no en tu cabeza.', - }, - { - name: 'Duración', - desc: 'Cada proceso tiene su ritmo. Lo definimos juntos en la sesión de diagnóstico.', - }, - ], +// ── Alias de compatibilidad ──────────────────────────────────────────────── +// Los consumen las diez secciones viejas mientras conviven con las seis +// nuevas. Derivan de los objetos de arriba, nunca duplican sus cadenas, y se +// van enteros en la limpieza final. No añadir usos nuevos. + +export const hero = { + eyebrow: `— ${apertura.eyebrow} —`, + headline: { + line1: apertura.headline.line1, + effort: apertura.headline.acento, + line3: apertura.headline.line3, + clarity: apertura.headline.cierre, + }, + cta: apertura.cta, + ctaSecundario: apertura.ctaSecundario, +}; + +// La voz de creeer en el recorrido que sigue montado: cada situación del lector +// recibe su respuesta. El rediseño las retira junto con el recorrido (ver +// `dondeEstas`), pero mientras la sección vieja siga en la página, pregunta y +// respuesta no pueden decir lo mismo. Son las del 31/jul pasadas al plural; +// 01 y 05 las improvisó la dueña en esa llamada. Mismo orden que PUNTOS. +const RESPUESTAS = [ + 'Nos sumergimos hasta entender tu empresa desde dentro. Sales sabiendo por dónde, no intentándolo por todos lados a la vez.', + 'Ordenamos lo que ya existe antes de ponerle más peso encima. Escalas sobre algo que aguanta.', + 'Cuestionamos lo que todos dan por hecho en tu operación. El margen no siempre está en vender más.', + 'Separamos lo que solo tú puedes decidir de lo que la empresa debería resolver sin ti.', + 'Revisamos tu proceso y ubicamos la oportunidad real antes de mover una ficha. Expandirte deja de ser una apuesta.', + 'Construimos, juntos, el criterio para decidir qué sigue y qué se deja ir. Por escrito, no en tu cabeza.', +]; + +export const puntosInflexion = { + eyebrow: 'el contexto', + title: dondeEstas.title, + lead: '¿Te reconoces?', + items: PUNTOS.map((p, i) => ({ ...p, respuesta: RESPUESTAS[i] })), +}; + +export const dolores = { + eyebrow: 'lo que realmente sientes', + title: 'Los dolores detrás del punto de inflexión', + quotes: dondeEstas.dolores, + remate: dondeEstas.remate, }; export const disciplinas = { eyebrow: 'las herramientas', - title: 'Las disciplinas que uso', + title: 'Las disciplinas que usamos', lead: 'No son la promesa — son los medios.', items: [ { name: 'Comunicación', desc: 'Mensaje que alinea a tu equipo y a tu mercado.' }, @@ -173,37 +212,48 @@ export const disciplinas = { ], }; +export const proceso = { + eyebrow: 'el trabajo completo', + title: elTrabajo.title, + lead: elTrabajo.sumario, + fases: elTrabajo.fases, + etiquetaEntregables: elTrabajo.etiquetaEntregables, + etapas: elTrabajo.etapas.map(({ n, name, entregables, remate }) => + remate ? { n, name, entregables, destacada: true } : { n, name, entregables }, + ), +}; + +export const paraQuien = { + title: paraQuienQuien.title, + lead: paraQuienQuien.si.lead, + criteria: paraQuienQuien.si.criterios, + leadNo: paraQuienQuien.no.lead, + criteriaNo: paraQuienQuien.no.criterios, +}; + export const estratega = { - // Split so the brand name keeps its lowercase inside an uppercase eyebrow — - // CSS text-transform would otherwise render it as CREEER. - eyebrow: { before: 'la mente detrás de', brand: 'creeer' }, - title: 'Sobre la estratega', - bio: 'Más de 30 años construyendo marcas. Hoy acompaño a empresarios que ya lograron tracción y necesitan claridad para su siguiente movimiento.', - quote: 'El tiempo no se compra, se invierte.', + eyebrow: { before: 'quiénes estamos detrás de', brand: 'creeer' }, + title: paraQuienQuien.quien.title, + bio: paraQuienQuien.quien.bio, + quote: paraQuienQuien.quien.quote, }; export const comoEmpezar = { eyebrow: 'cómo empezar', - title: 'Tu siguiente movimiento empieza con una conversación.', - steps: [ - { n: '01', name: 'Escríbeme', desc: 'Cuéntame dónde está tu negocio hoy.' }, - { n: '02', name: 'Sesión de diagnóstico', desc: 'Una conversación para ubicar tu punto de inflexión.' }, - { n: '03', name: 'Plan en mano', desc: 'Sales con claridad y tu siguiente movimiento definido.' }, - ], - cta: 'Agenda tu sesión de diagnóstico', - whatsappText: 'Hola, quiero agendar una sesión de diagnóstico', + title: empezar.title, + steps: empezar.pasos, + cta: empezar.cta, + whatsappText: empezar.whatsappText, }; 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 - }, + quote: empezar.quote, + cta: apertura.cta, + contact: empezar.contacto, }; +export const fraseAncla = { quote: queCambia.ancla, despues: queCambia.despues }; + export const header = { cta: 'Hablemos', // short nav CTA → #contacto (the full CTA lives in Hero/Cierre) }; @@ -219,12 +269,24 @@ export const footer = { // image was modified — all of them get the navy/gold duotone treatment, and the // six in `recorrido` are additionally cropped and resized. export const creditosFotos = [ - { desc: 'Foto: Golden Gate Bridge — Dietmar Rabich', href: 'https://commons.wikimedia.org/wiki/File:San_Francisco_(CA,_USA),_Golden_Gate_Bridge_--_2022_--_3023_(bw).jpg', licencia: 'CC BY-SA 4.0', licenciaHref: 'https://creativecommons.org/licenses/by-sa/4.0/' }, { desc: 'Foto: amanecer, Monte Nardi — Tony 1212', href: 'https://commons.wikimedia.org/wiki/File:Sunrise_over_the_summit_of_Mount_Nardi_from_the_south.jpg', licencia: 'CC BY-SA 4.0', licenciaHref: 'https://creativecommons.org/licenses/by-sa/4.0/' }, - { desc: 'Foto: distrito financiero de Riad — B.alotaby', href: 'https://commons.wikimedia.org/wiki/File:Riyadh_Skyline_showing_the_King_Abdullah_Financial_District_(KAFD)_and_the_famous_Kingdom_Tower_.jpg', licencia: 'CC BY-SA 4.0', licenciaHref: 'https://creativecommons.org/licenses/by-sa/4.0/' }, - { desc: 'Foto: sala de reuniones — Breather', href: 'https://commons.wikimedia.org/wiki/File:Minimalist_meeting_room_(Unsplash).jpg', licencia: 'CC0', licenciaHref: 'https://creativecommons.org/publicdomain/zero/1.0/' }, - { desc: 'Foto: cosechadora en el trigo — Michael Gäbler', href: 'https://commons.wikimedia.org/wiki/File:The_combine_Claas_Lexion_584_in_the_wheat_harvest.jpg', licencia: 'CC BY-SA 3.0', licenciaHref: 'https://creativecommons.org/licenses/by-sa/3.0/' }, - { desc: 'Foto: pantalla de la Bolsa de Fráncfort — Ank Kumar', href: 'https://commons.wikimedia.org/wiki/File:Frankfurt_Stock_Exchange_(Ank_Kumar)_04.jpg', licencia: 'CC BY-SA 4.0', licenciaHref: 'https://creativecommons.org/licenses/by-sa/4.0/' }, - { desc: 'Foto: panel de salidas, aeropuerto de Ginebra — Tiia Monto', href: 'https://commons.wikimedia.org/wiki/File:Departure_board_at_Geneva_Airport.jpg', licencia: 'CC BY-SA 3.0', licenciaHref: 'https://creativecommons.org/licenses/by-sa/3.0/' }, - { desc: 'Foto: distrito financiero de Boston — Ingfbruno', href: 'https://commons.wikimedia.org/wiki/File:USA-Financial_District.JPG', licencia: 'CC BY-SA 3.0', licenciaHref: 'https://creativecommons.org/licenses/by-sa/3.0/' }, + { desc: 'Foto: Torre Mitikah en construcción, Ciudad de México — ProtoplasmaKid', href: 'https://commons.wikimedia.org/wiki/File:Torre_Mitikah_en_construcci%C3%B3n.jpg', licencia: 'CC BY-SA 4.0', licenciaHref: 'https://creativecommons.org/licenses/by-sa/4.0/' }, + { desc: 'Foto: oficina — Importaciones a México', href: 'https://commons.wikimedia.org/wiki/File:Importaciones_a_M%C3%A9xico_oficina.jpg', licencia: 'CC BY-SA 4.0', licenciaHref: 'https://creativecommons.org/licenses/by-sa/4.0/' }, + { desc: 'Foto: piñas de agave, Tequila, Jalisco — Gzzz', href: 'https://commons.wikimedia.org/wiki/File:Hearts_of_agave_plants_Tequila_Mexico.jpg', licencia: 'CC BY-SA 4.0', licenciaHref: 'https://creativecommons.org/licenses/by-sa/4.0/' }, + { desc: 'Foto: Distribuidor Vial San Antonio, Ciudad de México — ProtoplasmaKid', href: 'https://commons.wikimedia.org/wiki/File:Distribuidor_Vial_San_Antonio_2025_32.jpg', licencia: 'CC BY-SA 4.0', licenciaHref: 'https://creativecommons.org/licenses/by-sa/4.0/' }, + { desc: 'Foto: puertas de embarque, aeropuerto de Cancún — Vmzp85', href: 'https://commons.wikimedia.org/wiki/File:MMUN006.jpg', licencia: 'CC BY-SA 4.0', licenciaHref: 'https://creativecommons.org/licenses/by-sa/4.0/' }, + { desc: 'Foto: skyline de Monterrey — Daniel Escobedo', href: 'https://commons.wikimedia.org/wiki/File:Skyline_de_Monterrey.jpg', licencia: 'CC BY-SA 4.0', licenciaHref: 'https://creativecommons.org/licenses/by-sa/4.0/' }, + + // Las ocho de `Proceso`, todas CC0 — ninguna obliga a atribuir, pero se + // acreditan igual por coherencia con el resto de la lista. `archivo` ata cada + // crédito a su imagen: hay un test en Proceso que falla si aparece una foto + // en la sección sin entrada aquí. + { archivo: '/images/proceso/01.jpg', desc: 'Foto: sala de juntas — Benjamin Child', href: 'https://commons.wikimedia.org/wiki/File:Benjamin_Child_2015_(Unsplash).jpg', licencia: 'CC0', licenciaHref: 'https://creativecommons.org/publicdomain/zero/1.0/' }, + { archivo: '/images/proceso/02.jpg', desc: 'Foto: taller — Adi Goldstein', href: 'https://commons.wikimedia.org/wiki/File:The_Workshop_(Unsplash).jpg', licencia: 'CC0', licenciaHref: 'https://creativecommons.org/publicdomain/zero/1.0/' }, + { archivo: '/images/proceso/03.jpg', desc: 'Foto: escaleras concurridas — José Martín Ramírez C', href: 'https://commons.wikimedia.org/wiki/File:Busy_stairs_in_large_building_(Unsplash).jpg', licencia: 'CC0', licenciaHref: 'https://creativecommons.org/publicdomain/zero/1.0/' }, + { archivo: '/images/proceso/04.jpg', desc: 'Foto: tipografía sobre escritorio — rawpixel.com', href: 'https://commons.wikimedia.org/wiki/File:Typography,_coffee_and_roses_(Unsplash).jpg', licencia: 'CC0', licenciaHref: 'https://creativecommons.org/publicdomain/zero/1.0/' }, + { archivo: '/images/proceso/05.jpg', desc: 'Foto: estudio fotográfico — Alexander Dummer', href: 'https://commons.wikimedia.org/wiki/File:Photographic_studio_(Unsplash).jpg', licencia: 'CC0', licenciaHref: 'https://creativecommons.org/publicdomain/zero/1.0/' }, + { archivo: '/images/proceso/06.jpg', desc: 'Foto: tienda de discos — Samuel Dixon', href: 'https://commons.wikimedia.org/wiki/File:Vintage_record_store_(Unsplash).jpg', licencia: 'CC0', licenciaHref: 'https://creativecommons.org/publicdomain/zero/1.0/' }, + { archivo: '/images/proceso/07.jpg', desc: 'Foto: dispositivos sobre la mesa — Alejandro Escamilla', href: 'https://commons.wikimedia.org/wiki/File:Iphone,_macbook_and_smartphone_on_a_table_(Unsplash).jpg', licencia: 'CC0', licenciaHref: 'https://creativecommons.org/publicdomain/zero/1.0/' }, + { archivo: '/images/proceso/08.jpg', desc: 'Foto: planificador mensual — Eric Rothermel', href: 'https://commons.wikimedia.org/wiki/File:Monthly_schedule_(Unsplash).jpg', licencia: 'CC0', licenciaHref: 'https://creativecommons.org/publicdomain/zero/1.0/' }, ]; diff --git a/lib/enlaces.ts b/lib/enlaces.ts new file mode 100644 index 0000000..c997ac7 --- /dev/null +++ b/lib/enlaces.ts @@ -0,0 +1,19 @@ +// Links that leave the site open in a new tab, so the reader never loses the +// page: WhatsApp, LinkedIn, the photo credits. Everything else stays put — +// in-page anchors (#contacto) obviously, and mailto: too, which hands off to +// the mail client and would only leave an empty tab behind. +// +// `rel="noopener noreferrer"`: without noopener the opened page gets a handle +// on this one through window.opener. + +export function esExterno(href: string): boolean { + return /^https?:\/\//i.test(href); +} + +export function propsEnlace(href: string) { + return esExterno(href) ? ({ target: '_blank', rel: 'noopener noreferrer' } as const) : {}; +} + +// Screen-reader users get told before the context switch happens (WCAG G201); +// sighted readers see the new tab open. +export const AVISO_PESTANA = 'se abre en otra pestaña'; diff --git a/lib/motion.ts b/lib/motion.ts index b44978c..aa45ac3 100644 --- a/lib/motion.ts +++ b/lib/motion.ts @@ -64,7 +64,7 @@ export const motionProps = (reduced: boolean) => ? { initial: 'visible' as const, animate: 'visible' as const, whileInView: undefined, viewport: undefined } : {}; -// Cycles through `count` steps on its own (Mecanismo, ComoEmpezar): each +// Cycles through `count` steps on its own (ComoEmpezar): each // manual change restarts the dwell timer, hovering pauses it, and it never // advances at all for reduced-motion users. export function useAutoStep(count: number, intervalMs = 3500) { diff --git a/package.json b/package.json index 7aad0a2..21b97bf 100644 --- a/package.json +++ b/package.json @@ -11,12 +11,14 @@ "test:watch": "vitest" }, "dependencies": { + "@react-three/fiber": "^8.18.0", "embla-carousel-autoplay": "^8.6.0", "embla-carousel-react": "^8.6.0", "framer-motion": "^11.18.2", "next": "14.2.35", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "three": "^0.185.1" }, "devDependencies": { "@testing-library/jest-dom": "^6.9.1", @@ -25,6 +27,7 @@ "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "@types/three": "^0.185.4", "@vitejs/plugin-react": "^4.7.0", "eslint": "^8.57.1", "eslint-config-next": "^14.2.35", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31feabd..bce4caa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@react-three/fiber': + specifier: ^8.18.0 + version: 8.18.0(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.185.1) embla-carousel-autoplay: specifier: ^8.6.0 version: 8.6.0(embla-carousel@8.6.0) @@ -26,6 +29,9 @@ importers: react-dom: specifier: ^18 version: 18.3.1(react@18.3.1) + three: + specifier: ^0.185.1 + version: 0.185.1 devDependencies: '@testing-library/jest-dom': specifier: ^6.9.1 @@ -45,6 +51,9 @@ importers: '@types/react-dom': specifier: ^18 version: 18.3.7(@types/react@18.3.31) + '@types/three': + specifier: ^0.185.4 + version: 0.185.4 '@vitejs/plugin-react': specifier: ^4.7.0 version: 4.7.0(vite@5.4.21(@types/node@20.19.43)) @@ -197,6 +206,9 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} + '@dimforge/rapier3d-compat@0.12.0': + resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} + '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -489,6 +501,31 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@react-three/fiber@8.18.0': + resolution: {integrity: sha512-FYZZqD0UUHUswKz3LQl2Z7H24AhD14XGTsIRw3SJaXUxyfVMi+1yiZGmqTcPt/CkPpdU7rrxqcyQ1zJE5DjvIQ==} + peerDependencies: + expo: '>=43.0' + expo-asset: '>=8.4' + expo-file-system: '>=11.0' + expo-gl: '>=11.0' + react: '>=18 <19' + react-dom: '>=18 <19' + react-native: '>=0.64' + three: '>=0.133' + peerDependenciesMeta: + expo: + optional: true + expo-asset: + optional: true + expo-file-system: + optional: true + expo-gl: + optional: true + react-dom: + optional: true + react-native: + optional: true + '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -673,6 +710,9 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@tweenjs/tween.js@23.1.3': + resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} + '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} @@ -708,9 +748,26 @@ packages: peerDependencies: '@types/react': ^18.0.0 + '@types/react-reconciler@0.26.7': + resolution: {integrity: sha512-mBDYl8x+oyPX/VBb3E638N0B7xG+SPk/EAMcVPeexqus/5aTpTphQi0curhhshOqRrc9t6OPoJfEUkbymse/lQ==} + + '@types/react-reconciler@0.28.9': + resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==} + peerDependencies: + '@types/react': '*' + '@types/react@18.3.31': resolution: {integrity: sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==} + '@types/stats.js@0.17.4': + resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} + + '@types/three@0.185.4': + resolution: {integrity: sha512-gAsBIC07NIFrxjbf7tH2t71c38uulFfk/RFoC7FNBSjMRAQ8J1x/RBvusX0N5PJouaYFJawXQqfCQ0RKUx/1nA==} + + '@types/webxr@0.5.24': + resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==} + '@typescript-eslint/eslint-plugin@8.65.0': resolution: {integrity: sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1039,6 +1096,9 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.10.44: resolution: {integrity: sha512-T3ghW+sl/ZJ8w1v/yQx3qvJ9040DWoLBz8JT/CILbAKcFyG9b2MRe75v6W5uXjv6uH1lumK2Kv46y2zSkcej0Q==} engines: {node: '>=6.0.0'} @@ -1067,6 +1127,9 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -1476,6 +1539,9 @@ packages: picomatch: optional: true + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -1652,6 +1718,9 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1819,6 +1888,11 @@ packages: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} + its-fine@1.2.5: + resolution: {integrity: sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==} + peerDependencies: + react: '>=18.0' + jackspeak@2.3.6: resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} engines: {node: '>=14'} @@ -1936,6 +2010,9 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + meshoptimizer@1.1.1: + resolution: {integrity: sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -2254,10 +2331,25 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-reconciler@0.27.0: + resolution: {integrity: sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==} + engines: {node: '>=0.10.0'} + peerDependencies: + react: ^18.0.0 + react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} + react-use-measure@2.1.7: + resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==} + peerDependencies: + react: '>=16.13' + react-dom: '>=16.13' + peerDependenciesMeta: + react-dom: + optional: true + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -2340,6 +2432,9 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} + scheduler@0.21.0: + resolution: {integrity: sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==} + scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -2500,6 +2595,11 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + suspend-react@0.1.3: + resolution: {integrity: sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==} + peerDependencies: + react: '>=17.0' + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -2518,6 +2618,9 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + three@0.185.1: + resolution: {integrity: sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -2778,6 +2881,15 @@ packages: resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} + zustand@3.7.2: + resolution: {integrity: sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==} + engines: {node: '>=12.7.0'} + peerDependencies: + react: '>=16.8' + peerDependenciesMeta: + react: + optional: true + snapshots: '@adobe/css-tools@4.5.0': {} @@ -2926,6 +3038,8 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} + '@dimforge/rapier3d-compat@0.12.0': {} + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -3135,6 +3249,26 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@react-three/fiber@8.18.0(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.185.1)': + dependencies: + '@babel/runtime': 7.29.7 + '@types/react-reconciler': 0.26.7 + '@types/webxr': 0.5.24 + base64-js: 1.5.1 + buffer: 6.0.3 + its-fine: 1.2.5(@types/react@18.3.31)(react@18.3.1) + react: 18.3.1 + react-reconciler: 0.27.0(react@18.3.1) + react-use-measure: 2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + scheduler: 0.21.0 + suspend-react: 0.1.3(react@18.3.1) + three: 0.185.1 + zustand: 3.7.2(react@18.3.1) + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/rollup-android-arm-eabi@4.62.2': @@ -3260,6 +3394,8 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 + '@tweenjs/tween.js@23.1.3': {} + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 @@ -3302,11 +3438,32 @@ snapshots: dependencies: '@types/react': 18.3.31 + '@types/react-reconciler@0.26.7': + dependencies: + '@types/react': 18.3.31 + + '@types/react-reconciler@0.28.9(@types/react@18.3.31)': + dependencies: + '@types/react': 18.3.31 + '@types/react@18.3.31': dependencies: '@types/prop-types': 15.7.15 csstype: 3.2.3 + '@types/stats.js@0.17.4': {} + + '@types/three@0.185.4': + dependencies: + '@dimforge/rapier3d-compat': 0.12.0 + '@tweenjs/tween.js': 23.1.3 + '@types/stats.js': 0.17.4 + '@types/webxr': 0.5.24 + fflate: 0.8.3 + meshoptimizer: 1.1.1 + + '@types/webxr@0.5.24': {} + '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -3646,6 +3803,8 @@ snapshots: balanced-match@4.0.4: {} + base64-js@1.5.1: {} + baseline-browser-mapping@2.10.44: {} binary-extensions@2.3.0: {} @@ -3675,6 +3834,11 @@ snapshots: node-releases: 2.0.51 update-browserslist-db: 1.2.3(browserslist@4.28.6) + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + busboy@1.6.0: dependencies: streamsearch: 1.1.0 @@ -4256,6 +4420,8 @@ snapshots: optionalDependencies: picomatch: 4.0.5 + fflate@0.8.3: {} + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 @@ -4446,6 +4612,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + ieee754@1.2.1: {} + ignore@5.3.2: {} ignore@7.0.6: {} @@ -4613,6 +4781,13 @@ snapshots: has-symbols: 1.1.0 set-function-name: 2.0.2 + its-fine@1.2.5(@types/react@18.3.31)(react@18.3.1): + dependencies: + '@types/react-reconciler': 0.28.9(@types/react@18.3.31) + react: 18.3.1 + transitivePeerDependencies: + - '@types/react' + jackspeak@2.3.6: dependencies: '@isaacs/cliui': 8.0.2 @@ -4734,6 +4909,8 @@ snapshots: merge2@1.4.1: {} + meshoptimizer@1.1.1: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -5039,8 +5216,20 @@ snapshots: react-is@18.3.1: {} + react-reconciler@0.27.0(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.21.0 + react-refresh@0.17.0: {} + react-use-measure@2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + react@18.3.1: dependencies: loose-envify: 1.4.0 @@ -5168,6 +5357,10 @@ snapshots: dependencies: xmlchars: 2.2.0 + scheduler@0.21.0: + dependencies: + loose-envify: 1.4.0 + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -5359,6 +5552,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + suspend-react@0.1.3(react@18.3.1): + dependencies: + react: 18.3.1 + symbol-tree@3.2.4: {} tailwindcss@3.4.19: @@ -5399,6 +5596,8 @@ snapshots: dependencies: any-promise: 1.3.0 + three@0.185.1: {} + tinybench@2.9.0: {} tinyglobby@0.2.17: @@ -5692,3 +5891,7 @@ snapshots: yocto-queue@0.1.0: {} yocto-queue@1.2.2: {} + + zustand@3.7.2(react@18.3.1): + optionalDependencies: + react: 18.3.1 diff --git a/public/images/mecanismo-bg.jpg b/public/images/mecanismo-bg.jpg deleted file mode 100755 index 72941e4..0000000 Binary files a/public/images/mecanismo-bg.jpg and /dev/null differ diff --git a/public/images/proceso/01.jpg b/public/images/proceso/01.jpg new file mode 100644 index 0000000..3dd61ea Binary files /dev/null and b/public/images/proceso/01.jpg differ diff --git a/public/images/proceso/02.jpg b/public/images/proceso/02.jpg new file mode 100644 index 0000000..edc5ea6 Binary files /dev/null and b/public/images/proceso/02.jpg differ diff --git a/public/images/proceso/03.jpg b/public/images/proceso/03.jpg new file mode 100644 index 0000000..2910b62 Binary files /dev/null and b/public/images/proceso/03.jpg differ diff --git a/public/images/proceso/04.jpg b/public/images/proceso/04.jpg new file mode 100644 index 0000000..c1a5508 Binary files /dev/null and b/public/images/proceso/04.jpg differ diff --git a/public/images/proceso/05.jpg b/public/images/proceso/05.jpg new file mode 100644 index 0000000..58043a7 Binary files /dev/null and b/public/images/proceso/05.jpg differ diff --git a/public/images/proceso/06.jpg b/public/images/proceso/06.jpg new file mode 100644 index 0000000..097e393 Binary files /dev/null and b/public/images/proceso/06.jpg differ diff --git a/public/images/proceso/07.jpg b/public/images/proceso/07.jpg new file mode 100644 index 0000000..8373c89 Binary files /dev/null and b/public/images/proceso/07.jpg differ diff --git a/public/images/proceso/08.jpg b/public/images/proceso/08.jpg new file mode 100644 index 0000000..7939718 Binary files /dev/null and b/public/images/proceso/08.jpg differ diff --git a/tailwind.config.ts b/tailwind.config.ts index 0c9f423..74446d8 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -10,6 +10,14 @@ const config: Config = { 'azul-profundo': '#0A1D3D', // Pantone 2965 C — texto, fondos oscuros, CTAs 'dorado': { DEFAULT: '#C4942E', // Pantone 872 C — acentos, títulos, símbolo ≡ + // El dorado del manual sobre fondo claro es un problema de lectura, + // no de gusto: 2,76:1 sobre blanco y 2,46:1 sobre gris-claro, cuando + // AA pide 4,5:1 para texto pequeño. Sobre azul profundo da 6,07:1 y + // ahí no se toca. Éste es el mismo tono bajado hasta 5,3:1 sobre + // blanco y 4,7:1 sobre gris-claro, y existe para una sola cosa: + // TEXTO PEQUEÑO SOBRE FONDO CLARO. Rellenos, reglas, iconos, el ≡ y + // todo lo que vaya sobre navy siguen usando el DEFAULT. + texto: '#8A6520', }, // Colores neutros 'azul-claro': '#C8EEF5', // fondos alternativos, secciones suaves @@ -24,13 +32,49 @@ const config: Config = { // (mismo diseñador: Steve Matteson). Cero autoalojamiento, cero problemas de licencia. sans: ['var(--font-open-sans)', 'system-ui', 'sans-serif'], }, + // Profundidad. Un color plano de fondo se lee como bloque impreso: no + // tiene fuente de luz y todas las secciones pesan igual. Cada uno de + // estos degradados levanta un punto y hunde los bordes, así que la + // página gana relieve sin salirse de la paleta del manual. + // + // Van siempre acompañados de su color sólido (`bg-azul-profundo + // bg-hondo-navy`): el color pinta `background-color` y el degradado + // `background-image`, de modo que si el segundo no carga queda el fondo + // correcto y no un hueco blanco. + backgroundImage: { + 'hondo-navy': 'radial-gradient(125% 95% at 12% 0%, #18345F 0%, #0A1D3D 52%, #061530 100%)', + 'hondo-claro': 'radial-gradient(120% 100% at 85% 0%, #FFFFFF 0%, #F2F2F2 58%, #E7E9EC 100%)', + 'hondo-blanco': 'radial-gradient(110% 90% at 18% 0%, #FFFFFF 0%, #FBFBFC 55%, #F0F2F5 100%)', + // Dorado como superficie, no como muestra de color: más claro arriba, + // como si le diera la luz. + 'dorado-metal': 'linear-gradient(145deg, #E2B75C 0%, #C4942E 46%, #9C7222 100%)', + // Halo para soltar sobre las secciones oscuras. + 'velo-dorado': 'radial-gradient(50% 50% at 50% 50%, rgba(196,148,46,0.20) 0%, rgba(196,148,46,0) 70%)', + 'velo-frio': 'radial-gradient(50% 50% at 50% 50%, rgba(200,238,245,0.14) 0%, rgba(200,238,245,0) 70%)', + }, + boxShadow: { + // Dos sombras en vez de una: un contacto corto y cerrado que apoya la + // tarjeta en la página, y una difusa y lejana que le da aire. Una sola + // sombra siempre parece calcomanía. + tarjeta: '0 1px 2px rgba(10,29,61,0.06), 0 12px 32px -12px rgba(10,29,61,0.14)', + 'tarjeta-alta': '0 2px 4px rgba(10,29,61,0.08), 0 28px 60px -20px rgba(10,29,61,0.28)', + }, maxWidth: { - content: '1152px', + content: '1280px', + // Medida de texto corrido. La tensión entre reglas anchas (1280px) y + // texto angosto (68ch) es lo que hace que la composición se lea + // editorial en vez de como un bloque de landing. + medida: '68ch', }, fontSize: { - // Two title roles, so a brand statement never looks like a section - // heading. Fluid so they hold from 360px to desktop without a stack of - // breakpoint overrides. + // Cinco roles. `display` se gasta UNA sola vez en toda la página. + display: ['clamp(3.5rem, 9vw, 7rem)', { lineHeight: '0.94', letterSpacing: '-0.03em' }], + titulo: ['clamp(2.25rem, 4.5vw, 3.5rem)', { lineHeight: '1', letterSpacing: '-0.02em' }], + sumario: ['clamp(1.375rem, 2vw, 1.75rem)', { lineHeight: '1.35', letterSpacing: '-0.01em' }], + cuerpo: ['1.0625rem', { lineHeight: '1.7' }], + rotulo: ['0.75rem', { lineHeight: '1.2', letterSpacing: '0.3em' }], + // Se van con la limpieza de la Tarea 13, cuando ya no queden secciones + // viejas que los usen. ancla: ['clamp(2.5rem, 6.5vw, 4.5rem)', { lineHeight: '0.98', letterSpacing: '-0.025em' }], seccion: ['clamp(2rem, 4vw, 3.25rem)', { lineHeight: '1.02', letterSpacing: '-0.02em' }], },