La primera parte de esta serie termina con una pregunta: si se puede escalar el glifo sin tocar el elemento que lo contiene, evitando que el 100% incrustado en una frase árabe crezca con el resto del texto. La respuesta es la técnica que documenté en el artículo de julio: una familia @font-face por script, acotada con unicode-range y calibrada con size-adjust, resuelta contra fuentes de sistema con local(). Funciona carácter a carácter porque el font matching de CSS opera así: el texto latino atraviesa la familia no latina como si no existiera y aterriza en la fuente de marca, sin que nadie tenga que envolver el 100% en ningún span. Ese artículo cubre el cómo con detalle, los nombres PostScript con los que resuelve cada navegador, la tabla de fuentes por sistema operativo con su size-adjust calibrado script a script. Este artículo no repite nada de eso: cubre lo que aquel no medía todavía cuando lo publiqué en julio, porque entonces no tenía instrumentación para medirlo. size-adjust resuelve el tamaño del glifo y deja sin resolver la caja que lo contiene, y esa caja es la que decide, al final, si el texto se ve completo o recortado.

La misma frase قميص دنيم قطن 100% بقصة عادية renderizada con size-adjust por unicode-range: el texto árabe crece hasta igualar el tamaño visual del latino, mientras el fragmento 100% incrustado se mantiene en el tamaño original de la fuente de marca, sin el crecimiento indebido de la primera parte de la serie.

La invariante que decide si el texto cabe

Todo el problema del texto recortado se reduce a un cociente por estilo tipográfico: el interlineado dividido por el tamaño. Ni los píxeles absolutos ni el idioma entran en la condición, solo esa proporción.

line-height ÷ font-size ≥ R

R es una constante que depende de la geometría de las fuentes en juego, no del producto ni de la pantalla. Un badge de 12/12 px tiene cociente 1,00; si R para el script que contiene es 1,86, ese badge recorta el texto sin excepción, sea cual sea el dispositivo o el tamaño de fuente base. De dónde sale R es lo que explica por qué size-adjust no lo cambia: la caja de línea reparte su altura entre lo que queda por encima y por debajo de la línea base, y ese reparto lo decide la asimetría de la cara que posee la línea, Δ = ascent − descent de esa cara concreta.

R = max(2·bboxAsc − Δ, 2·bboxDesc + Δ)

Si Δ queda al azar, R sube. Si Δ se declara a medida con ascent-override y descent-override, R baja hasta su mínimo teórico, que es la altura real de la tinta dibujada. size-adjust no toca ninguno de los dos términos de esa fórmula: es un descriptor de @font-face que escala el glifo antes de que el navegador calcule el resto, no una propiedad de maquetación que participe en el cálculo de la caja. Multiplicar el tamaño de la letra y dejar el interlineado quieto es, en términos del cociente, subir el denominador sin tocar el numerador: R efectivo baja, y con R bajo la tinta ya no cabe.

Dos puertas para la misma métrica

ascent-override y descent-override sí participan en la maquetación, pero solo llegan a la caja de línea por una de dos puertas, y cuál de las dos está abierta depende de una propiedad que nada tiene que ver con la tipografía a simple vista: cómo se declaró line-height.

Con line-height numérico o en una unidad de longitud, que es como cualquier token de espaciado se declara en un sistema de diseño calculable, la altura de la caja cuenta solo las métricas de la first available font: la primera cara de la pila de font-family que tenga un glifo para el carácter que se está dibujando. Con line-height: normal, en cambio, cuentan las métricas de todas las caras que intervienen en la línea, y la caja crece hasta la mayor de todas ellas.

La consecuencia práctica es que un sistema de tokens que use interlineados calculables, que es la base de cualquier ritmo vertical predecible entre dispositivos, vive siempre detrás de la primera puerta. Y esa puerta se abre o se cierra en función de qué cara del stack dibuje el carácter U+0020, el espacio, no en función de qué cara dibuje el texto visible.

La diferencia entre las dos puertas se puede medir en píxeles, no solo describir. Sobre esa misma pila a 100 px de font-size y line-height: 1.5, es decir 150 px de caja, la cara árabe con ascent-override: 105% y descent-override: 45% sitúa la línea base a 105 px del borde superior de la caja cuando es ella quien la posee. Si en cambio la posee la cara latina, sin ningún override propio para el árabe, la baseline cae a 109,5 px: un desplazamiento de 4,5 px que no depende de ningún cambio de fuente ni de tamaño, solo de qué cara reclamó el espacio. Ese desplazamiento es exactamente el margen que le falta a la tinta árabe para caber dentro de la caja cuando el override correcto no llega a aplicarse.

Una cara sin glifos visibles puede poseer la línea

Aquí está el hallazgo contraintuitivo del que salió el contrato de arquitectura tipográfica que acompaña a esta serie: una cara puede no dibujar ni un solo glifo visible en una línea y aun así ser la que decide la altura de esa línea, por el único hecho de reclamar el espacio. Cuatro medidas sobre una pila «árabe, latina» a 100 px con line-height: 1.5 y la cara árabe pidiendo un ascent del 105 % y un descent del 45 % lo dejan sin ambigüedad: si la cara árabe no declara U+0020 en su unicode-range, la baseline la coloca la cara latina aunque el texto entero, sin un solo carácter latino, esté escrito en árabe. Solo cuando la cara árabe reclama explícitamente el espacio pasa a ser la first available font y su override llega a la caja de línea. Y en el caso extremo, una tercera cara cuyo único rango sea U+0020 se lleva la geometría completa de la línea sin dibujar nada, ni una letra.

