Un botón primario, display: flex; align-items: center, con la etiqueta REGISTRARSE en las mayúsculas fijas del sistema de diseño y sin una sola línea de CSS que huela a truco. Y aun así el texto no cae en el centro del botón: se ve un par de píxeles por debajo. El primer sospechoso que revisa cualquiera es el line-height, y ahí está el error de diagnóstico que me costó una tarde: align-items: center no centra la tinta del texto, centra la caja de línea, y la caja de línea de una fuente no es simétrica respecto al texto que dibuja.

Comparación de un botón «Registrarse» con y sin ascent-override y descent-override. Sin el ajuste, el texto queda visiblemente por debajo de la línea de centrado del botón. Con ascent-override al 72% y descent-override al 4%, el texto se alinea con la línea de centrado y aparece un check verde. Debajo, un diagrama muestra cómo estos descriptores reducen la caja reservada de la fuente al tamaño real de la tinta sin tocar font-size ni line-height.

Por qué el centrado por flexbox no ve lo mismo que el ojo

La caja de línea de un texto sale de tres valores que trae la fuente en sus tablas hhea y OS/2: el ascent, la distancia reservada por encima de la línea base, el descent, la distancia reservada por debajo, y el line gap, el margen extra entre líneas consecutivas. Con line-height: normal, el navegador suma los tres y ese es el alto que align-items: center reparte en partes iguales arriba y abajo.

El problema es que esos tres valores están pensados para la fuente completa, no para el subconjunto de glifos que aparece en un componente concreto. Una familia que tiene que cubrir mayúsculas acentuadas como Á reserva un ascent que sube más que cualquier mayúscula sin tilde. Medí la tinta real de REGISTRARSE con canvas.measureText() y no supera el 71% del em, frente al ascent que la fuente reserva para cubrir el resto del alfabeto. El navegador cuenta ese margen de sobra al centrar la caja igual que cuenta cualquier otro, así que el hueco vacío de arriba empuja la tinta hacia abajo. El desajuste no es un bug de la fuente ni del componente: son dos cosas distintas, la caja tipográfica reservada y la tinta realmente dibujada, y nada garantiza que compartan centro.

Los dos descriptores que corrigen la caja sin tocar la tinta

ascent-override y descent-override son descriptores de @font-face, igual que size-adjust: solo existen dentro de una cara con src que resuelva, y cada uno sustituye el valor que la fuente trae en sus tablas métricas por el porcentaje que se declare, calculado sobre el tamaño del em. Lo que no tocan es la geometría del glifo. Un carácter dibujado ocupa exactamente los mismos contornos, en el mismo sitio respecto a la línea base, antes y después de aplicar cualquiera de los dos. Lo único que cambia es cuánto espacio reserva el navegador por encima y por debajo de esa línea base para construir la caja de línea, que es justo el valor que usan line-height: normal, vertical-align y el centrado por flexbox o grid.

@font-face {
  font-family: 'p4-typo-font-family-cta';
  src: local('Quicksand');
  ascent-override: 72%;
  descent-override: 4%;
  line-gap-override: 0%;
}

Redeclaro la familia de marca bajo un nombre distinto, solo para la etiqueta del botón, y reparto el 100% del em de otra manera: menos reserva arriba, la mínima indispensable abajo. Los tres valores salen de medir la tinta real del texto con canvas.measureText(), no de una proporción a ojo: es el mismo método que detallo más abajo, aplicado antes de dar cualquier cifra por buena.

