/* ═══════════════════════════════════════
   ▓ COMPONENTE · Button — gemelo vanilla ▓
   v1.0 · 2026-08-23. Ficha ÚNICA: `design-system/COMPONENTE_BOTON.md` (los estados están ahí:
   `COMPONENTE_BUTTON.md` se fusionó en ella el 2026-08-23 y quedó en archive/2026-08-23/)
   Kit: `catalogo-cd/components/Button.jsx`

   La pieza que DISPARA una acción. Es la más montada del sistema y era la única sin gemelo:
   18 archivos de las tres apps la redibujaban a mano, y cada copia salía un poco distinta.

   Siete variantes, en orden de jerarquía:
     · sales       — CTA de VENTA (esmeralda). Apps públicas: cotizador, mi-exante.
     · primary     — el operativo (ink slate). Por defecto. Backoffice.
     · secondary   — el compañero del primario: superficie + contorno. Atrás, Cancelar, Editar.
     · link/ghost  — terciarios, sin contorno ni relleno.
     · destructive — contorno rojo: peligro SUAVE.
     · danger      — relleno rojo SÓLIDO: peligro fuerte (Salir, eliminar definitivo).

   Tres tamaños: sm 32 · md 40 (canónico) · lg 48.

   REQUISITOS DEL ANFITRIÓN:
     1. Los tokens del canon (`colors_and_type.css`).
     2. `box-sizing: border-box`.
   ═══════════════════════════════════════ */

/* ── BASE ────────────────────────────────────────────────────────────────────
   Lo que comparten las siete variantes y los tres tamaños. La familia NO se sostiene en el
   relieve —que sólo llevan los rellenos sólidos—: se sostiene en esta base compartida.

   ⛔ EL MOVIMIENTO SE DECLARA POR PROPIEDADES NOMBRADAS, NUNCA `all`
   (`COMPONENTE_MOVIMIENTO.md` regla 5). Un `transition: all` anima también lo que nadie
   quiso animar —el alto al cargar, el ancho al cambiar la etiqueta— y eso no se ve como un
   defecto: se ve como una interfaz que va a tirones. */
.exds-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font-family: var(--font-sans, "Inter", -apple-system, system-ui, sans-serif);
  font-weight: 500;
  border-radius: var(--radius-button, 10px);
  border: 1px solid transparent;
  cursor: pointer;
  white-space: nowrap;
  user-select: none;
  /* ⛔ VA EN LA BASE, y no sobra: esta pieza se monta tambien sobre un enlace <a> — la propia
     ficha lo pide cuando la accion es navegar de verdad. Sin esta linea el navegador le pone
     SU subrayado, y el boton sale con una raya que nadie escribio. Medido el 2026-08-28: hoy
     no muerde a nadie (cero enlaces con la clase exds-btn en las tres apps) y mordia al
     primero que hubiera, que iba a ser el «Solicitar» de la barra publica.
     *Un valor por defecto del navegador no se queja: se ve.* */
  text-decoration: none;
  transition: background-color var(--motion-fast,120ms) var(--ease-out, cubic-bezier(0.32, 0.72, 0, 1)), border-color var(--motion-fast,120ms) var(--ease-out, cubic-bezier(0.32, 0.72, 0, 1)), color var(--motion-fast,120ms) var(--ease-out, cubic-bezier(0.32, 0.72, 0, 1)), box-shadow var(--motion-fast,120ms) var(--ease-out, cubic-bezier(0.32, 0.72, 0, 1)), transform var(--motion-fast,120ms) var(--ease-out, cubic-bezier(0.32, 0.72, 0, 1));
}
/* ⛔ LA GUARDA EN LA BASE — sin ella un botón con `hidden` SIGUE PINTADO Y SIGUE PULSABLE.
   `[hidden] { display:none }` vive en la hoja del NAVEGADOR, y el `display:inline-flex` de
   arriba, por ser de autor, le gana. El marcado canónico de la Fila de reenvío ya monta así
   su botón (`<button class="exds-btn" … hidden>`) y `fila-reenvio.css` lo salva con una
   guarda de su clase propia — pero eso obliga a que CADA uso se acuerde. Se guarda la BASE
   para que no haya uso que se pueda olvidar. Medido el 2026-08-25.
   *Y en un botón el defecto no es sólo estético: un control invisible-pero-presente sería un
   clic posible; uno visible-que-debía-estar-oculto es una acción ofrecida sin querer.* */
.exds-btn[hidden] { display: none; }

/* ── LOS TRES TAMAÑOS · `data-size` ──────────────────────────────────────────
   ⛔ LA ALTURA CANÓNICA ES 40 y el resto son excepciones con motivo, no gustos: `sm` para
   barras densas, `lg` para un héroe. La deuda medida del 2026-07-11 tenía tres alturas base
   distintas en las tres apps (40 · 44 · una calculada por relleno) — de ahí sale este eje.
   ⛔ Y NUNCA DOS MEDIDAS DEL MISMO BOTÓN A UN PALMO DE DISTANCIA (COMPONENTE_BOTON.md §DO/NO;
   la regla venía de COMPONENTE_BUTTON.md, fusionada ahí el 2026-08-23).

   `md` se declara DOS VECES —sin atributo y con él— para que quien monta pueda omitirlo:
   un botón sin `data-size` es un botón de 40, que es lo que el 90 % de los casos quiere. */