La implicación para cualquier arquitectura de fuentes es directa: declarar ascent-override y descent-override en la cara del script no basta. Hace falta una segunda declaración de esa misma cara, sin size-adjust distinto pero con U+0020 en su unicode-range, para que la puerta se abra. Sin esa reclamación explícita, los overrides existen en la hoja de estilos y no llegan nunca a la línea, sin ningún error en consola que lo señale.

Anatomía de la caja de línea en árabe: la line box en azul, el bounding box del texto dibujado a trazos discontinuos, la línea base marcada y la tinta árabe sobresaliendo por debajo del límite inferior de la caja, en la zona que un overflow: hidden recortaría.

El mínimo por script, medido

El interlineado mínimo que evita el recorte no es un número único para todo lo no latino, igual que el size-adjust de cada script no lo era en el artículo de julio. Medido sobre el banco de pruebas que acompaña a esta serie, con la estrategia que hoy está en producción, el cociente mínimo por script es este:

Scriptline-height ÷ font-size mínimo
Latín1,14
Chino1,03
Japonés1,12
Coreano1,16
Hebreo1,30
Devanagari1,50
Tailandés1,78
Árabe1,86
Árabe vocalizado2,05

Frente a esa tabla, la escala real de tokens tiene estilos que se quedan cortos: las sondas del banco de pruebas, con nombres de patrón habitual como badge o counter, replican a propósito componentes de cociente 1,00, un número y su interlineado exactamente iguales. No forman parte hoy del catálogo de tokens de p4, pero son el patrón de componente, un contador, una insignia de estado, donde el contrato se rompe primero en cuanto alguien lo añada sin conocer esta tabla. El árabe vocalizado, con harakat, es el caso límite de toda la tabla y el que menos margen deja: ningún interlineado razonable en una escala de producto llega a 2,05 sin desentonar visualmente con el resto de idiomas. Puedes reproducir esta medición sobre tus propias fuentes y tu propio dispositivo en el banco de pruebas: las cifras cambian con la fuente instalada, el procedimiento no.

El sitio donde este mínimo se paga en producción no es el texto suelto, es el componente que lo envuelve. Una insignia de estado con altura fija y overflow: hidden, un contador de notificaciones, una etiqueta de talla dentro de una tarjeta de producto: cualquier contenedor que fije su propia altura en vez de dejar que la marque el contenido recorta en cuanto el idioma cambia a uno de cociente alto, aunque el mismo componente lleve meses en producción sin un solo defecto reportado en español. El defecto no está en el componente, está en la suposición de que la altura del texto es una constante del sistema quien puede fijarla, y no del script quien la impone.

El error que me costó entender la unión de dos alfabetos

Elegí mantener los tokens de interlineado del sistema en valores numéricos calculables, en píxeles derivados de una escala modular, y descarté declarar line-height: normal como atajo para heredar automáticamente la métrica de todas las caras implicadas. Con normal el problema de la puerta cerrada desaparecería solo, porque cada cara que interviene en la línea aportaría su propia geometría sin que nadie tuviera que reclamar U+0020 a mano. Lo descarté porque los valores numéricos son la base del ritmo vertical de todo el sistema, y normal depende del valor por defecto de cada fuente en cada sistema operativo: ese ritmo dejaría de ser predecible entre dispositivos, que es justo la propiedad que un sistema de tokens tiene que sostener. El coste que asumo por mantener valores numéricos es tener que declarar explícitamente, cara por cara, quién posee la línea.

Ese coste es el que no pagué bien la primera vez. La primera versión del banco de pruebas de esta serie calculaba ascent-override y descent-override con la tinta del script solamente, bboxAsc y bboxDesc de la cara árabe o tailandesa medidos en aislamiento. Es un error razonable sobre el papel, porque parece que la cara que hay que ajustar es la del script que se está corrigiendo. Es incorrecto en la práctica porque la cara que posee la línea tiene que cubrir la tinta más alta que pueda aparecer en ella, y en una línea mixta esa tinta puede ser la latina incrustada, no la del script. Corregido, el cálculo usa la unión de los dos alfabetos: el máximo ascendente entre script y latino, el máximo descendente entre los dos. La cifra que expone el error es esta: en seis de los nueve idiomas de la tabla anterior, el ascendente que hay que cubrir es el latino, no el del script, porque los dígitos y las siglas incrustadas dibujan más alto que los propios ideogramas o el propio alifato. El límite de validez de esta corrección es el mismo que el de toda la capa: solo se sostiene mientras cada cara nueva que se añada al stack declare a conciencia si reclama el espacio o lo cede, porque en cuanto una se añade sin decidirlo, el contrato deja de cumplirse en silencio, sin un solo error, hasta que alguien vuelve a medir.