El token de tamaño no se entera: font-size sigue en el valor que traía el componente y ningún rem cambia de cómputo. El de interlineado sí se entera, y de una manera que la primera vez me dio un resultado idéntico al de partida y me hizo dudar de si el override estaba haciendo algo: si el botón fijaba line-height: 1 o cualquier valor propio en píxeles, el texto no se movía ni un pixel al aplicar ascent-override y descent-override, exactamente el mismo sitio que sin ellos. La razón es que los dos descriptores sustituyen los valores que consulta el cómputo de line-height: normal; un line-height fijado a mano no vuelve a consultar esos valores, así que el override queda sin ningún efecto aunque el navegador lo haya aceptado sin error. La consecuencia de arquitectura es que el componente que reciba esta cara tiene que dejar line-height: normal, y cualquier espaciado adicional que antes resolviera un line-height fijo pasa al padding del contenedor, no a la línea.

El límite: lo que se sale de la caja nueva, se recorta

La reserva de espacio no es decorativa, es la que protege la tinta que se sale del rango habitual. Calibré el ascent-override al 72% mirando solo REGISTRARSE, sin una sola tilde. El mismo canvas.measureText() sobre CONFIRMACIÓN o ENVÍA da un ascent real del 90% del em: la vocal acentuada sube diecinueve puntos porcentuales más que cualquier letra de REGISTRARSE. Si esa misma cara se reutiliza sin revisar en un botón secundario cuya etiqueta lleve tilde, la Í o la Ó se dibujan por encima del límite que la fuente declara ahora. El glifo se sigue dibujando completo (el motor de render no descarta contornos por esto) pero cualquier contenedor que calcule su alto a partir de esa caja, o que lleve overflow: hidden ajustado a ella, corta la parte de tinta que sobresale. En un catálogo de botones en español, con tildes en la mitad de los verbos de acción (ENVÍA, CONFIRMACIÓN, ), el riesgo no es una excepción rara, es la norma.

La consecuencia de arquitectura es que esta cara no es la familia base del sistema, es una variante de un solo uso: la reservo para el catálogo de etiquetas de botón que verifiqué sin tilde y la excluyo de cualquier lugar donde pueda aparecer una mayúscula acentuada. La alternativa que descarté fue aplicar el mismo ascent-override/descent-override a la familia de marca completa, con un valor intermedio que no recortara ni las mayúsculas acentuadas ni el resto de componentes: la probé y el centrado volvía a quedar a medias, porque el valor que centra REGISTRARSE no es el que centra CONFIRMACIÓN. Pagar una cara @font-face adicional, con su propio nombre y su propio alcance de unicode-range si hiciera falta acotarlo, sale más barato que un centrado aproximado en todo el sistema.

Cómo verifico que no hay recorte antes de dar el valor por bueno

No hay atajo fiable a ojo, porque un recorte de un par de píxeles en una tilde pasa desapercibido en una revisión visual rápida y aparece tres sprints después en un informe de un usuario que vio ENVA donde debía decir ENVÍA. La comprobación que uso es sencilla y repetible: renderizar, en el mismo componente y con la misma cara, la etiqueta más exigente del catálogo de botones aprobado (la que lleve la vocal acentuada más alta, no la más frecuente) y comparar getBoundingClientRect() del contenedor con el alto real de tinta que reporta un canvas mediante actualBoundingBoxAscent y actualBoundingBoxDescent. Si la tinta medida excede la caja del contenedor en cualquiera de los dos sentidos, el valor de override está demasiado ajustado y hay que devolverle margen, aunque eso separe un poco más el centrado óptico del centrado perfecto.

Cuándo no merece la pena tocar estos descriptores

Si el botón admite cualquier texto que escriba un equipo de producto, no un catálogo de etiquetas aprobado por contenido, el coste de mantener una cara aparte y de volver a medir cada vez que aparece una etiqueta nueva supera lo que se gana en centrado. Ahí la corrección razonable sigue siendo un ajuste de padding-block asimétrico sobre el propio componente, que no toca ninguna cara de fuente y que un diseñador puede volver a calibrar sin tocar CSS de @font-face. ascent-override y descent-override valen su coste cuando el conjunto de etiquetas está cerrado y se puede verificar por completo, como el de un sistema de botones con contenido gobernado, no como corrección genérica de cualquier desajuste vertical.