/* ════════════════════════════════════════════════════════════════════════
   DESIGN TOKENS — malo-cms
   ════════════════════════════════════════════════════════════════════════
   Fuente ÚNICA de los valores de diseño del producto. Lo cargan cms.html,
   login.html y device-preview.html.

   Vive aquí y no dentro de cada HTML porque el CMS no tiene build step: cada
   archivo es autocontenido, así que sin una hoja compartida el sistema no
   puede cruzar de uno a otro. Antes de esto, --accent y los --beta-* estaban
   DUPLICADOS a mano entre cms.html y login.html: coincidían solo porque
   alguien los alineó, y nada impedía que se separaran en silencio.

   Regla: si un valor de diseño se usa en más de un lugar, va aquí. Si algo
   no encaja en ningún token, el valor nuevo se define AQUÍ con su razón —
   nunca suelto en el componente.

   ⚠️ El middleware sirve .css sin autenticación (ASSET_RE), lo cual es
   necesario porque login.html es público. No hay riesgo: son valores de
   diseño, no secretos. Nunca poner nada sensible en este archivo.
   ════════════════════════════════════════════════════════════════════════ */

/* --accent = acento de la APP (focus, drop zones, badges, checkboxes).
   --beta    = ámbar SEMÁNTICO del banner de beta (aviso/temporal), no marca.
               Mismos valores que login.html para que ambas pantallas coincidan. */