.exds-btn:not([data-size]),
.exds-btn[data-size="md"] {
  height: 40px;
  padding: 0 20px;
  font-size: var(--fs-button, 14px);
}
.exds-btn[data-size="sm"] {
  height: 32px;
  padding: 0 14px;
  font-size: var(--fs-button-sm, 13px);
}
.exds-btn[data-size="lg"] {
  height: 48px;
  padding: 0 24px;
  font-size: var(--fs-button-lg, 15px);
}

/* ── SÓLO ÍCONO · `data-icon-only` ───────────────────────────────────────────
   Cuadrado: el ancho iguala al alto de su tamaño, sin relleno lateral y sin hueco entre
   hijos porque no hay dos hijos que separar.
   ⛔ EXIGE `aria-label`: no hay texto visible que un lector de pantalla pueda leer, así que
   sin él el botón se anuncia como «botón» a secas. Va DESPUÉS de los tamaños a propósito —
   comparten especificidad y aquí gana el que se declara al final, que es el que debe ganar. */
.exds-btn[data-icon-only] {
  padding: 0;
  gap: 0;
}
.exds-btn[data-icon-only]:not([data-size]),
.exds-btn[data-icon-only][data-size="md"] { width: 40px; }
.exds-btn[data-icon-only][data-size="sm"] { width: 32px; }
.exds-btn[data-icon-only][data-size="lg"] { width: 48px; }

/* ── FORMA · `data-shape` ────────────────────────────────────────────────────
   Por defecto el botón lleva `--radius-button`. `circle` y `pill` son las dos únicas
   desviaciones, y sólo tienen sentido con un contenido que las justifique (un ícono solo
   para el círculo; una etiqueta corta para la pastilla). */
.exds-btn[data-shape="circle"] { border-radius: 50%; }
.exds-btn[data-shape="pill"]   { border-radius: var(--radius-pill, 999px); }

/* ══ LAS SIETE VARIANTES · `data-variant` ═══════════════════════════════════
   ⛔ EL RELIEVE NO SE REPARTE POR IMPORTANCIA, SINO POR SI HAY RELLENO QUE ILUMINAR
   (Ricardo, 2026-07-19 · «el componente de botón secundario con relleno blanco se aligera»).
   Sólido → `--relieve-btn`. Contorno, fantasma y enlace → SIN relieve, y no es una omisión.

   ⛔ Y AQUÍ VIVE LA REGLA DE LA VISTA GLOBAL (INC-059 · no-negociable #7-bis): un elemento
   CONTENIDO nunca pesa más que su contenedor. El origen fue exactamente éste — Ricardo notó
   que los botones de una fila del expediente pesaban más que la card que los contenía. La
   hipótesis natural, «el borde del botón es muy oscuro», se MIDIÓ y era falsa: el borde da
   1,47:1 y el texto 18,51:1. Lo que competía era el RELIEVE. *Quitarlo no cuesta
   accesibilidad, porque lo que identifica al botón es su texto, no su sombra.*
   Corolario que se hereda: una acción REPETIDA por fila va en ghost o link, nunca en un
   control bordeado.

   ⛔ Peso B (Ricardo, 2026-07-22): los sólidos son COLOR PLANO + relieve de susurro. Sin
   gradiente. Sobre un color saturado el relieve pone SOMBRA, no luz: subirle el tope al
   verde deja de ser el verde de marca.

   `primary` se declara DOS VECES —sin atributo y con él— para que un botón sin `data-variant`
   sea el operativo, que es el que se quiere por omisión. */

/* PRIMARIO · ink slate. El contraste más alto del sistema con su texto blanco.
   ⛔ NO es negro puro: `--color-action-ink` migró a #21272C el 2026-07-22, mismo matiz frío
   que la familia notice. Las superficies oscuras y el texto (`--fg1`) NO migraron. */
