Portada de «Cuando un 5♠ parecía un 7: jerarquía visual en Blupoli Cards» — Diseño de Blupoli.

El problema cabía en una sola carta

La señal más útil de esta iteración no fue un error de JavaScript, una excepción en producción ni una regla de solitario mal implementada. Fue algo mucho más pequeño: al mirar un cinco de picas, los dos símbolos de palo que acompañaban al rango en las esquinas tenían un tamaño y una presencia demasiado próximos a los cinco pips del centro. La carta seguía teniendo cinco pips en el DOM y el motor seguía sabiendo que su rango era cinco, pero visualmente podía parecer que había siete picas equivalentes.

Eso importa porque una baraja funciona como una interfaz extremadamente comprimida. En una fracción de segundo queremos distinguir rango, palo, orientación y posición, incluso cuando parte de la carta está tapada por otra. Los pips de una carta numérica no son decoración: representan el valor mediante una composición familiar. El pequeño palo del índice cumple otra función: acompaña al número o letra para identificar el palo cuando sólo queda visible la esquina. Si ambos elementos reciben el mismo peso, el diseño mezcla dos canales semánticos diferentes.

La Issue #336 convirtió esa observación en un criterio concreto: el rango debía ser dominante en el índice, el palo pequeño claramente secundario y los pips centrales debían conservar la mayor presencia visual. También pedía varios reversos de estilo clásico para el selector de apariencia. Lo interesante es que el problema no apareció en una baraja improvisada. Llegó después de varias mejoras deliberadas. Para entender por qué, hay que retroceder unas horas en la evolución de Blupoli Cards.

La primera mejora correcta: volver a dibujar A–10 como cartas de verdad

La PR #314 había sustituido una representación mucho más simple por una baraja compartida con patrones de pips originales para los rangos 2–10, un tratamiento específico para el as y una composición vectorial simétrica para J, Q y K. El cambio vivía en apps/cards/src/ui/renderers.js y apps/cards/styles.css, no dentro de cada juego. Esa decisión era importante: Klondike, Spider, FreeCell, Pyramid, TriPeaks y los solitarios que fueron llegando después debían hablar el mismo lenguaje visual.

El renderer definió explícitamente la geometría de cada número. El cinco, por ejemplo, se construye con cuatro pips en las esquinas de la matriz y uno en el centro. El diez usa diez posiciones. Las figuras se dibujan con SVG propio y el as recibe un símbolo central distinto. La suite de navegador se amplió para comprobar que el diez contenía diez pips reales y que el as conservaba su marca específica en móvil. Ya no dependíamos de un único símbolo central para representar cualquier carta numérica.

Ese rediseño resolvía un problema real de reconocimiento. También creó una nueva responsabilidad: una vez que los pips centrales representan visualmente el número, cualquier otro símbolo del mismo palo necesita una jerarquía claramente diferente. Antes, con un único símbolo central, el palo de la esquina podía crecer sin competir con un conteo. Después de #314, el centro de la carta ya tenía una semántica cuantitativa mucho más fuerte.

La lección retrospectiva no es que #314 estuviera equivocado. Al contrario: hizo posible detectar el problema siguiente. Las interfaces evolucionan así con frecuencia. Una capa más expresiva revela conflictos que la versión simplificada no podía tener.

Una baraja compartida amplifica tanto los aciertos como los errores

Cards ya no era un experimento con cinco juegos aislados. La arquitectura que contamos en el Devlog sobre Cards como segundo producto del monorepo había evolucionado hacia un motor y una UI compartidos con módulos de dominio por juego. Cuando se incorporó Baker’s Game reutilizando la familia de FreeCell, la misma idea volvió a aparecer: compartir lo que realmente es común y especializar sólo la regla que cambia.

El dibujo de las cartas pertenece claramente a la parte común. No tendría sentido que Spider tuviese un cinco de picas con una jerarquía distinta de Klondike, ni que Montana inventase otro reverso para resolver su densidad. El renderer compartido reduce duplicación y hace que una mejora llegue a todos los juegos. Pero esa ventaja tiene una cara inversa: una mala proporción también llega a todos.

