El shell aurora, la placa y los tokens ya están construidos y validados. Esto no es un rediseño: es vestir con ellos las siete rutas de auth que faltan del client, resolver de verdad las dos que sí piden diseño —las cajas de OTP y la checklist de contraseña— y llegar hasta el otro lado de la puerta: el sidebar y el topbar a los que se entra. Todo a 1440 y a 390.
AuroraShell, BrandPlate y los tokens --mogos-* ya
viven en packages/ui y corren en local. Las dos puertas de /login
y el regreso de Google están hechos. Lo que falta son siete rutas del client
que hoy siguen siendo un formulario centrado sobre bg-background, sin marca,
con el isotipo de 36×32 flotando arriba a la derecha.
El tono es «puerta», no «formulario»: entrar a Mogos es cruzar un umbral. Por eso estas pantallas heredan el shell en vez de inventar el suyo, y por eso la placa «Importamos futuro» aparece exactamente donde se abre una sesión — una sola vez en las siete.
Las escenas de escritorio miden 920 px y representan un viewport de 1440: lo real es la proporción, no el número. 409 sobre 920 es el 44.5 % que el formulario ocupa a 1440. Las escenas móviles miden 390 px y son 1:1.
Antes de dibujar se leyeron las siete rutas. Cuatro hallazgos cambiaron el encargo, y quedan acá escritos porque el encargo original no los contemplaba.
/verify-otp y /verify-whatsapp-otp maquetan seis
<input> pegados a mano, con
focus:ring-brand-500 — o sea el azul del producto #3765c0, no el
celeste de la marca. /forgot-password usa el InputOTP de
shadcn que ya vive en packages/ui.
El trabajo no es inventar la caja: es elegir una y matar dos. Converge todo en el
InputOTP de packages/ui, retintado.
password-requirements.tsx corre sobre usePasswordValidation y
pinta bg-green-100 / text-green-600 / text-gray-500: tres valores de
plantilla Tailwind fuera de la paleta.
El comportamiento ya era el correcto. Lo que cambia es la tinta —
y el gray-500 a 12 px sobre blanco da ~4.6:1, que no llega a AA en ese
tamaño.
/register son 2 pasos · /activate son 3 estadios ·
/forgot-password son 3 pasos (correo → OTP → contraseña)
— o sea que ya carga OTP y contraseña, algo que el contrato de fase no decía ·
/complete-profile son 5 pasos más confirmación.
Se dibujan las siete rutas más los sub-pasos que cambian de forma: diez pantallas. Un paso que repite el patrón anterior no gana escena, gana una regla escrita.
register/page.tsx:112 hace router.push('/verify-otp?email=…').
Ahí no se abre ninguna sesión: se celebraría «Importamos futuro» para descubrir detrás
otra caja de OTP.
La placa sale de /register. De estas siete rutas queda
únicamente en /activate. La placa marca una puerta, no un envío — esa es
toda la regla, y aplicarla es lo que la vuelve real.
Seis cajas de 44 × 48 separadas por 8 px. 44 es el piso de área táctil, y la separación es lo único que deja leer cuál caja tiene el foco: sin costura, seis inputs se leen como un solo campo y el cursor no dice en qué dígito estás. Radio 11, padding y foco idénticos a los del resto de campos — la caja de OTP no es un widget aparte, es el mismo campo repetido seis veces.
Avance automático al escribir · borrado hacia atrás que salta a la caja anterior cuando la actual ya está vacía · pegado de los seis dígitos de golpe desde cualquier caja, no solo la primera (hoy solo la primera lo acepta).
Hoy hay un botón que cambia de texto y a veces otro al lado. Acá es
uno solo con dos estados: deshabilitado muestra «Reenviar en 45 s» con
la cuenta bajando, y a cero se convierte en «Reenviar código» activo. El número va en
tabular-nums para que el texto no baile cada segundo — sin
eso, «Reenviar en 9 s» y «Reenviar en 45 s» tienen anchos distintos y la línea salta.
El aro de la izquierda solo existe mientras corre la cuenta: es la única pista de que el botón está esperando y no roto.
El estado real viaja en un aria-live="polite" — «Código completo»,
«Verificando…», el error. Las cajas por sí solas no le cuentan nada a un lector de
pantalla.
/forgot-password del admin también tiene paso de código.--line sobre blanco. Ningún foco automático: robarle el cursor a alguien que llegó leyendo es hostil.--celeste-deep, halo de 3 px al 18 % y sube 1 px. El avance es automático.rgba(1,18,58,.52), borde --line-onmarino, foco en --celeste (no en celeste-deep: sobre marino se apaga).
Seis reglas que se marcan mientras escribes, no al enviar. El
comportamiento ya existía en usePasswordValidation; lo que cambia es que el
disco cambia de color, no solo el ícono — a 11 px un tick verde sobre gris
claro es un detalle que se pierde, un disco que se llena no.
La sexta regla —«Ambas contraseñas coinciden»— solo se evalúa cuando el campo de confirmación tiene algo. Marcarla en rojo mientras el campo está vacío es regañar a alguien por no haber terminado de escribir.
Pendiente — disco --line-soft, guion en
--ink-soft. Es una tarea por hacer, no un fallo: no se pinta de rojo lo que
todavía nadie intentó.
Cumplida — disco --state-success-soft, tick en
--state-success, entrando con --ease-spring en 240 ms. El
rebote es lo que se ve por el rabillo del ojo mientras escribes.
Fallida — solo la de coincidencia, y solo cuando hay algo que comparar:
disco --state-danger-soft, aspa en --rojo, texto en
--rojo-deep.
Bajo prefers-reduced-motion el estado cambia igual, sin el rebote. La
información nunca depende de la animación.
bg-green-100 y text-gray-500, traducido a la paleta.--line-soft, guion en --ink-soft. Es una tarea por hacer, no un fallo — no se pinta de rojo lo que nadie intentó.--state-success-soft, tick en --state-success, entrando con --ease-spring en 240 ms.--state-danger-soft, aspa en --rojo, texto en --rojo-deep.
Todas heredan el split: formulario claro a la izquierda, panel aurora al 46 %
a la derecha. El panel no cambia de una ruta a otra — ese es el punto. Lo
que cambia es lo que se pide, y la marca queda constante detrás de todo el trayecto.
/verify-otp, no a una sesión.maskEmail() ya existe. Mostrarlo entero en una pantalla de verificación es regalar la mitad del par./forgot-password: una sola pantalla sirve a las dos entradas.Cuando alguien llega acá la sesión ya existe — entró por Google o acaba de verificar su correo. Es un formulario, no una puerta. Poner «Importamos futuro» al guardar una dirección gasta el único gesto que la marca tiene reservado para el momento en que se abre el acceso.
El split no se apila entero. El panel colapsa a una banda de
180 px de piso que conserva los dos glows, el monograma y el eslogan; el formulario
queda debajo, en claro. La banda usa min-height, nunca height
fija: a 180 px planos el panel recortaba su propio contenido y el eslogan perdía el borde
superior.
En el producto el orden del DOM es flex-col-reverse: el
formulario va primero y la banda se pinta encima. Así el teclado y el
lector de pantalla llegan al primer campo sin cruzar la decoración, mientras el ojo sí ve
la marca arriba.
/verify-whatsapp-otp es idéntica a /verify-otp salvo el texto y
la píldora de canal · /forgot-password paso 1 es un campo y un botón, el caso
más simple del sistema · /activate bienvenida es el mismo bloque de texto más
el correo en un campo de solo lectura. Dibujarlas sería repetir, no diseñar.
La regla que las cubre: bajo 768 px toda ruta de auth es banda de 180 px + formulario en claro, con 22 px de margen lateral y el botón a ancho completo. La caja de OTP no se encoge — 6×44 + 5×8 = 304 px caben de sobra en los 346 útiles.
Una puerta sin destino no se puede juzgar. La placa se levanta y lo que queda detrás es esto: sidebar a la izquierda, topbar arriba. Entra en la misma pieza porque es donde el retinte tiene que aterrizar — si la puerta es marino y el destino es genérico, la marca dura 1.4 segundos.
Y hay una trampa concreta: el estado activo del sidebar no puede pintarse con
--primary. Ese token es #3765c0, el azul del producto, y
remapearlo repinta las dos apps enteras. El activo se marca con superficie
--celeste-soft, tinta marino y una barra de 3 px con
--grad-marca — la misma regla degradada de los correos, puesta de
canto.
client-sidebar.tsx: cinco grupos, catorce destinos, ni uno inventado.MobileShell, con sus cuatro pestañas y el FAB entre la segunda y la tercera.--primary
Superficie --celeste-soft, tinta --marino, y una barra de 3 px
con --grad-marca pegada al borde izquierdo. Se lee de inmediato y no toma
prestado un solo token del producto.
40 px, fondo --hueso al 90 % con backdrop-filter, una sola
línea abajo. La marca ya la carga el sidebar: repetirla arriba sería decir el nombre dos
veces en la misma frase.
--grad-marca aparece en la app en dos sitios: la barra del
activo y el botón de crear. Un degradado que sale en todas partes deja de ser una firma
y pasa a ser textura.
«Importamos futuro» es el momento en que se abre la sesión, no un spinner de propósito general. Una regla de una línea: la placa marca una puerta, no un envío de formulario. Aplicada de verdad, deja una sola placa nueva en las siete rutas de esta fase.
| Ruta | Placa | Por qué |
|---|---|---|
client /login | Sí | Ya hecho. Se abre la sesión. |
admin /login | Sí | Ya hecho. Se abre la sesión. |
/auth/callback | Sí | Ya hecho. Es el regreso de Google — el clic en Google no puede animar nada porque la redirección lo corta. |
/activate · paso 2 | Sí | La única nueva. Al guardar la contraseña la cuenta queda activa y se entra. |
/register · paso 2 | Nocambio | El contrato original la ponía acá. register/page.tsx:112 navega a /verify-otp: no se abre sesión, se pide otro código. |
/register · paso 1 | No | Es un «Siguiente». Ni siquiera envía nada al servidor. |
/verify-otp | No | Envío de formulario. El botón conserva su spinner. |
/verify-whatsapp-otp | No | Igual: se verifica un dato, no se abre una puerta. |
/forgot-password · 1-3 | No | Los tres pasos son envíos. Celebrar «pedí un código» vacía el gesto. |
/reset-password | No | Cambia una credencial y devuelve al login. La puerta viene después. |
/complete-profile | No | La sesión ya existe. Es un formulario, no una puerta. |
| logout | No | La salida es instantánea. No se anima irse. |
La placa tiene un piso de 1.4 s y la petición corre en paralelo, así que el tiempo percibido es 1.4 s en vez de 300 ms + 1.4 s. Si la petición falla, la placa se retira en 220 ms y el error aparece bajo el campo con el foco devuelto ahí — nunca queda una celebración colgada sobre algo que no ocurrió.
Se resetea en pageshow: sin eso, volver con el botón atrás revive la página
desde bfcache con la placa encima, tapando la puerta. Y bajo
prefers-reduced-motion no hay placa: se navega directo y el botón conserva su
spinner.
Una regla sin su porqué es una regla que alguien rompe. Estas seis salieron de romperlas primero, y por eso vienen con la marca de cómo dolió.
--primary, --primary-foreground ni --ring.--mogos-* añaden paleta; no sustituyen ninguna.globals.css. Nada de <style> dentro del componente.<style> hijos. En el App Router llegó cortado: las reglas del split se cayeron y el panel aurora colapsó del 46 % a 21.6 % — el ancho que midió su propio contenido. Una hoja importada se carga una vez, en orden, y no puede aplicarse a medias.#AAE4F3 la tinta es marino. El texto apagado sobre blanco es #3A4A72.--ink-soft, que es marino aclarado, no un gris cualquiera. Contraste medido sobre la superficie pintada, no token contra token.prefers-reduced-motion apaga las tres capas en movimiento.::before. La lista que olvida una es la regresión que nadie ve venir: la escena parece quieta hasta que el tercer plano sigue girando a 44 s por vuelta detrás de un formulario. Nombrar las tres explícitamente es la única defensa — ningún test lo atrapa.(auth) no decora nada. Cada puerta pinta su propia superficie.layout.tsx posicionan un MogosIcon absoluto arriba a la derecha. Con el shell puesto, ese ícono queda flotando sobre la aurora, a 36×32 y con sus fills quemados. Se va. Cada ruta llega ella misma hasta el borde de la ventana.page.tsx solo compone. La lógica vive en components/auth/./complete-profile ya está en 370 líneas dentro del page.tsx — es exactamente la deuda que este proyecto no agranda.| Par | Ratio | Dónde |
|---|---|---|
| marino / blanco | 16.31:1 | Tinta principal y botón primario del client. |
| marino / celeste | 11.73:1 | El botón del admin. Con tinta blanca daría 1.39:1 — por eso sobre celeste se escribe en marino. |
| marino / celeste-soft | 14.57:1 | Item activo del sidebar y píldora «En tránsito». |
| ink-soft / blanco | 8.74:1 | Todo el texto secundario. Un gris de plantilla daría ~4.6:1 a 12 px. |
| rojo-deep / blanco | 7.57:1 | Errores bajo el campo. |
| rojo-deep / rojo-soft | 5.53:1 | Píldora «Por pagar». |
| celeste / marino-deep | 13.14:1 | El tag de la placa. |
| on-marino-soft / marino | 11.57:1 | La cita del panel aurora. |
| warning / warning-soft | 3.11:1corregido | Píldora «En aduanas». No llegaba a AA. Se derivó --state-warning-deep #8A5A10 → 5.03:1. |
| success / success-soft | 4.49:1corregido | El tick de la checklist, a un pelo del piso. Se derivó --state-success-deep #276742 → 5.81:1. |
--muted
#7B88A6 sobre blanco da 3.55:1: sirve para etiquetas en
versalita de 9–11 px con tracking-widest, que son señalización, y para nada
que alguien tenga que leer. Los contadores del sidebar, las claves de las
tarjetas y los encabezados de tabla se pasaron a --ink-soft por eso.
Esta es la fase 1: client. Lo de abajo queda escrito por nombre para que nadie lo dé por hecho ni por olvidado.
/forgot-password y /reset-password del admin, en variante
noche: tarjeta de vidrio de 400 px sobre la aurora completa. Su
/forgot-password también tiene paso de código, así que
hereda la caja de OTP en su versión oscura — ya diseñada en §03.
OnboardingStepper, StepSuccessTransition y
TrustMessage tienen su propia gramática de progreso. Acá se retintaron;
rediseñarlos es trabajo aparte y sale del contrato de «retintada».
Google Maps dentro de /complete-profile: el único punto de las siete rutas
donde hay una superficie de terceros. Necesita su propia decisión de encaje, no un
retinte.
Hoy los page.tsx mezclan font-family-display,
font-family-sans y utilidades sueltas sin una escala común. Merece una
pasada propia — es lo que hace que dos pantallas retintadas igual se sientan distintas.
actions.ts, los esquemas Zod y password-fields.tsx existen dos
veces, uno por app. El shell aurora es la excusa para consolidarlos en
packages/ui — pero es refactor, no presentación.
Campos nuevos · pasos nuevos · cualquier cambio en lo que estas rutas
envían · la mecánica de auth (Supabase, las server actions, el guardia de
returnTo). Esto consolida presentación.