.exds-btn:not([data-variant]),
.exds-btn[data-variant="primary"] {
  background: var(--color-action-ink, #21272C);
  color: var(--fg-inverse, #FFFFFF);
  border-color: transparent;
  box-shadow: var(--relieve-btn, 0 1px 2px rgba(15,20,25,0.18));
}

/* VENTAS · esmeralda. El CTA por el que entra el dinero, y SÓLO ése.
   ⛔ SU CONTRASTE ES UNA DESVIACIÓN DECLARADA, no un descuido — `GATE-ALLOW:
   contraste-cta-ventas` (Ricardo, 2026-08-13). Medido en el sitio real: blanco sobre
   #0B8F6B da 4,07:1. Eso DESPEJA el 3:1 que WCAG 1.4.11 pide a un CONTROL y NO despeja el
   4,5:1 que 1.4.3 pide a texto de tamaño normal. Se descartó oscurecer el verde —cambiar el
   botón de conversión de tres apps por dos décimas— y subir la letra —resolver una norma
   rompiendo una proporción—. *Durante meses viajó como 4,54:1, un número que no sale de ese
   color, y con él se dio por cerrada una excepción que seguía abierta: la diferencia entre
   una decisión y un descuido no es el resultado, es que esté escrita y tenga dueño.*
   ⛔ Y NO ES EL VERDE SEMÁNTICO RETIRADO. Aquél (#1FAE5F, validar/aprobar) murió como botón
   el 2026-07-22 y su trabajo lo hace hoy `primary`. Éste es el verde de MARCA. */
.exds-btn[data-variant="sales"] {
  background: var(--color-emerald, #0B8F6B);
  color: var(--fg-inverse, #FFFFFF);
  border-color: transparent;
  box-shadow: var(--relieve-btn, 0 1px 2px rgba(15,20,25,0.18));
}

/* SECUNDARIO · superficie + contorno, PLANO.
   ⛔ EL BORDE CLARO ES DELIBERADO Y YA ESTÁ DECIDIDO — NO SE «ARREGLA».
   `ALCANCE-FUENTE: Ricardo 2026-07-18 — «regresar el outline de los botones secundarios al
   que estaba antes» (borde claro), aceptando que el filo sea sutil.` Sabe lo que compra:
   #D1D5DB da 1,47:1 y queda por debajo del 3:1 que WCAG 1.4.11 pide a un control. La
   afordancia del secundario no descansa en el borde sino en su texto ink (18,51:1) y en su
   posición junto al primario; **el borde agrupa, no señala** (marco negociable,
   `JERARQUIA_Y_ACCESIBILIDAD.md`). El #8B92A1 que se probó queda archivado como historia,
   y sigue vigente para inputs, cards y radios: el token NO se tocó, sólo los botones.
   ⛔ `box-shadow: none` VA ESCRITO AQUÍ, no heredado de `--relieve-control`. Ese token lo
   comparten dos usos OPUESTOS —este botón, que va plano, y la perilla del switch, que
   necesita levante—; el 2026-07-22 se le devolvió un valor por el switch y el secundario se
   re-sombreó solo. *Un token compartido entre dos necesidades opuestas es una trampa: un
   cambio para uno alcanza al otro en silencio.* */
.exds-btn[data-variant="secondary"] {
  background: var(--color-surface, #FFFFFF);
  color: var(--fg1, #0F1419);
  border-color: var(--color-border-default, #D1D5DB);
  box-shadow: none;
}

/* LINK y GHOST · terciarios. Comparten declaración exacta —transparentes, texto ink, sin
   contorno y sin relieve— y lo que los separa es el HOVER: el enlace se subraya, el fantasma
   lava el fondo. Se agrupan porque son el mismo reposo, no porque se parezcan. */
.exds-btn[data-variant="link"],
.exds-btn[data-variant="ghost"] {
  background: transparent;
  color: var(--fg1, #0F1419);
  border-color: transparent;
  box-shadow: none;
}

/* DESTRUCTIVO · contorno rojo, peligro SUAVE. Es un secundario que se tiñe al hover: sin
   relleno no hay nada que elevar, así que tampoco lleva relieve. */
.exds-btn[data-variant="destructive"] {
  background: transparent;
  color: var(--color-error, #DA342B);
  border-color: var(--color-error, #DA342B);
  box-shadow: none;
}

/* PELIGRO · relleno rojo SÓLIDO, peligro fuerte: Salir, eliminar definitivo (Ricardo,
   2026-07-22). Convive con `destructive` a propósito: el contorno es para lo reversible y el
   sólido para lo que no lo es. El rojo es el ÚNICO color no neutro que un botón puede llevar
   además del verde de venta. */
.exds-btn[data-variant="danger"] {
  background: var(--color-error, #DA342B);
  color: var(--fg-inverse, #FFFFFF);
  border-color: transparent;
  box-shadow: var(--relieve-btn, 0 1px 2px rgba(15,20,25,0.18));
}

/* ══ HOVER ══════════════════════════════════════════════════════════════════
   ⛔ LA GUARDA DE TRES `:not()` NO ES CEREMONIA — es la traducción exacta de lo que el kit
   React hacía con estado (`hover && !isOff`, con `isOff = disabled || loading`). Un botón
   apagado o trabajando **no promete nada al pasarle el ratón por encima**, y `:hover` sí
   dispara sobre un `<button disabled>`. Las tres puertas son las tres formas de estar
   apagado: el atributo nativo, el `aria-disabled` del canon y el trabajo en curso.
   ⛔ El hover del ink es #31363B **desde el token**. La deuda del 2026-07-11 midió TRES
   valores tecleados a mano y distintos en las tres apps para este mismo hover. */
.exds-btn:not([data-variant]):hover:not(:disabled):not([aria-disabled="true"]):not([data-loading]),
.exds-btn[data-variant="primary"]:hover:not(:disabled):not([aria-disabled="true"]):not([data-loading]) {
  background: var(--color-action-ink-hover, #31363B);
}
.exds-btn[data-variant="sales"]:hover:not(:disabled):not([aria-disabled="true"]):not([data-loading]) {
  background: var(--color-emerald-hover, #0A7D5D);
}
.exds-btn[data-variant="secondary"]:hover:not(:disabled):not([aria-disabled="true"]):not([data-loading]),
.exds-btn[data-variant="ghost"]:hover:not(:disabled):not([aria-disabled="true"]):not([data-loading]) {
  background: var(--color-bg-subtle, #F2F4F6);
}
/* El enlace no tiene fondo que lavar: su única señal es el subrayado, y el desplazamiento de
   4 px lo separa de las descendentes de la letra para que no se lea como tachado. */
.exds-btn[data-variant="link"]:hover:not(:disabled):not([aria-disabled="true"]):not([data-loading]) {
  text-decoration: underline;
  text-underline-offset: 4px;
}
.exds-btn[data-variant="destructive"]:hover:not(:disabled):not([aria-disabled="true"]):not([data-loading]) {
  /* ⛔ LA TINTA SUBE CON EL FONDO (2026-08-29). Al pasar el ratón este botón se pone su PROPIO
     tinte detrás, y ahí el rojo base mide 3,97 — no llega al mínimo de 4,5. El rojo VIEJO lo
     tapaba con 5,08, así que el defecto existía y era invisible: *cambiar un color no crea los
     defectos de contraste, los DESTAPA.* `--color-error-deep` da 4,55, que es además lo que el
     canon manda para texto sobre su propio tinte.
     ⚠ HOY ESTÁ LATENTE: `data-variant="destructive"` no se monta en ninguna pantalla —0
     apariciones en `app/` y `admin/`—, pero la hoja se sirve y la regla espera. Se arregla ahora
     porque un defecto latente se cobra el día que alguien usa la variante, y ese día nadie va a
     estar mirando esto. */
  background: var(--color-error-tint, #FEE8E7);
  color: var(--color-error-deep, #D61005);
  border-color: var(--color-error-deep, #D61005);
}
.exds-btn[data-variant="danger"]:hover:not(:disabled):not([aria-disabled="true"]):not([data-loading]) {
  /* ⛔ ERA `--color-error-deep`, que en la familia del rojo es MÁS CLARO que la base — así que
     pasar el ratón ACLARABA el botón (2026-08-29). Ahora usa su propio escalón de estado, con
     los mismos pasos de luz que la escala de ventas. Ver `--color-error-hover` en la hoja de
     tokens. *El rojo era el único color de botón sin escala de estados propia, y por eso pedía
     prestado un token cuyo trabajo era otro: pintar texto sobre tinte.* */
  background: var(--color-error-hover, #CE2D24);
}

/* ══ PULSADO ════════════════════════════════════════════════════════════════
   El hundido es de TODAS las variantes y de una sola medida: 0,97. Es el acuse de recibo del
   gesto — lo único que confirma que el dedo llegó antes de que el trabajo empiece.
   ⚠ En el kit React esto lo movía un estado de ratón (`onMouseDown`/`onMouseUp`); aquí lo
   mueve `:active`, que además responde al ENTER y al ESPACIO del teclado. Es la misma señal
   para más gente, no una señal distinta. */
/* ⛔ EL PULSADO DEL BOTÓN DE PELIGRO, que no existía (2026-08-29). Las otras dos familias de
   botón sí tienen su escalón —`--color-action-ink-press` y `--color-emerald-press`— y el rojo se
   quedaba en el color del hover al pulsarlo: la pulsación no se veía. */
.exds-btn[data-variant="danger"]:active:not(:disabled):not([aria-disabled="true"]):not([data-loading]) {
  background: var(--color-error-press, #AF261E);
}
.exds-btn:active:not(:disabled):not([aria-disabled="true"]):not([data-loading]) {
  transform: scale(.97);
}

/* ══ APAGADO ════════════════════════════════════════════════════════════════
   ⛔ RELLENO GRIS SÓLIDO, NUNCA OPACIDAD. Un botón a media tinta se lee como un botón mal
   pintado; uno gris se lee como un botón apagado. Canon desde `v0.6`.

   ⛔ LAS DOS PUERTAS, Y LA BUENA ES `aria-disabled` (2026-08-15). El `disabled` nativo trae
   dos defectos REPRODUCIDOS: el foco se cae al `<body>` si el botón se apaga estando
   enfocado (10 de 10 veces), y el botón desaparece del recorrido del teclado sin explicar
   nada. Con `aria-disabled="true"` no se mueve de sitio ni del orden de tabulación: sigue
   ahí, se ve apagado, se anuncia como no disponible y el foco no puede caerse porque nunca
   deja de ser enfocable. Se aceptan las dos porque el kit React emite la nativa; **en una app
   vanilla se monta con `aria-disabled`**, y entonces impedir el clic es trabajo del JS.
   ⛔ Y pulsarlo no puede ser un clic muerto: si está apagado porque falta algo, pulsarlo
   lleva a lo que falta. El atributo EXPLICA, la función IMPIDE.
   ⛔ EL FOCO NO SE TOCA aquí: quien navega con teclado tiene que seguir viendo dónde está.

   Va DESPUÉS de las variantes porque comparte especificidad con ellas y tiene que ganar —
   es exactamente el defecto que Ricardo cazó en el calendario el 2026-08-21, donde un
   primario apagado conservaba el borde tinta y el relieve del botón vivo porque la regla de
   apagado ponía fondo y tinta pero **no reiniciaba `border-color` ni `box-shadow`**. Alcance
   medido entonces: 52 botones. *Un estado que hereda a medias no se ve como un estado nuevo:
   se ve como un botón que se puede pulsar y no responde.* */
.exds-btn:disabled,
.exds-btn[aria-disabled="true"] {
  background: var(--color-bg-subtle, #F2F4F6);
  color: var(--fg4, #B8BDC7);
  border-color: var(--color-border-subtle, #E5E7EB);
  box-shadow: none;
  cursor: not-allowed;
}

/* ══ TRABAJANDO ═════════════════════════════════════════════════════════════
   ⛔ CARGANDO **CONSERVA SU COLOR**: no se pone gris. Apagado y ocupado son cosas distintas
   —«no se puede» contra «ya va»— y pintarlas igual las confunde. Lo único que cambia es el
   cursor: deja de invitar sin llegar a prohibir. */
.exds-btn[data-loading] { cursor: default; }

/* ── EL ANILLO · el botón con VERBO ──────────────────────────────────────────
   ⛔ ESTE ANILLO ES SÓLO PARA LOS BOTONES CON VERBO —Guardar, Enviar, Firmar—, que no tienen
   un ícono propio que girar. El de ACTUALIZAR gira el suyo (más abajo). *Añadir un segundo
   círculo al lado del primero es pagar dos veces por el mismo mensaje.*
   `currentColor` no es pereza: hace que el anillo sea del color del texto de CUALQUIER
   variante sin declarar siete anillos. */
.exds-btn-spinner {
  width: 15px;
  height: 15px;
  border-radius: 50%;
  border: 2px solid currentColor;
  border-top-color: transparent;
  display: inline-block;
  animation: exds-btn-spin .7s linear infinite;
}

/* ── EL ÍCONO QUE AVANZA · `iconoDeslizante` ─────────────────────────────────
   El ícono trailing direccional avanza 4 px al hover y 7 al pulsar. Es el patrón Slide del
   catálogo de movimiento aplicado a un ícono, y va SÓLO en `transform`: nunca reflowa, así
   que no altera el alto ni el ancho del botón.
   ⛔ REGLAS DE USO QUE EL CSS NO PUEDE IMPONER: sólo con íconos DIRECCIONALES (una lupa que
   se desplaza no significa nada) · sólo en el ÚLTIMO hijo (un leading que avanza se va
   contra su etiqueta) · reservado a CTAs de avance y de venta, nunca en `destructive`,
   `danger`, `ghost` ni `link` · una por pantalla.
   ⛔ Los dos valores tecleados (4 px y 7 px) son DESPLAZAMIENTOS DE RECORRIDO, no espaciado
   de maquetación: el sistema no tokeniza distancias de movimiento. */
.exds-btn-desliz {
  display: inline-flex;
  transition: transform var(--motion-base,250ms) var(--ease-emphasis, cubic-bezier(0.34, 1.26, 0.58, 1));
}
.exds-btn:hover:not(:disabled):not([aria-disabled="true"]):not([data-loading]) .exds-btn-desliz {
  transform: translateX(4px);
}
.exds-btn:active:not(:disabled):not([aria-disabled="true"]):not([data-loading]) .exds-btn-desliz {
  transform: translateX(7px);
}

/* ── EL GIRO · la variante ACTUALIZAR (Ricardo, 2026-08-10 · INC-404) ────────
   Cuando el botón es de actualizar, lo que gira es SU PROPIO ícono y no aparece anillo. La
   razón es económica: el ícono de actualizar YA es una flecha circular y YA está en pantalla.
   Ritmo 0,7 s lineal en bucle — el mismo del anillo, no una velocidad inventada. La clase de
   estado va en el BOTÓN y no en el ícono, así también puede vestir el cursor. */
@keyframes exds-btn-spin { to { transform: rotate(360deg); } }
.exds-btn-refresh-ic { transform-origin: 50% 50%; }
.exds-btn-refrescando .exds-btn-refresh-ic { animation: exds-btn-spin .7s linear infinite; }

/* ⛔ APAGAR EL MOVIMIENTO NO ES DEJAR SIN SEÑAL — se SUSTITUYE por una estática (INC-405).
   La primera versión sólo hacía `animation: none`, y se midió el resultado: con
   `prefers-reduced-motion` el usuario pulsaba y **no percibía absolutamente nada**. Era
   además una PÉRDIDA respecto del gesto anterior, que no tenía guarda. *Respetar una
   preferencia de movimiento es dar la información SIN movimiento, no quitarla.* Lo destapó
   una auditoría de ojos frescos. Se conserva tal cual: no lo «simplifiques». */
@media (prefers-reduced-motion: reduce) {
  .exds-btn-refrescando .exds-btn-refresh-ic { animation: none; opacity: .45; }
  .exds-btn-refrescando { cursor: progress; }
  /* ⛔ `.exds-btn-spinner` YA PARABA — el catch-all de abajo (`.exds-btn, .exds-btn *`) le
     gana con `!important` y se comprobó en el navegador: `animationName` sale `none`. Esta
     línea es DECLARATIVA, no funcional: `gate_reduced_motion.js` audita por TEXTO (no
     entiende la cascada, lo dice su propia cabecera) y necesita ver el nombre de la clase
     dentro de un bloque de guarda para no marcarla falsa-roja. El medio anillo (borde con un
     lado transparente) queda como forma ESTÁTICA y sigue leyéndose «cargando» sin girar —
     no hace falta 1ms, esto no es un `opacity` que pueda desaparecer. */
  .exds-btn-spinner { animation: none; }
}

/* ── EL RELEVO DE ETIQUETA · `data-hecho` ────────────────────────────────────
   El botón que dice «Guardado» NO ES OTRO BOTÓN: es el botón haciendo algo. La etiqueta
   se va mientras la siguiente llega, con el icon-swap homologado del catálogo —el mismo
   de cifra→palomita y el de la celda de cruce—: 250ms, `ease-in-out`, escala de entrada
   .25, desenfoque 2px. El botón no queda vacío en ningún fotograma.

   ⛔ LA SUPERPOSICIÓN VA EN UN `<span>` DENTRO DEL BOTÓN, NUNCA SOBRE EL BOTÓN, y esto se
   descubrió MIRÁNDOLO, no leyéndolo (2026-08-24). La primera versión ponía `display:grid`
   sobre el propio `.exds-btn` para superponer las dos etiquetas. Este archivo ya declara
   `display:flex` en `.exds-btn` con la MISMA especificidad, así que ganaba el que se
   cargara después: el resultado computado era `flex`, los dos rótulos se ponían UNO AL
   LADO DEL OTRO —«Guardar» a 50px y «Guardado» aplastado a 15— y el botón se iba a 149px
   contra los 90 de su hermano, con el texto pegado al borde derecho.

   El `grid-area:1/1` seguía escrito y era INERTE: dentro de un contenedor flex no
   significa nada. Y LOS DOS GATES DE ARCHIVO DABAN VERDE, porque el kit y el gemelo
   decían exactamente lo mismo — *los dos decían lo mismo y los dos estaban mal.*

   Además de romperse, era lo que la regla de oro prohíbe: deformar una pieza canónica
   para calzar un diseño (INC-210). Con la capa interna el botón sigue siendo el botón,
   intacto, y la superposición vive donde le toca.

   ⛔ Y EL ANCHO NO CAMBIA AL RELEVAR, que es la mitad del punto: la rejilla RESERVA la más
   larga de las dos etiquetas y se queda ahí en los dos estados. Medido en navegador el
   2026-08-23, tamaño `sm`: **91 px con «Guardar» y 91 px con «Guardado»**. Un «Guardar»
   sin relevo mide 80 en el mismo sitio — esos 11 px de más SON la reserva, y son el precio
   de que no se mueva. *Un botón que se ensancha al confirmar empuja a su vecino justo en el
   momento en que el usuario está mirando el resultado.*
   ⛔ El número depende de la TIPOGRAFÍA CARGADA: con la letra de reserva salen 83 y 94.
   Quien vuelva a medirlo tiene que esperar a `document.fonts.ready` o medirá otra fuente. */
.exds-btn-swap {
  --icon-swap-dur: var(--motion-base, 250ms);
  --icon-swap-ease: ease-in-out;
  --icon-swap-blur: 2px;
  --icon-swap-start-scale: .25;
  display: grid;
  place-items: center;
}
.exds-btn-swap > * { grid-area: 1 / 1; }
.exds-btn-swap-out,
.exds-btn-swap-in {
  transition: opacity var(--icon-swap-dur) var(--icon-swap-ease),
              transform var(--icon-swap-dur) var(--icon-swap-ease),
              filter var(--icon-swap-dur) var(--icon-swap-ease);
}
.exds-btn-swap-in {
  opacity: 0;
  transform: scale(var(--icon-swap-start-scale));
  filter: blur(var(--icon-swap-blur));
}
/* El atributo va en el BOTÓN, no en la capa: así también puede vestir el cursor o
   cualquier otra cosa del botón el día que haga falta, sin mover la superposición. */
.exds-btn[data-hecho="true"] .exds-btn-swap-out {
  opacity: 0;
  transform: scale(var(--icon-swap-start-scale));
  filter: blur(var(--icon-swap-blur));
}
.exds-btn[data-hecho="true"] .exds-btn-swap-in {
  opacity: 1;
  transform: scale(1);
  filter: blur(0);
}

/* ── FOCO ────────────────────────────────────────────────────────────────────
   ⛔ ANILLO NEUTRO DE 2 px, SÓLO CON TECLADO, JAMÁS ANULADO (`COMPONENTE_FOCO.md`).
   `:focus-visible` y no `:focus`: el anillo al hacer clic con el ratón es ruido, y quitarlo
   del todo —lo que hacía el cotizador— es dejar a quien navega con teclado sin saber dónde
   está. La segunda regla apaga el contorno del navegador SÓLO en el caso del ratón, que es
   el único que no lo necesita. */
.exds-btn:focus-visible { outline: 2px solid var(--color-border-focus, #6E7378); outline-offset: 2px; }
.exds-btn:focus:not(:focus-visible) { outline: none; }

/* En papel una sombra es una mancha gris: no hay luz que la justifique. */
@media print { .exds-btn { box-shadow: none !important; } }

/* La guarda general de movimiento reducido. El descendiente universal alcanza también al
   anillo de carga, que se queda QUIETO pero VISIBLE: el arco sigue dibujado y el rótulo ya
   está en gerundio, así que la señal no desaparece — sólo deja de moverse. */
@media (prefers-reduced-motion:reduce){ .exds-btn, .exds-btn *{ transition:none !important; animation:none !important } }
@media (prefers-reduced-motion:reduce){ .exds-btn-desliz{ transform:none !important } }
/* ══ LA ZONA DE DEDO DE 44px ═══════════════════════════════════════════════════
   DECISION-FUENTE: Ricardo, 2026-08-27 — decisión 2.4 de «LAS 19 DECISIONES DE LA
   HOMOLOGACIÓN» (ESTADO.md): «La zona de dedo de 44px se cosecha al botón del catálogo».
   Cosechada de .nav-cta del cotizador (app/css/cotizador.css), que es el único sitio del
   sistema donde esta regla estaba ESCRITA en código en vez de sólo en una ficha.

   ⛔ SE VE DE 32 (o 40, o 48) Y SE TOCA COMO DE 44. La caja NO crece: crecería la escala
   entera y con ella el peso del botón en cada pantalla. Crece un ::after INVISIBLE.

   ⛔ Y NUNCA CON RELLENO — es la forma que la ficha del botón de ícono ya fijaba
   (COMPONENTE_BOTON.md § botón de ícono: «con ::after, nunca con padding»). El relleno
   mueve el contenido, cambia el ancho de la caja y rompe la alineación con lo que tiene al
   lado; un ::after superpuesto no mueve un solo píxel de lo que se ve.

   ⛔ SÓLO BAJO @media (pointer: coarse) — el dedo, no el ratón. Con ratón el puntero es
   preciso y una zona invisible de 44px sólo serviría para robarle el clic al vecino.

   ⛔ EL ANCHO SE ESTIRA SÓLO EN EL BOTÓN CUADRADO. Un botón con etiqueta ya mide más de 44
   de ancho (el más estrecho, sm, son 28 de relleno + su texto), así que ahí sobra estirar y
   además invadiría al de al lado. El de sólo ícono mide 32·40·48 de ancho y sí lo necesita.

   Alcance: TODAS las medidas por debajo de 44 — sm (32), md (40) y el cuadrado en sus tres
   tamaños. lg mide 48 y no necesita nada, así que no se le da: una zona táctil MENOR que la
   caja visible sería peor que ninguna. */
@media (pointer: coarse) {
  /* ⛔ EL ANCLAJE SE DECLARA AQUÍ, y no se da por hecho. La base de .exds-btn NO lleva
     position:relative — se comprobó al escribir esto, después de haberlo supuesto. Sin
     ancla, la capa se mide contra el primer antepasado posicionado, que puede ser la
     pantalla entera: la zona de dedo aterrizaría en cualquier sitio y NADA se vería mal.
     *Un ::after mal anclado no se queja: sale a 44px de un origen que no es el suyo.*
     Va DENTRO de la consulta de medio: con ratón el botón no necesita anclar nada. */
  .exds-btn:not([data-size="lg"]) { position: relative; }
  .exds-btn:not([data-size="lg"])::after {
    content: "";
    position: absolute;
    left: 0; right: 0;
    top: 50%;
    transform: translateY(-50%);
    height: 44px;
    /* ⛔ AQUÍ DECÍA pointer-events:none, Y ESO APAGABA LA ZONA ENTERA (medido 2026-08-27).
       El argumento era: «invisible y sin capturar nada por su cuenta; los eventos los sigue
       recibiendo el botón porque la capa es hija suya, no una hermana que se le pone encima».
       La premisa es falsa. Ser hija no reparte el toque al padre: pointer-events:none vuelve
       la capa INALCANZABLE, y el punto sigue de largo hasta lo que haya DETRÁS — que fuera de
       la caja del botón ya no es el botón, es su contenedor.
       Medido sobre app/index.html a 390x844 con dedo, mismo punto y misma corrida:
       el CTA de hoy (sin esta línea) responde en 6 de 9 puntos; con ella, 3 de 9. El punto a
       4px por encima del botón devolvía DIV.nav-inner. Declaraba 44 y entregaba 32.
       Y el repositorio ya lo sabía: las otras cuatro zonas de dedo del proyecto
       (cotizador.css .nav-cta, .gAcc-av, .exds-acc-trig, .logo-svg, y el gemelo ia-bolt) NO
       llevan pointer-events. Ésta era la única.
       *Una zona de gesto con pointer-events:none es decorativa: el medidor que busca la REGLA
       la ve, el dedo no.* Lo que el comentario temía —robarle el toque al vecino— ya lo evita
       la consulta de medio (sólo dedo) y el que sólo se estire a lo ancho el botón cuadrado. */
  }
  /* Sólo el cuadrado se estira a lo ancho: el que lleva etiqueta ya pasa de 44. */
  .exds-btn[data-icon-only]:not([data-size="lg"])::after {
    left: 50%; right: auto;
    width: 44px;
    transform: translate(-50%, -50%);
  }
}

/* == SOBRE SUPERFICIE OSCURA · data-superficie="oscura" en un ANTEPASADO =============
   DECISION-FUENTE: Ricardo, 2026-08-28 — decision 1 de
   plans/decisiones/_decision_nav_cotizador_2026-08-27.html: «si a tus recomendaciones»
   sobre «¿el boton y el boton de icono aprenden a pisar suelo oscuro?».

   Es la misma mecanica que ya usa la pestana (tabs.css) y la que gobierna
   COMPONENTE_SUPERFICIE_OSCURA.md: la condicion se DECLARA en el contenedor, no se adivina
   del color de fondo. Mirando el pixel de detras no se puede saber si un fondo es oscuro a
   proposito (una barra de marca) o por accidente (una foto, un degradado).

   ⛔ DIFERENCIA CON LA PESTANA, y es la que decide el selector: alli el atributo va en la
   PROPIA pieza, porque la barra de pestanas ES la superficie. Aqui el boton solo la PISA —
   quien la declara es el contenedor de mas arriba. Por eso el selector es descendente y no
   de atributo propio.

   ⛔ LO QUE **NO** CAMBIA, Y ES LA PARTE PRINCIPAL DE LA REGLA: los rellenos solidos
   —primary, sales, danger, destructive— no se tocan. La ficha de superficie oscura lo dice
   con todas las letras: «una pieza que pinta su propio relleno oscuro no lo es; ahi el
   oscuro es de la pieza y muere con ella, nada hereda nada». El «Solicitar» esmeralda de la
   barra publica se ve identico dentro y fuera de una superficie oscura, y ese es el
   comportamiento correcto: su fondo lo pone el, no el suelo.
   Los que si cambian son los tres que NO pintan fondo propio y por eso dependen del suelo:
   contorno, fantasma y enlace. */

/* Contorno · en claro es superficie blanca + borde gris. Sobre oscuro una caja blanca
   pareceria un primario: se vacia el relleno y el borde pasa a la divisoria del canon. */
[data-superficie="oscura"] .exds-btn[data-variant="secondary"] {
  background: transparent;
  color: var(--color-ink-on-dark, #FFFFFF);
  border-color: var(--color-hairline-dark, rgba(255, 255, 255, 0.08));
}

/* Fantasma y enlace · en claro son texto ink sobre nada. Traduccion 1:1 de la escala de
   tinta, que es la primera de las tres traducciones del canon de superficie oscura. */
[data-superficie="oscura"] .exds-btn[data-variant="ghost"],
[data-superficie="oscura"] .exds-btn[data-variant="link"] {
  color: var(--color-ink-on-dark, #FFFFFF);
}

/* El lavado de fondo del contorno y del fantasma. ⛔ Blanco al 10 %, NUNCA un gris solido:
   un gris opaco TAPA el suelo y la zona deja de ser «la misma superficie un poco levantada»
   para pasar a ser «otra cosa pegada encima». Es el mismo token que la pildora de pestana. */
[data-superficie="oscura"] .exds-btn[data-variant="secondary"]:hover:not(:disabled):not([aria-disabled="true"]):not([data-loading]),
[data-superficie="oscura"] .exds-btn[data-variant="ghost"]:hover:not(:disabled):not([aria-disabled="true"]):not([data-loading]) {
  background: var(--color-bg-subtle-on-dark, rgba(255, 255, 255, 0.10));
}

/* ⛔ EL ENLACE SE SUBRAYA — no se desvanece, y esto es una decision, no una omision.
   DECISION-FUENTE: Ricardo, 2026-08-28 — decision 2 de la misma hoja.
   La barra publica lo tenia a mano con opacity .75 al apuntarlo. Se retira: un elemento que
   se APAGA cuando lo apuntas dice «me estoy yendo», no «puedes pulsarme».
   *El argumento del suelo oscuro justificaba cambiar la PESTANA, porque su marca es una
   subraya en tinta y sobre negro es invisible. La subraya de un ENLACE si se ve sobre negro,
   asi que aqui no hay caso que justifique la excepcion.*
   No se escribe ninguna regla: la hereda del hover de la variante enlace, mas arriba. */

/* Foco · el token neutro (#6E7378) da 3,92:1 sobre #0F1419 — despeja el 3:1 que WCAG 1.4.11
   pide a un CONTROL, asi que NO estaria mal. Se sube igualmente a la tinta blanca (18,51:1)
   porque el anillo es lo unico que ve quien navega con teclado, y sobre negro un gris medio
   se lee como parte del cromo. *El minimo legal es el suelo, no el objetivo.* */
[data-superficie="oscura"] .exds-btn:focus-visible {
  outline-color: var(--color-ink-on-dark, #FFFFFF);
}
/* == Y LA VUELTA ATRAS · data-superficie="clara" ====================================
   ⛔ ESTO NO ES SIMETRIA POR ELEGANCIA: es el agujero que abrio la PRIMERA superficie
   oscura de verdad, el mismo dia que se construyo (2026-08-28).
   La mecanica declara el suelo en un ANTEPASADO, y un antepasado alcanza a TODOS sus
   descendientes. La barra publica del cotizador es negra, pero cuelgan de ella dos paneles
   BLANCOS —el mega-desplegable y la hoja de telefono—, y el atributo se filtraba dentro.
   Medido en el navegador: un boton secundario dentro de la hoja salia con texto
   rgb(255,255,255) sobre un fondo rgb(255,255,255). Blanco sobre blanco.
   *Una condicion que se hereda necesita una forma de dejar de heredarse, o el primer
   contenedor que la declare se la impone a media aplicacion.*
   Se pone DESPUES del bloque oscuro a proposito: misma especificidad, gana el ultimo, asi
   que el suelo mas CERCANO al elemento es el que manda — que es lo que uno espera. */
[data-superficie="clara"] .exds-btn[data-variant="secondary"] {
  background: var(--color-surface, #FFFFFF);
  color: var(--fg1, #0F1419);
  border-color: var(--color-border-default, #D1D5DB);
}
[data-superficie="clara"] .exds-btn[data-variant="ghost"],
[data-superficie="clara"] .exds-btn[data-variant="link"] {
  color: var(--fg1, #0F1419);
}
[data-superficie="clara"] .exds-btn[data-variant="secondary"]:hover:not(:disabled):not([aria-disabled="true"]):not([data-loading]),
[data-superficie="clara"] .exds-btn[data-variant="ghost"]:hover:not(:disabled):not([aria-disabled="true"]):not([data-loading]) {
  background: var(--color-bg-subtle, #F2F4F6);
}
[data-superficie="clara"] .exds-btn:focus-visible {
  outline-color: var(--color-border-focus, #6E7378);
}