Por eso esta tarea no podía resolverse añadiendo una excepción sólo para las cartas numéricas de Klondike. El conflicto existía en el sistema visual. Había que modificar el contrato de la carta compartida y después comprobar que seguía funcionando en tableros de siete, ocho, diez y trece columnas, en escritorio y en móvil.

Ese alcance explica por qué un detalle que parece puramente cosmético terminó tocando renderer, CSS, preferencias, traducciones, tests de fuente y Playwright. La superficie era común por diseño; la verificación también tenía que serlo.

Hacer los palos más grandes fue una decisión razonable… hasta que dejó de serlo

La siguiente etapa, PR #322, partió de otra observación válida: las cartas se veían algo chatas y los símbolos de palo tenían menos protagonismo del deseado. La proporción base pasó de 5/7 a 2/3, haciendo la carta un poco más alta. Los pips, el as y otros símbolos crecieron. Además añadimos tres estilos de cara —Clásica, Palos grandes y Minimal— y cuatro reversos iniciales —Blupoli, Violeta, Coral y Medianoche—.

La prueba E2E de aquella versión refleja literalmente la intención. Medía el rango y el palo del índice y esperaba que el palo fuese mayor que el rango. En otras palabras, no fue un accidente de CSS: habíamos convertido “dar protagonismo al palo” en una expectativa automatizada. Esa decisión tenía sentido si se analizaba únicamente la legibilidad del símbolo. El problema apareció al analizar la carta completa.

Un índice de naipe no es una pequeña ilustración independiente. El número y el palo forman una unidad de lectura, pero el número sigue respondiendo a la pregunta principal “¿qué carta es?”. En las cartas numéricas, los pips del centro repiten el palo precisamente para expresar el valor. Si el palo auxiliar de la esquina crece hasta acercarse a esos pips, el ojo deja de distinguir con suficiente rapidez qué símbolos pertenecen al conteo y cuáles al índice.

La corrección de #337 hizo algo importante desde el punto de vista del proceso: no intentó preservar un test sólo porque ya existía. Cambió la expectativa. La prueba que antes premiaba un palo de esquina mayor pasó a exigir lo contrario: el rango debe ser al menos un 35 % mayor que ese palo, y un pip central también debe superar al palo auxiliar al menos en ese margen. El test dejó de proteger una implementación anterior y empezó a proteger la intención correcta.

La proporción 2:3 no era un cambio aislado

Alargar las cartas de 5/7 a 2/3 parece una modificación pequeña si se observa una sola carta. En un solitario, sin embargo, la geometría se multiplica. Una columna de Klondike puede tener varias cartas solapadas; Spider puede mostrar diez columnas; Montana distribuye cincuenta y dos posiciones en una cuadrícula muy densa. Unos pocos píxeles adicionales de altura pueden modificar cuánto tablero queda visible antes de hacer scroll y cuánto espacio necesita una animación.

Por eso #322 no cambió únicamente .playing-card. La misma proporción se aplicó a slots vacíos, placeholders de columnas, animaciones de Spider y vuelos de cartas de victoria. Donde el layout era especialmente denso, el sistema ya tenía variables y reglas específicas de anchura y solape. El objetivo no era maximizar la fidelidad física a una baraja concreta, sino conseguir una silueta más reconocible sin romper la información de los juegos más comprimidos.

También apareció un recurso compartido, .card-hero-suit, que permite que los estilos Palos grandes y Minimal muestren un símbolo central muy visible sin duplicar el HTML de la carta. En pilas donde una carta queda cubierta por otra, ese elemento se oculta salvo cuando realmente aporta información. En layouts móviles densos se puede recuperar un símbolo central de menor tamaño para conservar reconocimiento.

La proporción y la jerarquía, por tanto, no se pueden tratar como dos temas separados. Una carta más alta crea más espacio para distinguir zonas: índice, centro y esquina invertida. Aprovechar ese espacio sin volver a saturarlo era el siguiente paso.

Diagrama de una carta de cinco de picas que separa tres niveles: rango dominante, palo auxiliar reducido y cinco pips centrales con mayor peso visual
El contrato visual final separa función e intensidad: el rango identifica, el palo auxiliar contextualiza y los cinco pips centrales representan el valor.

