feat(recorrido): el fotograma es el mensaje, y la respuesta llega en azul

Fuera tarjetas: la situación va a tamaño de titular sobre el fondo claro,
y al bajar el escenario entero se vuelve azul profundo para la respuesta
de creeer, en blanco y del mismo tamaño. No es una tarjeta sobre la
página del lector, es el terreno de creeer.

El rótulo del punto se repite en la respuesta a propósito: como solo hay
una cosa en pantalla, es lo único que la ata a la pregunta que contesta.
El test lo fija con esa razón escrita.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
erikghandClaude Opus 5 committed 2026-08-03 12:26:32 -06:00
1 parent 253ff26fa2
commit 3f65a2a2c3
3 files changed
+68 -48

No files matched your search

+43 -42
View File
@@ -3,19 +3,23 @@
import { motion, useTransform, type MotionValue } from 'framer-motion';
import { Wordmark } from '@/components/primitives/Wordmark';
// One beat of the pinned conversation, staged as two calling cards laid on a
// table: the reader's, in white, and creeer's, in navy, handed back.
// One beat of the pinned conversation. Not a card on a page — the whole frame
// is the message: the reader's situation set large on the light ground, then
// creeer's answer set large on azul-profundo, the section's own background
// turning over with it.
//
// One thing per frame, never both. The two halves share a grid cell so the
// answer takes the stage the question leaves instead of stacking under it, and
// they arrive from opposite sides so the swap reads as a relay.
//
// The cards are driven by STATE, not by scroll position. Mapping opacity
// straight onto scroll made every intermediate position a valid frame, so
// stopping mid-swipe left creeer's card as a half-transparent grey rectangle —
// it read as a rendering fault. Scroll now only decides which beat we are on;
// the cards animate to that beat over time and always land somewhere finished,
// wherever the reader lets go.
// stopping mid-swipe left the answer as a half-transparent slab that read as a
// rendering fault. Scroll now only picks the beat; the frame animates to it
// over time and always lands somewhere finished, wherever the reader lets go.
//
// Every scene stays mounted and stacked in the same place. Mounting only the
// active one would animate more cheaply and would strip five sixths of the
// section out of the static export.
// Every scene stays mounted. Rendering only the active one would animate more
// cheaply and would strip five sixths of the section out of the static export.
const EASE = [0.22, 1, 0.36, 1] as const;
@@ -44,66 +48,63 @@ export function EscenaDialogo({
// The one thing still tied to scroll. A drifting numeral never looks broken
// at any position it can be caught in, so it costs nothing and keeps the
// stage from feeling frozen between beats.
const cifraY = useTransform(progreso, [i * paso - paso, (i + 1) * paso + paso], [60, -60]);
const cifraY = useTransform(progreso, [i * paso - paso, (i + 1) * paso + paso], [70, -70]);
return (
<motion.li
data-activa={activa ? 'true' : 'false'}
animate={{ opacity: activa ? 1 : 0, y: activa ? 0 : 26 }}
transition={{ duration: 0.45, ease: EASE }}
className="absolute inset-x-0 top-1/2 -translate-y-1/2"
animate={{ opacity: activa ? 1 : 0 }}
transition={{ duration: 0.4, ease: EASE }}
className="absolute inset-0 flex items-center"
style={{ pointerEvents: activa ? 'auto' : 'none' }}
>
<motion.span
aria-hidden="true"
style={{ y: cifraY }}
className="pointer-events-none absolute -top-32 right-0 select-none font-sans font-black leading-none text-azul-profundo/[0.045] text-[9rem] md:text-[17rem]"
animate={{ color: respondida ? 'rgba(255,255,255,0.06)' : 'rgba(10,29,61,0.05)' }}
transition={{ duration: 0.5, ease: EASE }}
className="pointer-events-none absolute right-0 top-8 select-none font-sans font-black leading-none text-[11rem] md:text-[22rem]"
>
{n}
</motion.span>
{/* One card per frame — never both. Both live in the same grid cell so
the second can take the stage the first leaves, instead of stacking
under it: the question is asked, it goes, and then the answer is the
only thing on screen. They come in from opposite sides so the swap
reads as one card replacing another, not as one fading into the same
spot. */}
<div className="grid">
<div className="relative grid w-full">
{/* The reader's situation. */}
<motion.div
animate={{
opacity: respondida ? 0 : 1,
y: respondida ? -52 : 0,
rotate: respondida ? -3.4 : -0.8,
scale: respondida ? 0.96 : 1,
}}
animate={{ opacity: respondida ? 0 : 1, y: respondida ? -60 : 0 }}
transition={{ duration: 0.5, ease: EASE }}
style={{ pointerEvents: respondida ? 'none' : 'auto' }}
className="col-start-1 row-start-1 self-center justify-self-start max-w-xl rounded-xl border border-azul-profundo/10 bg-white p-7 shadow-[0_28px_70px_-32px_rgba(10,29,61,0.5)] md:p-9"
className="col-start-1 row-start-1 max-w-5xl"
>
<p className="text-[0.65rem] font-black uppercase tracking-[0.34em] text-dorado">
<p className="text-[0.7rem] font-black uppercase tracking-[0.34em] text-dorado md:text-xs">
{n} · {name}
</p>
<p className="mt-4 font-sans font-black leading-[1.12] text-azul-profundo text-2xl md:text-4xl">
<p className="mt-6 font-sans font-black leading-[0.98] text-azul-profundo text-4xl md:text-6xl lg:text-7xl">
«{situacion}»
</p>
</motion.div>
{/* creeer's card, handed back: navy against the reader's white, tilted
the other way, and coming in from the opposite margin. */}
{/* creeer's answer, on the navy the whole frame turns to. The point
label repeats here on purpose: with the question gone from the
screen, it is the only thing tying the answer to what it answers. */}
<motion.div
animate={{
opacity: respondida ? 1 : 0,
y: respondida ? 0 : 64,
rotate: respondida ? 1.6 : 5,
scale: respondida ? 1 : 0.95,
}}
animate={{ opacity: respondida ? 1 : 0, y: respondida ? 0 : 60 }}
transition={{ duration: 0.55, ease: EASE }}
style={{ pointerEvents: respondida ? 'auto' : 'none' }}
className="col-start-1 row-start-1 self-center justify-self-end max-w-xl rounded-xl bg-azul-profundo p-7 shadow-[0_34px_80px_-30px_rgba(10,29,61,0.75)] md:mr-6 md:p-9"
className="col-start-1 row-start-1 max-w-5xl"
>
<span aria-hidden="true" className="absolute left-0 top-7 h-10 w-[3px] rounded-r bg-dorado md:top-9" />
<Wordmark size="sm" onDark />
<p className="mt-4 text-base leading-relaxed text-white/75 md:text-xl">{respuesta}</p>
<div className="flex items-center gap-4">
<span aria-hidden="true" className="h-[3px] w-10 rounded bg-dorado" />
<p className="text-[0.7rem] font-black uppercase tracking-[0.34em] text-dorado md:text-xs">
{n} · {name}
</p>
</div>
<p className="mt-6 font-sans font-bold leading-[1.06] text-white text-3xl md:text-5xl lg:text-[3.6rem]">
{respuesta}
</p>
<div className="mt-10">
<Wordmark size="sm" onDark />
</div>
</motion.div>
</div>
</motion.li>