:root {
  --accent: #E8A433; --accent-dim: rgba(232,164,51,0.12);
  --beta: #FFB224; --beta-bg: #402300; --beta-border: #B77700;
  --beta-h: 40px;

  /* ═══ MOTION ═══════════════════════════════════════════════════════
     Fuente única de duraciones y curvas. Los valores NO son inventados:
     salen de auditar lo que ya dominaba el archivo (150ms tenía 18 usos,
     280ms nueve, 400ms cinco). La escala nombra lo que ya existía y
     absorbe los one-offs que se habían ido colando (0.22s, 0.42s, 220ms).

     ── Duración: se elige por DISTANCIA, no por importancia ────────────
     A igual duración, más recorrido se lee como más rápido. Un accordion
     que crece 40px y un sheet que sube 400px NO pueden durar lo mismo.

       --dur-fast   Sin desplazamiento: hover, color, opacity, borde.
                    También TODA salida — lo que el usuario ya decidió no
                    debe hacerlo esperar. Cabe dentro del unmount de
                    useModalState (180ms); no lo subas sin revisar ese
                    setTimeout o los modals se cortarán a media salida.
       --dur-base   Recorrido corto: expand/collapse de secciones,
                    rotación del chevron, transforms pequeños.
       --dur-slow   Recorrido largo: bottom sheets, borrado de filas.

     ── Curva ───────────────────────────────────────────────────────────
       --ease-standard  Default. Entra decidido y asienta.
       --ease-exit      Solo salidas. Acelera hacia afuera.
       --ease-out       Decelerate puro. Respuesta inmediata a un click.
       --ease-sheet     Recorridos largos. Decelerate largo: se siente
                        asentado en vez de frenado.

     ⛔ REGLA DURA — expand/collapse SIEMPRE con
        `grid-template-rows: 0fr → 1fr`. NUNCA `max-height`.

        `max-height` obliga a adivinar un tope, y ese magic number falla
        de las dos maneras posibles:
          • Si QUEDA CORTO, el elemento se clampea de golpe en el primer
            frame → brinco al inicio.
          • Si SE PASA, la altura visible es min(alto real, max-height), o
            sea que no se mueve nada hasta que el max-height baja del alto
            real. La animación arranca muerta y colapsa en la cola: se
            percibe como "no hubo transición" aunque técnicamente corrió.
        El truco de grid anima al alto REAL del contenido. No hay número
        que adivinar, así que no hay nada que se pueda desincronizar.
        Ya es el patrón de .section-content y .desc-reveal.

     ⛔ Al colapsar a 0, TODO lo que ocupa layout debe llegar a 0: alto,
        margin, padding Y border. Un `border: 1px` que sobrevive deja el
        elemento en 2px, y al desmontarlo todo lo de abajo brinca esos
        2px. Es el clásico "mini jump" al final de un borrado.

     Al agregar motion nuevo: usa un token. Si ninguno encaja, el valor
     nuevo se define AQUÍ con su razón — nunca suelto en el componente. */
  --dur-fast: 150ms;
  --dur-base: 280ms;
  --dur-slow: 400ms;

  --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
  --ease-exit:     cubic-bezier(0.4, 0, 1, 1);
  --ease-sheet:    cubic-bezier(0.32, 0.72, 0, 1);
  /* Decelerate PURO: arranca a máxima velocidad y desacelera. Se distingue de
     --ease-standard en el arranque — standard tiene ease-in inicial (recorre
     3px en el primer paso de un colapso de 77px) y este recorre 28px. Para
     algo que responde a un click directo, esa diferencia es la que separa
     "respondió" de "se trabó". Medido con la Web Animations API pausando la
     animación y muestreando currentTime. */
  --ease-out:      cubic-bezier(0, 0, 0.2, 1);

  /* ═══ Z-INDEX ══════════════════════════════════════════════════════
     Antes había once valores, cada uno usado UNA vez: 1, 2, 10, 39, 40,
     50, 79, 80, 100, 200, 9999. Los pares 39/40 y 79/80 son la firma del
     problema — alguien poniendo "uno más que aquello" sin saber qué había
     debajo. Y el 9999 es el clásico "ya me rindo".

     Los bugs de z-index son de los más difíciles de debuggear porque no
     ves el sistema, solo el síntoma: algo quedó detrás de algo. Con capas
     nombradas la pregunta al agregar algo deja de ser "¿qué número es más
     grande?" y pasa a ser "¿a qué capa pertenece esto?".

     El orden relativo es EXACTAMENTE el que ya existía — esto renombra,
     no reordena. Los saltos de 100 dejan espacio para insertar capas sin
     volver a la aritmética de 39/40.

     ⚠️ Un z-index solo compite con sus HERMANOS dentro del mismo stacking
     context. --z-local es para eso: elevarse dentro del propio contenedor.
     Un valor alto NO saca a un elemento de un ancestro con transform,
     opacity < 1 o contain — eso crea un stacking context nuevo y el hijo
     queda atrapado. Si algo "no sube" con z-index, el problema está en un
     ancestro, no en el número. */
  --z-local:      2;    /* dentro de una card: badges, header sticky del modal */
  --z-nested:     10;   /* overlay contenido (save prompt dentro del modal) */
  --z-banner:     100;  /* beta banner */
  --z-nav:        200;  /* barra de tabs */
  --z-header:     300;  /* header sticky */
  --z-floating:   400;  /* botón flotante (deshacer) */
  --z-bar:        500;  /* barra de acciones de selección */
  --z-modal:      600;  /* ModalShell y su scrim */
  --z-toast:      700;  /* avisos — encima del modal a propósito */
  --z-fullscreen: 800;  /* editor de crop, que tapa todo */

  /* ═══ ELEVACIÓN ════════════════════════════════════════════════════
     Había seis sombras, todas distintas y cada una a medida. Una sombra
     no es decoración: comunica QUÉ TAN ARRIBA está algo. Seis valores
     únicos significa que esa señal no decía nada consistente.

     Tres alturas, dos direcciones. La dirección importa: los elementos
     anclados al borde inferior (bottom sheets, barra de selección)
     proyectan hacia ARRIBA, porque la luz sigue viniendo de arriba y la
     sombra cae del lado contrario al que el elemento se despega.

     Las de arriba usan más alpha (0.4 contra 0.3) a propósito: aparecen
     sobre contenido y necesitan separar más; las de abajo suelen caer
     sobre el fondo, que ya es oscuro.

     Se fusionaron `0 1px 4px .4` y `0 1px 3px .3` en --shadow-sm: a 1px
     de offset la diferencia es imperceptible, y eran el mismo caso de uso
     (controles chicos). Se conservó un valor existente, no se inventó uno.

     ⚠️ NO todo box-shadow es elevación. El anillo del tab activo usa
     `0 0 0 2px` — eso es un ring, no una sombra, y no lleva token de
     elevación porque no comunica altura. */
  --shadow-sm:    0 1px 3px rgba(0,0,0,0.3);   /* controles: toggle, slider thumb */
  --shadow-md:    0 4px 16px rgba(0,0,0,0.3);  /* flotante: botón de deshacer */
  --shadow-lg:    0 8px 24px rgba(0,0,0,0.3);  /* overlay alto: toast */
  --shadow-up-md: 0 -4px 24px rgba(0,0,0,0.4); /* anclado abajo: barra de selección */
  --shadow-up-lg: 0 -8px 32px rgba(0,0,0,0.4); /* anclado abajo: bottom sheet */

  /* ═══ TIPOGRAFÍA ═══════════════════════════════════════════════════
     Ocho tamaños sueltos: 10, 11, 12, 13, 14, 15, 16, 20. Los 40 usos de
     13px decían que ya había un tamaño base; solo faltaba nombrarlo.

     El 10px se absorbió en --text-xs (11px): un solo uso y a ese tamaño
     un pixel no se percibe.

     ⚠️ --text-input NO es un paso de la escala, es SEMÁNTICO. Los cuatro
     usos de 16px son todos campos de formulario, y no es coincidencia:
     debajo de 16px iOS Safari hace ZOOM automático al enfocar un input.
     Si alguien lo "unifica" a 14 para que se vea más parejo, rompe la
     captura en móvil de una forma que en desktop es invisible. Por eso
     vive aparte y con nombre propio.

     ⚠️ PENDIENTE: --text-md (14px) y --text-lg (15px) tienen 11 usos cada
     uno y son perceptualmente casi lo mismo. Una escala real no tiene dos
     pasos separados por 1px — eso es drift. No se fusionaron aquí porque
     cambiaría cómo se ve en 22 lugares y esa es una decisión de diseño,
     no de limpieza. Tokenizarlos primero vuelve el problema VISIBLE en
     vez de dejarlo escondido entre números sueltos. */
  --text-xs:    11px;  /* micro: tag de beta, metadatos */
  --text-sm:    12px;  /* secundario */
  --text-base:  13px;  /* base — 40 usos */
  --text-md:    14px;
  --text-lg:    15px;
  --text-xl:    20px;
  --text-input: 16px;  /* ⚠️ semántico: menos de esto y iOS hace zoom */

  /* ═══ RADIUS ═══════════════════════════════════════════════════════
     Esta familia ya casi era sistema sola: 4px con 37 usos contra un
     puñado de sueltos. Solo hubo que absorber dos y nombrar el resto.

     Dos fusiones, ambas por SIGNIFICADO y no por parecido numérico:

     · 5px → --radius-sm. Era el checkbox (caja de 20×20). A ese tamaño
       la diferencia contra 4px no se percibe, y 5 no era una decisión:
       era drift.

     · 1px → --radius-pill. Era el track del slider de crop, que mide 2px
       de alto. Un radio de 1px sobre 2px de alto ES completamente
       redondeado — la intención no era "esquina chiquita", era "pill".
       Los browsers recortan el radio a la mitad de la caja, así que 99px
       da el mismo resultado y además dice lo que quiere decir.

     --radius-full (50%) es distinto de --radius-pill (99px): 50% siempre
     da un círculo/elipse siguiendo la proporción de la caja; el pill
     redondea los extremos y deja los lados rectos. No son intercambiables
     en cajas no cuadradas. */
  --radius-none: 0;
  --radius-sm:   4px;   /* base — 37 usos (absorbe el 5px del checkbox) */
  --radius-md:   6px;   /* contenedores de icono */
  --radius-lg:   8px;
  --radius-pill: 99px;  /* toggle, track del slider */
  --radius-full: 50%;   /* círculos: mod-dot, thumbs */

  /* ═══ GAP ══════════════════════════════════════════════════════════
     Escala de base 4. Los pasos de 6px y 10px se FUSIONARON hacia arriba
     (6→8, 10→12) y el 30px suelto subió a 32: 35 usos en total.

     Esto SÍ cambia cómo se ve — a diferencia de tokenizar, que solo
     renombra. Se hizo a propósito y redondeando hacia arriba: la interfaz
     gana aire en vez de apretarse.

     Por qué 6 y 10 sobraban: en una escala de base 4 no aportan un paso
     distinguible, solo obligan a decidir entre dos valores casi iguales
     cada vez que se agrega algo. Menos pasos = menos decisiones = menos
     drift.

     El 2xs (2px) se queda aunque no sea múltiplo de 4: es para casos
     muy apretados (icono pegado a su texto) donde 4px ya separa de más.

     Padding y margin NO están tokenizados — ver la nota del commit. */
  --gap-none: 0;
  --gap-2xs:  2px;   /* icono + texto, muy pegado */
  --gap-xs:   4px;
  --gap-sm:   8px;   /* base — 49 usos */
  --gap-md:   12px;
  --gap-lg:   16px;
  --gap-xl:   32px;
}