La solución fue separar semánticamente el palo del índice

Antes de #337, el rango y el palo auxiliar vivían dentro de .card-corner, pero el CSS podía dirigirse al segundo elemento con selectores relativamente genéricos. La corrección añadió una clase explícita: .card-corner-suit. Parece un cambio de nomenclatura mínimo, pero convierte una intención visual en una pieza con nombre propio. Ya no estamos estilizando “el span que casualmente está debajo del strong”; estamos estilizando “el palo auxiliar del índice”.

El CSS base deja el tamaño del palo en .58em respecto al índice, con una separación vertical propia. Clásica, Palos grandes y Minimal pueden ajustar ligeramente esa relación sin perder la jerarquía. Los pips centrales conservan un tamaño mayor y ocupan una región central que empieza más abajo, lejos de la zona del índice. La esquina invertida mantiene la misma lógica al otro extremo.

La distinción también ayuda a mantener el código. Si mañana queremos mejorar el tracking del rango, la alineación del palo o la densidad de las cartas estrechas, cada responsabilidad tiene un selector explícito. No necesitamos deducir qué elemento es el palo por su posición en el DOM.

Este tipo de semántica CSS suele parecer excesiva hasta que una interfaz crece. En una baraja compartida por trece juegos, tres estilos de cara, nueve reversos y varios breakpoints, nombrar la función de un elemento es una forma barata de evitar que un ajuste local cambie otra cosa por accidente.

Convertir “parece un siete” en una prueba reproducible

La percepción humana no puede reducirse por completo a una aserción de Playwright, pero sí podemos proteger parte de la causa que produjo la confusión. La nueva prueba construye una partida determinista de Klondike y coloca un 5♠ boca arriba en una posición conocida. Después comprueba tres cosas distintas.

Primero, el DOM de la carta debe contener exactamente cinco elementos .card-pip dentro de .card-pips--5. Eso confirma que el renderer no ha añadido accidentalmente símbolos centrales de más. Segundo, la geometría calculada por el navegador debe mantener la proporción alta de la carta. Tercero, compara tamaños computados: el rango y el pip central tienen que ser claramente mayores que .card-corner-suit.

La prueba no afirma que cualquier persona vaya a interpretar la carta perfectamente en cualquier dispositivo. No tenemos una métrica automática de percepción. Lo que hace es impedir que vuelva la condición concreta que originó el problema: un palo auxiliar con el mismo peso tipográfico que la información que representa el valor.

Además, el caso E2E continúa hasta el selector de apariencia. Cambia la cara a Palos grandes, el reverso a Poker azul, comprueba los atributos del documento, verifica que aparece el motivo del reverso, inspecciona el objeto guardado en localStorage, recarga y exige que la preferencia siga activa. Finalmente vuelve a comprobar que no existe overflow horizontal. Una sola prueba enlaza jerarquía, personalización, persistencia y responsive porque esas piezas forman parte del mismo contrato visible.

La apariencia es una preferencia, no una regla de la partida

Cuando añadimos los estilos configurables en #322, una decisión arquitectónica evitó mezclar dos conceptos diferentes. El estado del solitario —cartas, columnas, fundaciones, movimientos, semilla— continúa en el repositorio de partidas. La apariencia vive en browser-card-appearance-repository.js bajo la clave blupoli-cards:appearance:v1.

Ese repositorio sólo conoce dos valores: face y back. Normaliza opciones inválidas y vuelve a valores predeterminados si el almacenamiento contiene algo que ya no existe o si la lectura falla. Esta separación significa que cambiar de Clásica a Minimal no modifica una partida guardada, no altera estadísticas y no crea una variante de reglas.

También permite que la elección sea global para Cards. Una persona puede seleccionar un reverso clásico en Klondike y encontrarlo al abrir Spider o FreeCell. El producto se siente coherente sin forzar a cada motor a conocer preferencias de presentación.

El patrón es pequeño pero importante para futuras opciones. Una preferencia visual puede evolucionar con sus propios valores y migraciones. El dominio del juego permanece estable. Si alguna vez una opción afecta a reglas —por ejemplo, una variante de reparto— debe seguir el camino contrario y formar parte de la identidad de la partida. La frontera no es “todo en localStorage”; la frontera es qué significado tiene cada dato.

Tres caras, un solo HTML

Los estilos Clásica, Palos grandes y Minimal no son tres renderers. La carta se genera una sola vez con índice, centro apropiado, .card-hero-suit y esquina invertida. A partir de los atributos data-card-face del elemento raíz, CSS decide qué elementos tienen presencia.

Clásica mantiene la composición de pips, as y figuras. Palos grandes oculta esas composiciones en las cartas expuestas y muestra un palo central enorme, pensado para reconocimiento inmediato. Minimal reduce todavía más la ornamentación, oculta la esquina invertida y simplifica borde y sombra. Las tres opciones comparten la misma identidad accesible de la carta y los mismos movimientos.

Ese enfoque evita dos tipos de deriva. El primero sería funcional: que una cara tuviese un atributo de drag o un nombre accesible distinto. El segundo sería responsive: corregir una regla para Montana en un renderer y olvidarla en los otros dos. Con un HTML común, las diferencias son de presentación y pueden probarse a través de atributos de documento.

La corrección de #337 tenía que respetar las tres. No bastaba con arreglar Clásica porque fuese la predeterminada. El palo de esquina sigue siendo secundario en Palos grandes y Minimal; lo que cambia es la escala del símbolo central y el nivel de detalle de la cara.

Los reversos nos obligaron a obedecer una restricción poco visible

El primer rediseño del reverso en #314 utilizó gradientes CSS. La suite global del proyecto los prohibía dentro del contrato visual de Cards. La PR #317 apareció inmediatamente después para sustituirlos por geometría CSS plana: bordes, rombos y anillos construidos con pseudo-elementos. Además añadió al test específico de Cards la misma regla que ya protegía la suite global.

Ese episodio cambió la forma de abordar los reversos posteriores. Cuando #322 añadió Blupoli, Violeta, Coral y Medianoche, no recurrimos a imágenes externas. Cada diseño usa variables de color y geometría compartida. Aun así, el primer merge introdujo otro recurso que parecía inocuo: color-mix() para derivar tonos transparentes. El Quality global lo detectó después del merge.

La PR #324 eliminó color-mix() y sustituyó cada mezcla por valores RGBA explícitos. También añadió una aserción Cards-only que falla si el patrón vuelve a aparecer. No hubo que rediseñar la arquitectura ni aceptar una excepción; el reverso conservó su aspecto mediante valores planos compatibles con el contrato.

Es un buen ejemplo de por qué la definición de terminado del proyecto no termina en “la PR de feature pasó sus tests locales”. #322 había superado el lane de Cards, pero el Quality de main encontró una incompatibilidad global. La tarea siguió abierta de facto hasta que #324 dejó ambos niveles verdes y el despliegue de producción verificó la revisión corregida.

Cinco reversos clásicos sin copiar una baraja comercial

La segunda petición de #336 era ampliar el selector con diseños más próximos al lenguaje visual de una baraja de poker tradicional. La solución evita reproducir el reverso de una marca concreta. Añadimos cinco opciones genéricas: Poker azul, Poker rojo, Rombos clásicos, Entramado clásico y Medallón clásico.

Los cinco comparten .card-back-pattern y una pequeña familia de elementos .card-back-motif. El mismo markup puede convertirse en marcos concéntricos, rombos de esquina, líneas cruzadas o un medallón central cambiando reglas CSS. Los colores se expresan con valores planos y transparencias RGBA. No hay SVG remoto, textura raster, fuente especial ni dependencia de una CDN.

Esto reduce peso y, sobre todo, mantiene el reverso dentro del mismo pipeline que el resto de Cards. La Content Security Policy no necesita nuevas excepciones. El modo offline no pierde una textura. Un cambio de estilo no exige descargar otro recurso. Y el test de fuente puede comprobar que existen los selectores de los cinco reversos y que siguen sin aparecer gradientes o color-mix().

Los cuatro diseños anteriores no desaparecen. El selector agrupa la familia Blupoli —Blupoli, Violeta, Coral y Medianoche— y la familia clásica. Con nueve opciones en total, la preferencia sigue siendo sencilla: un único valor estable que se normaliza al cargar.

La localización también forma parte de una preferencia visual

Añadir valores técnicos como poker-blue o poker-medallion es sólo la mitad del trabajo. El selector es interfaz pública y Cards se publica en seis idiomas. #337 añadió nombres y grupos de traducción para español, inglés, italiano, portugués, francés y alemán.

Los valores persistidos no se traducen. Esa distinción evita que un cambio de idioma invalide una preferencia o que el código tenga que interpretar etiquetas visibles. El almacenamiento conserva poker-blue; la UI decide si mostrar “Poker azul”, “Poker blue” o el texto correspondiente al locale activo.

La suite de Cards comprueba que las nuevas claves no caigan en el fallback literal de su identificador. No demuestra por sí sola la calidad editorial de cada traducción, pero sí evita publicar accidentalmente backPokerMedallion como etiqueta visible.

Es otra consecuencia de tratar la personalización como parte del producto y no como un experimento de CSS. Una opción que el jugador puede elegir necesita nombre, persistencia, compatibilidad futura, foco de teclado, responsive y tests igual que cualquier otro control.

Los tableros densos son donde una baraja bonita se demuestra o se rompe

Una captura de una carta aislada puede ocultar casi todos los problemas de un solitario real. En Klondike hay siete columnas; FreeCell y Baker’s usan ocho; Spider puede necesitar diez; Montana comprime la baraja en trece posiciones por fila. El mismo índice debe seguir siendo útil cuando la carta mide mucho menos que en un mockup de escritorio.

La suite E2E de Cards ya recorre los trece juegos y comprueba overflow horizontal. También mide densidad de columnas y mantiene casos específicos para layouts de siete, ocho y diez columnas. Montana recibe reglas adicionales porque sus cartas pueden ser tan estrechas que mostrar todos los pips deja de ser razonable. En ese contexto, el sistema puede ocultar el patrón numérico y conservar un símbolo central más simple.

Esto no contradice el objetivo del 5♠. La jerarquía depende del contexto disponible. Cuando hay espacio suficiente para cinco pips, deben leerse como cinco y el palo de esquina no debe sumarse perceptivamente al grupo. Cuando el tablero es extremadamente denso, la UI puede degradar la representación a una señal más simple, siempre que rango y palo sigan siendo identificables.

Diseñar responsive no significa reducir todo con la misma escala. Significa decidir qué información conserva prioridad a medida que desaparece espacio. En Cards esa prioridad quedó más clara después de esta iteración: primero identidad de la carta, después representación ornamental.

El CI terminó formando parte de la historia visual

Hay una tendencia a pensar que los pipelines de CI protegen principalmente lógica, compilación y dependencias. En esta secuencia protegieron algo tan visual como la forma de un reverso. #317 nació porque un patrón de gradiente violaba el contrato del proyecto. #324 nació porque color-mix() hacía lo mismo. #337 reforzó E2E para que la relación entre rango, palo y pips no dependiese exclusivamente de una revisión manual.

La PR #337 pasó el job de verificación de Cards, el E2E de navegador, la preview de cards.blupoli.com y la comprobación de caché. Después de fusionarse, el Quality de main también quedó verde y el workflow de Firebase desplegó Cards junto al resto de superficies verificando revisión y política de caché.

Eso no convierte un test de font-size en sustituto de diseño. La revisión humana sigue siendo necesaria para juzgar equilibrio, densidad y reconocimiento. Pero sí elimina una categoría de regresiones conocidas. Una vez que hemos identificado una causa concreta, no necesitamos redescubrirla visualmente cada vez.

La diferencia es importante: automatizar una decisión visual no significa afirmar que el diseño se puede resolver por números. Significa usar números para proteger las partes medibles de una decisión que ya hemos tomado con criterio visual.

Qué cambió realmente para quien juega

Desde fuera, el resultado se puede resumir de forma mucho más corta que su implementación. Las cartas tienen una proporción más alta, los pips centrales son legibles, el rango domina la esquina y el pequeño símbolo de palo acompaña sin competir. Un cinco de picas contiene cinco pips centrales y los dos indicadores de esquina ya no tienen el mismo peso que esos cinco.

El panel de apariencia permite elegir entre Clásica, Palos grandes y Minimal, además de nueve reversos: cuatro de la familia visual de Blupoli y cinco diseños clásicos. La preferencia se conserva al cambiar de juego y después de recargar. No cambia reglas, puntuaciones, estadísticas ni partidas guardadas.

Para el código, el resultado más valioso es menos visible. Seguimos teniendo un solo renderer compartido. Los estilos se activan mediante datos en el documento. El almacenamiento de apariencia está separado del estado de juego. Los nuevos reversos reutilizan motivos comunes y obedecen el mismo contrato de CSP. La prueba del 5♠ convierte el bug perceptivo que originó esta fase en una regresión explícita.

Ese equilibrio —más opciones sin multiplicar implementaciones— es exactamente la clase de crecimiento que queremos para Cards.

Lo que no intentamos resolver

Este trabajo no pretende recrear una baraja física concreta ni perseguir una fidelidad histórica absoluta. Los reversos “clásicos” son composiciones originales inspiradas en recursos geométricos comunes de barajas de juego, no copias de diseños comerciales. Tampoco convertimos el selector en un editor con colores, tipografías y texturas arbitrarias.

No tenemos una prueba automática de percepción capaz de asegurar que nadie confundirá nunca un valor. La aserción de tamaños cubre la causa conocida y el conteo del DOM cubre la estructura, pero la calidad visual todavía necesita inspección. Diferencias de fuente, densidad de pantalla o preferencias del usuario pueden cambiar matices que un umbral no captura.

Tampoco hemos separado una baraja distinta por juego. Eso es intencional. Mientras la carta represente los mismos rangos y palos, una baraja compartida mantiene coherencia y reduce mantenimiento. Si en el futuro un juego necesitara información adicional realmente específica, habría que justificar esa excepción en vez de abrir un segundo sistema visual por comodidad.

La mejora queda, por tanto, deliberadamente acotada: jerarquía más clara, personalización razonable y una base que siga siendo común.

La lección: más grande no significa más importante

El recorrido de estas cinco PRs deja una enseñanza sencilla. En #314 queríamos que las cartas se reconocieran mejor y dimos a los pips una estructura real. En #322 queríamos que los palos tuviesen más presencia y llegamos a automatizar que el palo del índice fuese mayor que el rango. La observación que originó #336 mostró el límite de esa idea. El símbolo era más visible, sí, pero estaba ocupando el nivel equivocado de la jerarquía.

La solución no fue volver atrás ni hacer todos los símbolos pequeños. Fue distinguir funciones. El rango identifica. El palo auxiliar contextualiza. Los pips representan valor. El as y las figuras tienen composiciones propias. Palos grandes y Minimal pueden enfatizar un símbolo central porque lo hacen conscientemente como variantes de presentación. El reverso puede ser decorativo porque no compite con información de juego.

También aprendimos algo sobre los tests. Una aserción puede congelar una mala decisión igual que puede proteger una buena. La prueba de #322 estaba funcionando exactamente como se había escrito; el problema era la expectativa. En #337 la cambiamos en vez de tratarla como una verdad heredada. Mantener una suite sana exige revisar qué intención protege cada check cuando la evidencia del producto cambia.

Y, finalmente, la secuencia confirmó por qué el diseño compartido merece el mismo rigor que el dominio. Una carta no es sólo CSS. Es una pieza central de interacción repetida miles de veces, en trece juegos y varios tamaños de pantalla. Cuando esa pieza se diseña como sistema, un 5♠ que parecía un 7 puede acabar mejorando no sólo una carta, sino la forma en que construimos y verificamos toda la baraja.

El resultado puede probarse directamente en Blupoli Cards. Para el contexto de producto, el lanzamiento original de Cards explica por qué los solitarios viven como producto propio; y los Devlogs de arquitectura de Cards y reutilización de Baker’s Game muestran la misma idea desde otras capas: compartir una base no significa borrar las diferencias que realmente importan.