/* ═══════════════════════════════════════════════════════════════════════════
   MONOLITO GRIS — implementación de la hoja de componentes de Claude Design

   Reglas que no se rompen en ningún componente nuevo:
     · Monocromo estricto. Ni un solo color con saturación. La jerarquía se
       resuelve por VALOR y por TAMAÑO, nunca por matiz.
     · Cero sombras y cero bordes. La separación de capas es #FFFFFF sobre
       #E5E5E5 y nada más. Prueba para un componente nuevo: ¿lo puedo separar
       del fondo solo con blanco? Si no, está mal dimensionado.
     · O es círculo perfecto, o es cápsula, o es línea recta. No hay radios
       intermedios "de tarjeta" salvo el 20 que el propio sistema deriva.
     · Etiquetas siempre en MAYÚSCULAS con tracking — y en español mexicano
       los acentos en caja alta SÍ se ponen.

   El semáforo lumbar no usa rojo: CLAUDE.md admite "fíltralos o márcalos en
   rojo" y se eligió filtrar, que además es más seguro. Ver dominio/semaforo.js.
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
  --bg-base:        #E5E5E5;
  --surface:        #FFFFFF;
  --text-primary:   #333333;
  --text-secondary: #ADADAD;
  --text-disabled:  #CACACA;
  --track:          #D0D0D0;
  --overlay:        #ACACAC;
  --on-dark:        #FFFFFF;

  --margen: 30px;
  --contenido: 380px;

  /* Ø300 es la medida del entregable, pero es un techo y no un valor fijo: el
     disco encoge por ANCHO y por ALTO sin dejar de ser círculo perfecto.
       · 100dvw − 100  deja 50 px de aire a cada lado en pantallas angostas.
       · 100dvh − 360  es lo que ocupa todo lo demás de Hoy (barra 80, pie 76,
         riel 68, área segura 61, separación 30) más un respiro.
     En un iPhone 17 Pro Max —440 × 956— las dos ramas dan de sobra y el disco
     se queda en 300. En Safari con sus barras a la vista quedan ~745 de alto y
     también entra. Recién por debajo de 660 empieza a ceder, que es justo el
     punto en el que dejaría de caber. */
  --hero: min(300px, calc(100dvw - 100px), calc(100dvh - 360px));
  --chip: 32px;
  --marca: 10px;
  --icono: 24px;
  --toque: 44px;
  --alto-control: 64px;
  --capsula-platillo: 186px;

  /* ── LOS DOS ÚNICOS COLORES DEL PROYECTO ─────────────────────────────────
     Rompen la regla §1 del sistema —"monocromo estricto, ni un solo color con
     saturación"— y lo hacen con aprobación explícita de Alejandro
     (2026-08-06), registrada en docs/decisiones/ADR-006.

     Muestreados del video cuadro a cuadro, no elegidos: son los valores
     exactos del anillo de la referencia en el instante del estallido.

     Viven SOLO aquí, durante 168 ms, una vez al día, cuando se cierra la
     proteína. En ningún otro sitio de la app hay un píxel saturado. */
  --destello-naranja: #FEA800;
  --destello-azul:    #008DFE;

  --r-redondo: 9999px;
  --r-tarjeta: 20px;
  --r-hoja: 24px;

  --dur-instant: 120ms;
  --dur-quick: 240ms;
  --dur-base: 360ms;
  --dur-expressive: 560ms;
  --dur-ritual: 3000ms;

  --ease-standard:   cubic-bezier(0.4, 0, 0.2, 1);
  --ease-decelerate: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-gravity:    cubic-bezier(0.55, 0.06, 0.68, 0.19);

  /* spring/settle del sistema: rigidez 220, amortiguación 22, masa 1.
     Resuelto de la ecuación del oscilador amortiguado (ζ = 0.742) y muestreado
     en 24 puntos. Sobrepasa 3.1 % y se asienta en 533 ms — el sistema permite
     hasta 6 % y prohíbe las curvas `back` y `bounce`, así que esto entra.
     Un cubic-bezier no puede expresar un rebote; `linear()` sí. */
  --spring-settle: linear(
    0.0000, 0.0460, 0.1554, 0.2943, 0.4396, 0.5764, 0.6963, 0.7958,
    0.8741, 0.9329, 0.9747, 1.0025, 1.0193, 1.0280, 1.0309, 1.0301,
    1.0270, 1.0229, 1.0184, 1.0140, 1.0102, 1.0069, 1.0044, 1.0024, 1.0010
  );
  --dur-settle: 533ms;

  /* ── MEDIDO DE LA REFERENCIA ────────────────────────────────────────────
     Los tres valores de abajo NO son estimaciones ni curvas elegidas por
     gusto: salen de medir cuadro a cuadro una grabación de Not Boring Habits
     con ffmpeg. El método y los números están en docs/referencia-movimiento.md.

     `--spring-golpe` es literalmente la medición: cada muestra es el radio del
     disco en un cuadro, normalizado. No aproxima la curva con una bezier —
     ES la curva, muestreada a 30 Hz. Sobrepasa 8.9 % y asienta en 700 ms.

     Alejandro aprobó explícitamente que esto exceda el tope de 6 % de
     sobrepaso del sistema (2026-08-06). Es la única excepción viva. */
  --spring-golpe: linear(
    0.000, 0.177, 0.329, 0.468, 0.582, 0.684, 0.772, 0.848,
    0.924, 0.975, 1.013, 1.051, 1.076, 1.089, 1.089, 1.089,
    1.076, 1.063, 1.051, 1.025, 1.013, 1.000
  );
  --dur-crecer: 700ms;

  /* Salir acelera —como caer— y entrar frena. La referencia usa curvas
     OPUESTAS para las dos mitades, y es la diferencia entre "se va" y "llega". */
  --dur-encoger: 200ms;

  /* El relleno del disco: 780 ms con una S casi simétrica. A la mitad del
     tiempo lleva 39 % del recorrido; --ease-standard llevaría 60 %. */
  --dur-relleno: 780ms;
  --ease-relleno: cubic-bezier(0.45, 0, 0.55, 1);

  /* ── LAS DOS VELOCIDADES ─────────────────────────────────────────────────
     El patrón que aparece al medir las cinco animaciones de la referencia:
     NO tiene una duración, tiene dos, y la diferencia es de cinco veces.

       lo que ENTREGA algo   150-170 ms   aviso, teclado, hoja
       lo que TRANSFORMA
       el objeto héroe       780-900 ms   relleno, cambio de métrica

     Un aviso o un teclado son información que LLEGA: cuanto antes esté, antes
     se puede seguir. El disco es el objeto de la pantalla: su recorrido ES el
     contenido, y correrlo lo convierte en un parpadeo.

     Este proyecto tenía una sola —`--dur-base`, 360 ms— para todo: lenta para
     lo que entrega y rápida para lo que importa. Exactamente al revés. */
  --dur-aviso: 170ms;
  --dur-hoja: 150ms;

  /* Apertura de la app: 280 ms de revelado, frenando fuerte. */
  --dur-apertura: 280ms;

  /* El estallido: 333 ms, medidos del evento 2 del video (cuadros 497-517). */
  --dur-estallido: 333ms;

  /* El disco NUNCA llega a cero al cambiar: se queda en el 14 % del radio.
     Desaparecer del todo se lee como "se fue"; encogerse, como "sigue aquí,
     cambiando". */
  --escala-punto: 0.14;

  --display: Geist, -apple-system, "Segoe UI", "Helvetica Neue", sans-serif;
  --mono: "Geist Mono", ui-monospace, "SF Mono", Consolas, monospace;
}

@font-face {
  font-family: Geist; font-style: normal; font-weight: 400 800; font-display: swap;
  src: url("/estatico/fuentes/geist.woff2") format("woff2");
}
@font-face {
  font-family: "Geist Mono"; font-style: normal; font-weight: 400 500; font-display: swap;
  src: url("/estatico/fuentes/geist-mono.woff2") format("woff2");
}

* { box-sizing: border-box; }

/* Los enlaces no se subrayan: en este sistema lo tocable se distingue por su
   superficie —la fila blanca, la cápsula, el chip— y no por el subrayado, que
   además es la pieza que marca la métrica activa del riel. Dos significados
   para la misma línea de 2 px es exactamente lo que hay que evitar. */
a { color: inherit; text-decoration: none; }

/* `hidden` gana sobre cualquier `display` de clase. Sin esto, un elemento con
   [hidden] y una clase que declara display —`.bloque` es grid— se queda a la
   vista: así aparecía el temporizador de descanso en 0:00 al abrir la sesión,
   antes de existir ningún descanso que contar. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--bg-base);   /* NUNCA cambia, en ningún estado ni transición */
  color: var(--text-primary);
  font-family: var(--display);
  font-size: 16px;
  line-height: 21px;
  -webkit-font-smoothing: antialiased;
  overscroll-behavior-y: none;
}

/* ── LO QUE DELATA QUE ES UNA PÁGINA ──────────────────────────────────────
   Instalada en la pantalla de inicio, la app se juzga contra las nativas, y
   estos cuatro gestos del navegador son los que rompen la ilusión:

     · el destello azul al tocar cualquier cosa
     · el menú de "copiar / compartir" al dejar el dedo apoyado
     · la selección de texto al arrastrar sobre una etiqueta
     · el zoom al tocar dos veces seguidas

   `touch-action: manipulation` es además el que quita el retraso de 300 ms
   que el navegador espera por si el segundo toque llega.

   La selección se recupera donde SÍ sirve: los números y las notas se pueden
   copiar. Bloquearlo todo por prolijidad quitaría algo que se usa. */
* {
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;
}

button, a, .fila, .platillo, .chip, .tecla, .stepper, .metrica-item,
.barra-superior, .riel-metricas {
  -webkit-user-select: none;
  user-select: none;
  touch-action: manipulation;
}

.t-metrica, .t-hero, .t-cuerpo, .fila-valor, .entrada-numero, .nota {
  -webkit-user-select: text;
  user-select: text;
}

/* ── TIPOGRAFÍA ──────────────────────────────────────────────────────────── */

.t-hero   { font-size: 28px; font-weight: 800; letter-spacing: -0.01em; line-height: 32px; }
.t-cuerpo { font-size: 16px; font-weight: 400; line-height: 21px; max-width: 268px; text-wrap: pretty; }

.t-metrica {
  font-family: var(--mono); font-size: 17px; font-weight: 400;
  letter-spacing: 0.04em; line-height: 20px;
}
.t-contador {
  font-family: var(--mono); font-size: 15px; font-weight: 400;
  letter-spacing: 0.04em; line-height: 18px;
}
.t-etiqueta {
  font-family: var(--mono); font-size: 12px; font-weight: 500;
  letter-spacing: 0.06em; line-height: 14px; text-transform: uppercase;
}
.t-pestana {
  font-family: var(--mono); font-size: 14px; font-weight: 500;
  letter-spacing: 0.10em; line-height: 18px; text-transform: uppercase;
}

.gris  { color: var(--text-secondary); }
.tenue { color: var(--text-disabled); }

/* ── ESTRUCTURA — tres bandas en FLUJO, nunca superpuestas ────────────────
   Medidas tomadas del entregable, sobre su lienzo de 440 × 956:
     barra superior   top 80   ·  alto 24   ·  márgenes 30
     objeto héroe     top 326  ·  300 × 300 → centro y = 476
     riel de métricas top 827  ·  alto 68   → termina en 895

   Esos 80 de arriba y esos 61 de abajo (956 − 895) NO son números sueltos: son
   el área segura de un iPhone con isla dinámica más un margen.
       80 = 59 (isla)            + 21
       61 = 34 (barra de inicio) + 27
   Por eso van con env(safe-area-inset-*) y no clavados: en el navegador de
   escritorio los insets valen 0 y las bandas suben, que es lo correcto.

   ─── LO QUE ESTABA MAL ────────────────────────────────────────────────────
   Las dos bandas eran `position:absolute` —una centrada al 50 %, la otra
   anclada al fondo— sin ninguna relación entre ellas. Mientras la banda
   inferior fue un riel de 68 px funcionó; en cuanto creció (Peso trae traza,
   fila y dos botones; Yo trae ocho listas) invadió al héroe y el texto quedó
   encimado, ilegible y en parte fuera de la pantalla.

   Ahora las tres bandas son hijas de un flex en columna: el héroe se lleva el
   espacio sobrante y las otras dos ocupan lo suyo. Superponerse ya no es un
   estado alcanzable, no un caso que haya que recordar probar.

   El costo es que el héroe queda centrado en el HUECO entre barras y no sobre
   la pantalla completa: en Hoy son 11 px más arriba que en el entregable. Es
   un intercambio deliberado — 11 px imperceptibles a cambio de que ninguna
   pantalla pueda volver a encimarse.
   ───────────────────────────────────────────────────────────────────────── */

:root {
  --seguro-arriba: env(safe-area-inset-top, 0px);
  --seguro-abajo:  env(safe-area-inset-bottom, 0px);

  --banda-arriba: calc(var(--seguro-arriba) + 21px);
  --banda-abajo:  calc(var(--seguro-abajo) + 27px);
}

.pantalla {
  position: relative;
  min-height: 100dvh;
  max-width: 440px;
  margin: 0 auto;
  padding: 0 var(--margen);
  display: flex;
  flex-direction: column;
}

/* `height` incluye el relleno (border-box), así que el alto total es
   relleno + 24 y la caja de contenido mide exactamente los 24 del icono. Los
   blancos de toque de 44 se desbordan 10 px por lado y quedan centrados sobre
   esa línea, igual que en el entregable.

   Antes esto era `height:24px` + `padding-top:24px`: con border-box la caja de
   contenido salía de 0 px de alto y la barra entera se montaba en el borde
   superior de la pantalla, encima de la isla dinámica. */
.barra-superior {
  flex: none;
  position: relative; z-index: 2;
  height: calc(var(--banda-arriba) + var(--icono));
  padding-top: var(--banda-arriba);
  display: flex; align-items: center; justify-content: space-between;
}
/* ── RIEL DE PESTAÑAS — se desplaza, y la abierta se centra ───────────────
   Antes eran tres nombres fijos más un menú de tres puntos, y no cabía una
   cuarta sección: por eso COMIDA vivía detrás de unos puntos que no dicen a
   dónde llevan. Con el riel desplazable el número de secciones deja de estar
   limitado por el ancho de la pantalla.

   El hueco lateral que permite centrar la PRIMERA y la ÚLTIMA pestaña son dos
   separadores HIJOS, y no `padding` del riel. La diferencia no es de estilo:
   el relleno forma parte de la caja, así que con `padding-inline: 50%` el
   tamaño mínimo del riel crecía 380 px, se comía todo el ancho de la barra y
   empujaba la hamburguesa fuera de la pantalla. Como hijos, el hueco vive
   DENTRO del desplazamiento, que es donde hace falta.

   El desplazamiento es una consecuencia, no el gesto: quien manda es el toque,
   y `navegacion.js` centra la activa sola. */
.barra-superior nav {
  display: flex; align-items: center; gap: 20px;
  flex: 1 1 0; min-width: 0;
  overflow-x: auto; overscroll-behavior-x: contain;
  scrollbar-width: none;
  /* SIN scroll-snap a propósito: el navegador reajustaba al punto de anclaje
     más cercano después de cada centrado programático, y las dos primeras
     pestañas quedaban 65 px descentradas mientras las tres últimas caían
     perfectas. Aquí quien decide dónde para el riel es `navegacion.js`; el
     anclaje era una segunda autoridad discutiendo con la primera. */
  /* Las pestañas que salen del riel se desvanecen en vez de cortarse en seco:
     un corte limpio parece un error de maquetación, un desvanecido dice "hay
     más de esto hacia allá". A 20 px apenas se notaba; 40 es la mitad de un
     nombre corto, que es lo que hace falta para leerlo como degradado. */
  -webkit-mask: linear-gradient(90deg, transparent, #000 40px, #000 calc(100% - 40px), transparent);
          mask: linear-gradient(90deg, transparent, #000 40px, #000 calc(100% - 40px), transparent);
}
.barra-superior nav::-webkit-scrollbar { display: none; }
.barra-superior nav::before,
.barra-superior nav::after { content: ""; flex: none; width: 50%; }
.barra-superior nav .pestana { flex: none; }

/* ── GLIFOS DE 2 PT ──────────────────────────────────────────────────────────
   Se dibujan, no se escriben. El "+" tipográfico de cada fuente trae su propio
   grosor y su propia caja, y no coincide con el trazo de 2 pt del resto de los
   iconos. Dibujarlos mantiene un solo grosor en toda la barra.              */

.glifo-mas { position: relative; width: var(--icono); height: var(--icono); }
.glifo-mas::before, .glifo-mas::after {
  content: ""; position: absolute; background: var(--text-disabled);
  transition: background var(--dur-instant) var(--ease-standard);
}
.glifo-mas::before { top: 11px; left: 0; width: 24px; height: 2px; }
.glifo-mas::after  { left: 11px; top: 0; width: 2px; height: 24px; }
.icono-desnudo:active .glifo-mas::before,
.icono-desnudo:active .glifo-mas::after { background: var(--text-primary); }

.glifo-menu { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
.glifo-menu i { height: 2px; background: var(--text-disabled); }
.glifo-menu i:nth-child(1) { width: 26px; }
.glifo-menu i:nth-child(2) { width: 18px; }
.glifo-menu i:nth-child(3) { width: 22px; }

.glifo-puntos {
  display: flex; flex-direction: column; gap: 3px;
  padding: 10px 4px; margin: -10px 0; min-height: auto;
}
.glifo-puntos i { width: 4px; height: 4px; background: var(--text-disabled); }

/* Pantalla de OBJETO: el héroe se queda con todo el espacio sobrante y flota
   en su centro; la banda inferior ocupa lo suyo, abajo. El vacío entre las dos
   no se rellena nunca: si hay más contenido, es una pantalla de LISTA.

   Las tres filas `1fr auto 1fr` centran EL DISCO, no el conjunto del disco con
   su pie. No es lo mismo: centrando el conjunto, el disco sube ~80 px y deja un
   hueco muerto entre el pie y el riel. El pie cuelga en la fila de abajo, que
   es exactamente lo que hace el entregable —disco en el centro, métrica
   secundaria colgando— y lo que el sistema llama objeto suspendido (§5.2).

   Lo que queda arriba del disco entra en la primera fila pegado a su borde
   inferior: en Hoy son los registros pendientes de enviar, que el entregable
   pone en y=250, sobre el disco, porque son estado y no una acción. */
/* `min-height: min-content` es la red de seguridad: sin ella, flex COMPRIME la
   región por debajo de su contenido cuando no alcanza el espacio, y el pie del
   héroe se desborda encima de la banda inferior. Con ella, el contenido empuja
   y lo que cede es el disco, que para eso tiene su min() en --hero. */
.centro {
  flex: 1; min-height: min-content;
  display: grid;
  grid-template-rows: 1fr auto 1fr;
  justify-items: center;
}
.centro > .hero     { grid-row: 2; }
.centro > .hero-pie { grid-row: 3; align-self: start; }
.centro > :not(.hero):not(.hero-pie) { grid-row: 1; align-self: end; margin-bottom: 40px; }

.banda-inferior {
  flex: none;
  display: grid; gap: 16px; justify-items: center;
  width: 100%;
  padding-bottom: var(--banda-abajo);
}

/* Pantalla de LISTA: el contenido no cabe en el hueco, así que se le da su
   propia región con scroll interno. Las barras nunca se mueven ni se colapsan
   al hacer scroll (§8.4: la retícula no se anima).

   El scroll vive AQUÍ y no en el documento a propósito: la barra superior y la
   acción del pie tienen que seguir en su sitio con la lista a medio recorrer.
   Yo tiene ocho listas y Alimentación crece con cada comida del día; con el
   scroll en el documento, la acción principal se iba al fondo del recorrido. */
.pantalla--lista { height: 100dvh; }
.pantalla--lista .contenido {
  flex: 1; min-height: 0;
  margin-top: 32px;
  overflow-y: auto; overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  display: flex; flex-direction: column; gap: 24px;
  scrollbar-width: none;
  /* Sin acción al pie, el propio scroll respeta el área segura. Con acción,
     la respeta la acción y aquí basta un respiro. */
  padding-bottom: var(--banda-abajo);
}
.pantalla--lista .contenido::-webkit-scrollbar { display: none; }

/* La acción principal queda FUERA del scroll, fija al pie y al alcance del
   pulgar. Es la regla de "una pantalla, una acción" del sistema: si hay que
   recorrer la lista entera para encontrar el botón, no está al alcance. */
.pantalla--lista .accion {
  flex: none;
  display: grid; gap: 8px; justify-items: center;
  width: 100%;
  padding: 12px 0 var(--banda-abajo);
  /* Difumina el final del scroll contra el fondo, que nunca cambia de color. */
  background: linear-gradient(to bottom, rgb(229 229 229 / 0) 0%, var(--bg-base) 14px);
}

/* Dentro de una lista, héroe y banda son bloques más del flujo: no hay espacio
   sobrante que repartir, así que las filas de centrado no aplican. */
.pantalla--lista .centro {
  flex: none;
  display: flex; flex-direction: column; align-items: center;
  padding-top: 8px;
}
.pantalla--lista .centro > .hero,
.pantalla--lista .centro > .hero-pie,
.pantalla--lista .centro > :not(.hero) { margin-bottom: 0; align-self: center; }
.pantalla--lista .banda-inferior { padding-bottom: 0; }

/* ── NAVEGACIÓN — el único indicador es el contraste ─────────────────────── */

.pestana {
  font-family: var(--mono); font-size: 14px; font-weight: 500;
  letter-spacing: 0.10em; text-transform: uppercase;
  color: var(--text-disabled); text-decoration: none;
  background: none; border: 0; padding: 0; cursor: pointer;
  transition: color var(--dur-instant) var(--ease-standard);
}
.pestana[aria-current="page"] { color: var(--text-primary); }
.icono-desnudo {
  width: var(--toque); height: var(--toque);
  display: grid; place-items: center;
  color: var(--text-disabled); background: none; border: 0; cursor: pointer;
  font-size: var(--icono); line-height: 1; text-decoration: none;
  transition: color var(--dur-instant) var(--ease-standard);
}
.icono-desnudo:active { color: var(--text-primary); }

/* Los blancos de toque de 44 se salen 10 px hacia el margen para que el DIBUJO
   quede alineado con el contenido y no el rectángulo invisible que lo rodea.
   Sin esto la hamburguesa caía en x=375 con el margen en 410: se veía metida
   hacia dentro y desalineada con todo lo demás de la pantalla. */
.barra-superior > .icono-desnudo:first-child { margin-left: calc(-1 * (var(--toque) - var(--icono)) / 2); }
.barra-superior > .icono-desnudo:last-child  { margin-right: calc(-1 * (var(--toque) - var(--icono)) / 2); }
/* Las tres barras son de largos distintos (26/18/22): se alinean por la
   derecha, que es su lado recto. Centrarlas deja el borde izquierdo dentado. */
.barra-superior > .icono-desnudo:last-child { place-items: center end; }

/* ── OBJETO HÉROE ────────────────────────────────────────────────────────── */

/* El relleno son DOS piezas, como en el entregable:
     1 · un disco oscuro real, centrado, que crece por width/height
     2 · una copia del texto recortada a ese mismo círculo, en blanco
   El texto se invierte por máscara en vez de desvanecerse o moverse.

   Animar width/height y no clip-path es a propósito: clip-path no interpola
   igual de suave en todos los navegadores, y el relleno es el movimiento más
   visible de la app. */
.hero {
  position: relative;
  width: var(--hero); height: var(--hero);
  border-radius: var(--r-redondo);
  background: var(--surface);
  overflow: hidden;
  flex: none;
}
/* El relleno se mide en PORCENTAJE del disco, no en px. El disco encoge en
   pantallas cortas o angostas (ver --hero) y con un diámetro en píxeles el
   relleno se quedaba con la medida del disco de 300: al 100 % se desbordaba,
   y al 40 % mostraba una proporción que no era la del dato. */
.hero-relleno {
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: var(--llenado, 0%);
  height: var(--llenado, 0%);
  border-radius: var(--r-redondo);
  background: #1A1A1A;
  transition: width var(--dur-base) var(--ease-standard),
              height var(--dur-base) var(--ease-standard);
}
.hero-capa {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 0 32px; text-align: center;
}
/* Sobre una caja cuadrada, circle(R%) toma como referencia el lado: R = la
   mitad del diámetro, en las mismas unidades que .hero-relleno. */
.hero-capa--llena {
  color: var(--on-dark);
  clip-path: circle(var(--radio-llenado, 0%) at 50% 50%);
}
/* Sin dato no rellena NADA. Un disco a medias comunicaría una proporción que
   no se conoce, que es peor que no comunicar nada. */
.hero--sin-dato .hero-relleno,
.hero--sin-dato .hero-capa--llena { display: none; }

/* La métrica secundaria vive DEBAJO del disco, a 24 px: dentro, el borde del
   relleno la cortaba a la mitad justo en el rango de 30-45 %, que es el que se
   ve casi todas las mañanas. */
.hero-pie {
  display: flex; flex-direction: column; align-items: center;
  gap: 30px; margin-top: 24px;
  text-align: center;
}

/* ── CHIP DE MÉTRICA · Ø32 ───────────────────────────────────────────────── */

.riel-metricas { display: flex; gap: 12px; justify-content: center; }

/* Ancho fijo de 32 y separaciones por espaciadores de 10 y 8, no por `gap`:
   así la etiqueta y el subrayado quedan a distancia constante del chip aunque
   el texto cambie de largo (PROT / KCAL / CARB / GRAS / FIB / AGUA). */
.metrica-item {
  width: var(--chip);
  display: flex; flex-direction: column; align-items: center;
  background: none; border: 0; padding: 0; cursor: pointer;
  min-height: auto; min-width: auto;
  color: inherit; text-decoration: none;
}
.chip {
  width: var(--chip); height: var(--chip);
  border-radius: var(--r-redondo);
  display: flex; align-items: center; justify-content: center;
  background: var(--surface); color: var(--text-primary);
  font-family: var(--mono); font-size: 12px; letter-spacing: 0.02em;
  transition: transform var(--dur-instant) var(--ease-standard);
}
.metrica-item:active .chip { transform: scale(0.96); }
/* Cumplido cambia el SÍMBOLO y el relleno, no agrega insignia ni color. */
.chip--cumplido  { background: var(--text-primary); color: var(--on-dark); }
.chip--inactivo,
.chip--sin-dato  { color: var(--text-secondary); }

.metrica-hueco-etiqueta { height: 10px; }
.metrica-hueco-subrayado { height: 8px; }

/* La etiqueta no puede ensanchar la columna. `es-piso` le cuelga un "+" y eso
   llevaba CARB y GRAS de 32 a 38 px: cada una se desbordaba 3 px por lado y
   tocaba a sus vecinas, con el riel entero descuadrado. Ahora la etiqueta mide
   los 32 del chip y el "+" se dibuja fuera del flujo. */
.metrica-item > .t-etiqueta {
  position: relative;
  width: 100%;
  text-align: center;
  white-space: nowrap;
}
.metrica-item > .t-etiqueta .es-piso {
  position: absolute; top: -1px; left: 100%;
}

/* El indicador de selección es un subrayado de 24×2, la misma pieza que usa la
   navegación inferior del sistema. Siempre presente, transparente si no está
   activo: si apareciera y desapareciera, la retícula se movería, y §8.4 dice
   que la retícula no se anima. */
.metrica-subrayado {
  width: 24px; height: 2px; background: transparent;
  transition: background var(--dur-instant) var(--ease-standard);
}
.metrica-item[aria-current="true"] .metrica-subrayado { background: var(--text-primary); }

/* ── MARCA · Ø10 — seguridad y envío comparten pieza ─────────────────────── */

.marca {
  width: var(--marca); height: var(--marca);
  border-radius: var(--r-redondo);
  flex: none;
}
.marca--permitido { background: var(--text-primary); }
.marca--revisar   { background: var(--text-secondary); }
.marca--excluido  { background: var(--text-disabled); }
.marca--enviado   { background: var(--text-primary); }
.marca--pendiente { background: var(--text-disabled); }

/* Un ejercicio excluido se tacha además del disco: los tres estados tienen que
   distinguirse en blanco y negro puro y a tres metros. */
.excluido-nombre { color: var(--text-secondary); text-decoration: line-through 1.5px; }

/* ── FILA DE LISTA · 64 pt, radio 20 ─────────────────────────────────────── */

.fila {
  display: flex; align-items: center; gap: 12px;
  min-height: var(--alto-control);
  padding: 0 20px;
  background: var(--surface);
  border-radius: var(--r-tarjeta);
  width: 100%;
  border: 0; text-align: left; color: inherit; cursor: pointer;
  font-family: inherit; font-size: 16px;
  transition: transform var(--dur-instant) var(--ease-standard);
}
.fila:active { transform: scale(0.98); }
.fila--estatica { cursor: default; }
.fila--estatica:active { transform: none; }
.fila-cuerpo { flex: 1; min-width: 0; display: grid; gap: 2px; }
.fila-valor { font-family: var(--mono); font-size: 14px; color: var(--text-secondary); flex: none; }
.lista { display: grid; gap: 8px; width: 100%; }

/* Un grupo suelto dentro de una lista: la traza con su leyenda, un par de
   números. No es una lista de filas y no lleva superficie blanca. */
.bloque { display: grid; gap: 12px; justify-items: center; width: 100%; }

/* El texto explicativo se centra y se limita en ancho: a 380 px de ancho
   completo, 16/21 da renglones de más de 70 caracteres. */
.nota { text-align: center; margin: 0 auto; max-width: 300px; }

/* ── CÁPSULA DE PLATILLO · 186 × 64, dos columnas ────────────────────────── */

/* Un disco Ø96 no cabía los nombres de platillo completos, y con ingredientes
   sueltos registrar una comida costaba cuatro toques. La cápsula es un radio
   que el sistema ya permite. */
.reticula-platillos {
  display: grid; grid-template-columns: repeat(2, 1fr);
  gap: 12px; width: 100%;
}
.platillo {
  min-height: var(--alto-control);
  border-radius: var(--r-redondo);
  background: var(--surface); color: var(--text-primary);
  border: 0; cursor: pointer; padding: 8px 16px;
  display: grid; align-content: center; gap: 2px; text-align: center;
  transition: transform var(--dur-instant) var(--ease-standard);
}
.platillo:active { transform: scale(0.96); }
.platillo--receta { background: var(--text-primary); color: var(--on-dark); }
.platillo-nombre {
  font-family: var(--mono); font-size: 12px; font-weight: 500;
  letter-spacing: 0.06em; text-transform: uppercase;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.platillo-macros { font-family: var(--mono); font-size: 12px; opacity: 0.65; }

/* ── BOTONES ─────────────────────────────────────────────────────────────── */

/* Se centra con flex y no con `text-align`: la mitad de los botones de la app
   son <a> —anterior, siguiente, volver— y un ancla no hereda el centrado que
   un <button> trae de fábrica. Sin esto, "SIGUIENTE →" salía pegado a la
   izquierda de su cápsula y parecía un error de maquetación. */
.boton {
  min-height: var(--alto-control);
  width: 100%; max-width: var(--contenido);
  border-radius: var(--r-redondo); border: 0;
  background: var(--text-primary); color: var(--on-dark);
  font-family: var(--mono); font-size: 14px; font-weight: 500;
  letter-spacing: 0.10em; text-transform: uppercase;
  cursor: pointer; padding: 0 24px;
  display: inline-flex; align-items: center; justify-content: center;
  text-align: center;
  transition: transform var(--dur-instant) var(--ease-standard);
}
.boton:active { transform: scale(0.97); background: #000; }
.boton:disabled { background: var(--text-disabled); color: var(--bg-base); cursor: default; }
.boton--fantasma {
  background: none; color: var(--text-secondary);
  font-size: 12px; letter-spacing: 0.06em; min-height: var(--toque);
}

/* ── PRESIÓN SOSTENIDA · 3 s — la única acción destructiva ────────────────
   El entregable reservó este gesto para exactamente esto: "el gesto de presión
   sostenida de 3 s NO se usa para registrar —demasiada fricción seis veces al
   día— y se conserva íntegro para una sola acción destructiva, borrar el día".
   `--dur-ritual` llevaba declarado desde el principio sin usarse.

   El relleno avanza desde la izquierda y es la única realimentación: no hay
   diálogo de "¿estás seguro?", porque un diálogo se despacha con un toque
   reflejo y tres segundos de dedo puesto no. Soltar antes cancela sin más. */
.boton--ritual {
  position: relative; overflow: hidden;
  background: var(--surface); color: var(--text-primary);
  font-size: 12px; letter-spacing: 0.06em;
}

/* El relleno es GRIS y no negro, y el botón es blanco: así el texto se lee
   igual sobre la parte llena y sobre la vacía, sin trucos de mezcla. Con
   relleno oscuro había que invertir la etiqueta a media carga y quedaba
   ilegible justo en el tramo donde el borde la parte por la mitad. */
/* El relleno lo mueve JAVASCRIPT, no una transición.
   Con transición dependía de que la clase, la especificidad y el momento en
   que el navegador aplica el valor cayeran todos bien — y en iPhone no caían:
   el botón se aplastaba y la carga no avanzaba. Con una variable que se
   actualiza cuadro a cuadro no hay nada que pueda quedarse a medias, y el
   progreso es el mismo número que cuenta el temporizador. */
.boton--ritual::before {
  content: ''; position: absolute; inset: 0;
  background: var(--track);
  transform: scaleX(var(--carga, 0)); transform-origin: left;
}

.boton--ritual > span { position: relative; z-index: 1; }

/* La realimentación de ESTE botón es el relleno, así que se le quita la del
   resto: `.boton:active` lo pintaba entero de negro mientras el dedo estaba
   puesto y tapaba exactamente lo que hay que mirar. Era el sintoma de "solo se
   pone negro el bloque". El escalado tampoco aplica: durante tres segundos, un
   botón encogido se lee como atascado. */
/* `.boton.boton--ritual` y no `.boton--ritual` a secas: la sección §8 vuelve a
   declarar `.boton:active { transform: scale(0.97) }` 300 líneas más abajo, y
   con la misma especificidad gana la última. El botón se aplastaba y este
   `transform: none` no llegaba a aplicarse nunca. Doblar la clase sube la
   especificidad a 0-3-0 y deja de depender del orden del archivo. */
.boton.boton--ritual:active {
  background: var(--surface);
  transform: none;
}

/* `touch-action: none` en iOS: sin esto Safari trata el dedo quieto como el
   principio de un posible desplazamiento —el botón vive dentro de una región
   con scroll— y dispara `pointercancel` a los pocos cientos de milisegundos.
   El gesto se cancelaba solo y la carga no pasaba de la primera pulsación.
   Aquí no hace falta desplazar: son tres segundos con el dedo quieto. */
.boton--ritual { touch-action: none; }

/* Dos acciones secundarias lado a lado — anterior / siguiente de la sesión. */
.par-botones { display: flex; gap: 12px; width: 100%; }
.par-botones > * { flex: 1; }

.fab {
  position: fixed; right: var(--margen); bottom: calc(var(--margen) + 24px);
  width: var(--alto-control); height: var(--alto-control);
  border-radius: var(--r-redondo);
  background: var(--text-primary); color: var(--on-dark);
  border: 0; cursor: pointer; z-index: 10;
  display: grid; place-items: center;
  font-size: var(--icono); line-height: 1;
  transition: transform var(--dur-instant) var(--ease-standard);
}
.fab:active { transform: scale(0.96); }
/* Desaparece durante la sesión de entreno y con la hoja abierta. */
.fab[hidden] { display: none; }

/* ── STEPPER · 64 pt — tres números por serie sin abrir teclado ──────────── */

.steppers { display: flex; gap: 12px; width: 100%; justify-content: center; }
.stepper { display: grid; gap: 8px; justify-items: center; flex: 1; }
.stepper-valor { font-family: var(--mono); font-size: 17px; letter-spacing: 0.04em; }
.stepper-controles { display: flex; gap: 8px; }
.stepper-boton {
  width: var(--toque); height: var(--toque);
  border-radius: var(--r-redondo); border: 0;
  background: var(--surface); color: var(--text-primary);
  font-size: 20px; line-height: 1; cursor: pointer;
  transition: transform var(--dur-instant) var(--ease-standard);
}
.stepper-boton:active { transform: scale(0.94); }

/* ── TECLADO NUMÉRICO · 64 pt, sin contenedores ──────────────────────────── */

/* Extensión declarada por el diseño: el sistema no tiene campo de entrada, y
   los gramos y los kilos exigen dígitos. Se resuelve como en §4.10 — "el campo
   no se dibuja, se escribe": el número va en display 28/800 centrado sobre el
   fondo, sin caja ni borde. Un input redondeado gris sería el error fácil.

   Con teclado nativo no se puede: tapa media pantalla, y esto se usa de pie. */
.entrada-numero {
  font-size: 28px; font-weight: 800; letter-spacing: -0.01em;
  text-align: center; min-height: 40px;
}
.entrada-numero:empty::before { content: attr(data-placeholder); color: var(--text-disabled); }
.entrada-unidad { font-family: var(--mono); font-size: 17px; color: var(--text-secondary); }

.teclado {
  display: grid; grid-template-columns: repeat(3, 1fr);
  gap: 8px; width: 100%; max-width: var(--contenido);
}
.tecla {
  min-height: var(--alto-control);
  border: 0; border-radius: var(--r-redondo);
  background: var(--surface); color: var(--text-primary);
  font-family: var(--mono); font-size: 20px; letter-spacing: 0.02em;
  cursor: pointer;
  transition: transform var(--dur-instant) var(--ease-standard);
}
.tecla:active { transform: scale(0.96); }
.tecla--secundaria { background: none; color: var(--text-secondary); font-size: 16px; }

/* ── DISCO HÉROE: estado COMPLETO ────────────────────────────────────────── */

/* El sistema resuelve "éxito" como cambio de estado del objeto, no como color:
   la superficie plana se vuelve una esfera con volumen. Cero verde.
   El texto desaparece: una vez hecho, el objeto ES el mensaje. */
.hero--completo {
  background: radial-gradient(circle at 38% 32%, #EDEDED 0%, #9E9E9E 45%, #4A4A4A 100%);
}
.hero--completo .hero-capa { color: var(--on-dark); }
.hero--completo .hero-capa--llena { display: none; }
.hero-palomita {
  width: 96px; height: 96px;
  background: linear-gradient(150deg, #FFFFFF 0%, #9E9E9E 100%);
  clip-path: polygon(14% 48%, 8% 58%, 38% 88%, 92% 26%, 84% 18%, 38% 70%);
  animation: aparecer-palomita var(--dur-expressive) var(--ease-decelerate);
}
@keyframes aparecer-palomita {
  from { transform: scale(0.82) rotate(-8deg); }
  to   { transform: none; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   EL ESTALLIDO — la única animación de firma, y el único color

   Medido del video cuadro a cuadro (docs/cronometraje-referencia.md, evento 2:
   cuadros 497-517, 333 ms). Los cortes de color son SECOS: del naranja al azul
   se pasa de un cuadro al siguiente, sin interpolar. Por eso los pares de
   fotogramas clave están pegados —35 % y 35.001 %— en vez de dejar que el
   navegador funda entre ellos, que es lo que haría por omisión y lo que
   convertiría dos destellos en un degradado sucio.

     0 - 35 %   blanco    117 ms
    35 - 60 %   naranja    84 ms
    60 - 85 %   azul       84 ms
    85 - 100 %  se apaga y aparece la esfera

   Rompe la regla §1 del sistema con aprobación explícita (ADR-006). Es una vez
   al día, dura 168 ms de color, y el resto de la app no tiene un solo píxel
   saturado.
   ═══════════════════════════════════════════════════════════════════════════ */

/* El disco recorta su relleno, pero las astillas tienen que SALIR de él. */
.hero--completo { position: relative; overflow: visible; }

/* Y el recorte se hace aquí, al ancho de la pantalla.

   `clip` y no `hidden`: `hidden` crearía un contenedor de desplazamiento y las
   astillas, que se salen 165 px por lado, provocarían scroll horizontal — la
   regla dura 4 del estudio, que se verifica con
   `document.documentElement.scrollWidth === window.innerWidth` a 375 px. */
.pantalla { overflow: clip; }

/* El disco de color, encima de la esfera y por debajo de la palomita. */
/* ── SOLO AL COMPLETARSE, NO AL MIRARLO ───────────────────────────────────
   El estallido celebra un CAMBIO —pasar de no cumplido a cumplido— y no un
   estado. Sin esta condición se disparaba en cada navegación a la pantalla,
   porque el enrutador vuelve a renderizar el disco y la animación arranca de
   cero: verlo diez veces al día lo convierte en ruido y deja de significar
   nada la vez que sí importa.

   La clase la pone `festejo.js` una única vez por métrica y por día. Sin ella
   el disco completado se ve igual —esfera con volumen y palomita— pero
   quieto, que es lo correcto para un estado ya conocido. */
.hero-destello, .hero-astillas { display: none; }
.hero--festejar .hero-destello,
.hero--festejar .hero-astillas { display: block; }

.hero-destello {
  position: absolute; inset: 0;
  border-radius: var(--r-redondo);
  pointer-events: none;
  animation: destello var(--dur-estallido) linear both;
}

@keyframes destello {
  0%,     34.999% { background: #FEFEFE; }
  35%,    59.999% { background: var(--destello-naranja); }
  60%,    84.999% { background: var(--destello-azul); }
  85%             { background: var(--destello-azul); opacity: 1; }
  100%            { background: var(--destello-azul); opacity: 0; }
}

/* Las astillas: cuñas negras que salen despedidas.

   `repeating-conic-gradient` dibuja las diez de una vez y la máscara radial
   las recorta por dentro, para que nazcan del BORDE del disco y no de su
   centro. Se escalan hacia afuera acelerando —`--ease-gravity`— porque salen
   despedidas, no se despliegan.

   El contenedor lleva `overflow: clip` y NO `hidden`: el control 1 de
   nucleo/diseno/movimiento.md exige que nada salga del viewport, y `clip` no
   crea contexto de desplazamiento donde `hidden` sí lo haría. */
.hero-astillas {
  position: absolute; inset: -55%;
  pointer-events: none;
  background: repeating-conic-gradient(
    from 6deg,
    var(--text-primary) 0deg 2.4deg,
    transparent 2.4deg 36deg
  );
  -webkit-mask: radial-gradient(circle, transparent 31%, #000 33%);
  mask: radial-gradient(circle, transparent 31%, #000 33%);
  animation: astillas var(--dur-estallido) var(--ease-gravity) both;
}

/* La escala arranca en 0.8 y no más abajo, y el motivo es geométrico: el
   agujero de la máscara escala CON el elemento. La caja mide 630 px —los 300
   del disco más 165 por lado— así que el agujero vale 0.31 × 630 = 195 px de
   radio a escala 1. Por debajo de 0.77 ese agujero es menor que los 150 px del
   disco y las astillas se dibujan ENCIMA de él, cruzándolo, en vez de nacer de
   su borde. Con 0.8 el agujero empieza en 156: justo por fuera. */
@keyframes astillas {
  0%   { transform: scale(0.8); opacity: 0; }
  10%  { opacity: 1; }
  70%  { opacity: 1; }
  100% { transform: scale(1.32); opacity: 0; }
}

/* Sin movimiento: el objeto cambia de estado igual, pero sin destello ni
   astillas. El sistema exige que la versión quieta sea completa, no rota. */
@media (prefers-reduced-motion: reduce) {
  .hero--festejar .hero-destello,
  .hero--festejar .hero-astillas { display: none; }
}

/* ── PROGRESO — 56 × 2, centrada, minúscula a propósito ──────────────────── */

.progreso { width: 56px; height: 2px; border-radius: var(--r-redondo); background: var(--track); }
.progreso > i { display: block; height: 100%; border-radius: inherit; background: var(--text-primary); }

/* ── TRAZA DE TENDENCIA — 2 pt, sin ejes ni puntos ───────────────────────── */

/* `preserveAspectRatio="none"` deforma el trazo para llenar la caja, pero el
   SVG en línea sigue siendo un elemento de línea base: se sienta sobre la línea
   de texto y su descendiente se comía el hueco de abajo, con la traza tocando
   la etiqueta que va debajo. `display: block` lo saca de la línea base, que ya
   estaba, pero faltaba el alto reservado — sin `flex: none` la traza se
   comprimía dentro del bloque y se montaba con lo de al lado. */
.traza {
  width: 100%; max-width: var(--contenido);
  height: 64px; flex: none;
  display: block;
  overflow: visible;
}
.traza path { fill: none; stroke: var(--text-primary); stroke-width: 2; stroke-linecap: round; }

/* ── HOJA DE CAPTURA ─────────────────────────────────────────────────────── */

.hoja {
  border: 0; padding: 0; margin: auto auto 0;
  width: 100%; max-width: 440px;
  background: var(--surface); color: var(--text-primary);
  border-radius: var(--r-hoja) var(--r-hoja) 0 0;
  /* El catálogo de comida son 19 platillos en tres grupos y no cabe en una
     pantalla: sin tope de altura, la hoja crecía por debajo del borde inferior
     y los últimos grupos —y las otras rutas de captura— quedaban inalcanzables. */
  max-height: 88dvh;
  overflow-y: auto; overscroll-behavior: contain;
  scrollbar-width: none;
}
.hoja::-webkit-scrollbar { display: none; }
.hoja::backdrop { background: rgb(51 51 51 / 0.28); }
.hoja-cuerpo {
  padding: 24px var(--margen) calc(var(--banda-abajo) + 24px);
  display: grid; gap: 24px;
}

/* Dentro de la hoja se invierte la separación de capas: la superficie es
   blanca, así que lo que va encima es el gris base. Una cápsula blanca sobre
   hoja blanca es invisible —así se veían "PROTEÍNA RÁPIDA" y "DEL CONGELADOR",
   como texto suelto— y el sistema no admite bordes ni sombras para arreglarlo. */
.hoja .platillo,
.hoja .fila,
.hoja .tecla,
.hoja .chip { background: var(--bg-base); }
.hoja .platillo--receta { background: var(--text-primary); color: var(--on-dark); }

/* ── TOAST — único elemento que "flota", con material translúcido ────────── */

.toast {
  position: fixed; top: 77px; left: 15px; right: 15px; z-index: 20;
  min-height: 70px; border-radius: var(--r-hoja);
  background: rgb(172 172 172 / 0.82);
  backdrop-filter: blur(30px);
  color: var(--on-dark);
  display: flex; align-items: center; gap: 16px; padding: 16px;
}

/* ── AVISO DE DÍA SIMULADO ────────────────────────────────────────────────
   Deliberadamente feo y en medio. Es la única pieza de la app que puede
   saltarse el sistema de diseño, porque su trabajo es exactamente ése: que no
   se pueda olvidar que la fecha es inventada.

   Todo lo que se registre con esto puesto queda escrito con el día simulado, y
   después hay que poder distinguirlo de lo real. Un aviso discreto se deja de
   ver a los dos minutos. */
.aviso-simulacion {
  position: fixed; top: 0; left: 0; right: 0; z-index: 100;
  padding: 4px 8px;
  background: var(--destello-naranja); color: #000;
  font-family: var(--mono); font-size: 11px; font-weight: 500;
  letter-spacing: 0.08em; text-align: center;
  pointer-events: none;
}
/* La barra superior baja para no quedar debajo del aviso. */
body[data-simulado] .barra-superior { padding-top: calc(var(--banda-arriba) + 20px); }
body[data-simulado] .barra-superior { height: calc(var(--banda-arriba) + 20px + var(--icono)); }

/* ── ESTADO SIN DATO ─────────────────────────────────────────────────────── */

/* Cero y desconocido no son lo mismo, y la app no inventa el dato que falta.
   Con antecedente de prediabetes, la fibra es el número que menos se puede
   falsear: reportar "18 g" cuando son "18 g conocidos más tres registros sin
   medir" es mentir por omisión. */
.sin-dato { color: var(--text-disabled); }
.es-piso::after { content: "+"; color: var(--text-secondary); font-size: 0.8em; }

/* ═══════════════════════════════════════════════════════════════════════════
   MOVIMIENTO — sistema_diseno.md §8

   El entregable casi no lo implementa: solo trae la transición del relleno del
   héroe, el toast y un scale(0.97). Todo lo demás está especificado en el
   documento y es lo que hace que la app se sienta viva en vez de estática.

   Las tres reglas que gobiernan todo:
     · Solo una cosa se mueve a la vez. Si el héroe se llena, el riel está quieto.
     · La retícula NUNCA se anima. Nada cambia de tamaño para hacerle sitio a
       otra cosa; los objetos entran y salen del espacio que ya tenían.
     · El fondo #E5E5E5 es constante en todos los estados. Nunca hay un flash,
       un oscurecimiento ni un tinte de éxito.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Realimentación de toque ─────────────────────────────────────────────── */

/* Controles CON relleno: escala 1 → 0.96 y regreso con resorte. */
.chip, .platillo, .boton, .tecla, .stepper-boton, .fila {
  transition: transform var(--dur-instant) var(--ease-standard),
              background var(--dur-instant) var(--ease-standard);
}
.metrica-item:active .chip,
.platillo:active, .tecla:active, .stepper-boton:active { transform: scale(0.96); }
.boton:active { transform: scale(0.97); }
.fila:active { transform: scale(0.98); }

/* Al soltar, el regreso usa el resorte. Se aplica en :not(:active) para que
   la ida sea seca y la vuelta asiente. */
.chip:not(:active), .platillo:not(:active), .boton:not(:active),
.tecla:not(:active), .stepper-boton:not(:active), .fila:not(:active) {
  transition: transform var(--dur-settle) var(--spring-settle),
              background var(--dur-instant) var(--ease-standard);
}

/* Controles SIN relleno: opacidad 1 → 0.5, y NO escalan. Escalar un icono
   desnudo de 2 pt lo deforma; bajarle la opacidad no. */
.icono-desnudo, .glifo-puntos, .pestana, .boton--fantasma {
  transition: opacity var(--dur-instant) var(--ease-standard),
              color var(--dur-instant) var(--ease-standard);
}
.icono-desnudo:active, .glifo-puntos:active, .boton--fantasma:active { opacity: 0.5; }

/* ── ARRASTRE ENTRE PESTAÑAS ──────────────────────────────────────────────
   El contenido sigue al dedo; la barra NO se mueve (§8.4: las barras de
   navegación están siempre presentes e idénticas). Por eso el desplazamiento
   se aplica a los hijos de la pantalla y se excluye la barra, en vez de mover
   el contenedor entero.

   Durante el arrastre no hay transición —el movimiento ES el dedo— y al
   soltar entra el resorte del sistema. */
/* El desplazamiento se aplica SOLO mientras dura el gesto y su asentamiento.
   Un `transform` permanente —aunque valga translateX(0)— crea un bloque
   contenedor, y a partir de ahí cualquier `position: fixed` de dentro deja de
   referirse al viewport y se ancla a este elemento. El chip de pendientes y el
   toast se irían de sitio sin que nada más pareciera haber cambiado. */
.pantalla--arrastrando > :not(.barra-superior),
.pantalla--soltando > :not(.barra-superior) {
  transform: translateX(var(--arrastre, 0px));
}
.pantalla--soltando > :not(.barra-superior) {
  transition: transform var(--dur-settle) var(--spring-settle);
}
/* Mientras se arrastra, el scroll vertical de la lista se congela: pelear los
   dos ejes a la vez es lo que hace que un gesto se sienta resbaladizo. */
.pantalla--arrastrando .contenido { overflow-y: hidden; }

/* ── Cambio de pestaña — 48 pt lateral + fundido cruzado ─────────────────── */

/* View Transitions nativas: sin framework y sin JS. Los navegadores que no las
   soportan simplemente navegan, que es degradación limpia. */
@view-transition { navigation: auto; }

@media (prefers-reduced-motion: no-preference) {
  ::view-transition-old(contenido) {
    animation: salir-contenido var(--dur-base) var(--ease-standard) both;
  }
  ::view-transition-new(contenido) {
    animation: entrar-contenido var(--dur-base) var(--ease-standard) both;
  }
}

/* ── UN RELEVO, NI SOLAPE NI HUECO ────────────────────────────────────────
   Las dos mitades duran LO MISMO y corren a la vez, y la opacidad de una sube
   al mismo ritmo que baja la otra: en todo instante la suma es ~1, así que
   nunca hay dos textos legibles encima ni un momento en blanco.

   Se llegó aquí por dos correcciones seguidas, y las dos fueron de más:
   primero se adelantó la salida para que el disco no la pisara —y aparecieron
   44 ms de solape—; luego se retrasó la entrada para cerrarlo —y aparecieron
   86 ms de vacío, que se ven como un parpadeo blanco.

   La causa real no era el reparto de tiempos: era que el navegador fundía
   ADEMÁS la página entera (`root`, 250 ms) por encima de las piezas ya
   coreografiadas. Apagado eso, un relevo simétrico basta y es lo que se lee
   como continuo. */
@keyframes salir-contenido {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateX(calc(-1 * var(--sentido, 1) * 48px)); }
}
@keyframes entrar-contenido {
  from { opacity: 0; transform: translateX(calc(var(--sentido, 1) * 48px)); }
  to   { opacity: 1; transform: none; }
}

/* Las bandas del pie se van hacia abajo acelerando —como caer— y entran desde
   abajo frenando. Nunca de lado: su borde es el inferior. */
@keyframes bajar-saliendo {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateY(28px); }
}
@keyframes subir-entrando {
  from { opacity: 0; transform: translateY(28px); }
  to   { opacity: 1; transform: none; }
}

/* La barra superior NO se mueve. §8.3: "Las etiquetas de las pestañas no se
   mueven: solo cambian de color". §8.4: las barras de navegación están siempre
   presentes e idénticas. Al llevar nombre propio, el navegador la saca del
   recuadro de la pantalla y la anima aparte — con duración 0, o sea quieta. */
.barra-superior { view-transition-name: barra; }
::view-transition-group(barra) { animation-duration: 0s; }

/* ── SE MUEVE LO QUE CAMBIA, NO LA PÁGINA ─────────────────────────────────
   Antes, toda la pantalla llevaba UN nombre y se iba entera de lado: se leía
   como pasar de página, aunque el disco estuviera en el mismo sitio antes y
   después. La referencia no hace eso — al cambiar de pestaña ahí solo se mueve
   el disco; la barra, el riel y la retícula se quedan quietos.

   Ahora cada pieza lleva su propio nombre. Cuando una existe en las dos
   pantallas, el navegador la reconoce como LA MISMA y le interpola posición y
   tamaño en vez de sacarla y meter otra: el disco de HOY se convierte en el de
   PESO sin irse a ningún lado.

   `view-transition-name` tiene que ser ÚNICO en el documento. Hoy renderiza
   seis discos —uno por métrica, cinco con [hidden]— y por eso el selector los
   excluye: un elemento en display:none no participa, pero dejarlo implícito es
   cómo se rompió esto una vez, con el navegador abortando por nombre duplicado
   y quedándose sin transición ninguna. */
.hero:not([hidden])  { view-transition-name: disco; }
.hero-pie            { view-transition-name: pie; }
.banda-inferior      { view-transition-name: banda-baja; }
.contenido           { view-transition-name: lista; }
.accion              { view-transition-name: accion; }

/* El fundido de la PÁGINA ENTERA se apaga.
   Por omisión el navegador funde `root` —un recorte de todo lo que no lleva
   nombre propio— durante 250 ms, ADEMÁS de animar cada pieza nombrada. Con las
   piezas ya coreografiadas una a una, ese fundido es una capa fantasma encima:
   la pantalla vieja sigue visible mientras la nueva entra, y se lee como
   información encimada aunque el disco ni se mueva —de COMIDA a PESO se veía
   igual, y ahí el disco se queda quieto. */
::view-transition-old(root),
::view-transition-new(root) { animation: none; }

/* El disco NO se desvanece: se desplaza y cambia de tamaño. Es la pieza que da
   continuidad entre pantallas, y desvanecerla rompería justo eso. */
::view-transition-group(disco) {
  animation-duration: var(--dur-base);
  animation-timing-function: var(--ease-decelerate);
}
::view-transition-old(disco),
::view-transition-new(disco) { animation: none; mix-blend-mode: normal; }

/* La cápsula negra se queda donde está y solo cambia de etiqueta: su grupo
   morfa con la misma curva y duración que el disco, para que las dos piezas
   que persisten se muevan al mismo ritmo. */
::view-transition-group(accion),
::view-transition-group(banda-baja) {
  animation-duration: var(--dur-base);
  animation-timing-function: var(--ease-decelerate);
}

/* Lo que de verdad cambia sí entra y sale de lado. */
@media (prefers-reduced-motion: no-preference) {
  /* LO QUE SE LEE se releva de lado: el pie del disco y las listas cambian de
     contenido, y el desplazamiento de 48 px dice de dónde viene lo nuevo.

     Misma duración y misma curva en las dos mitades: eso es lo que hace que el
     relevo se lea continuo. Curvas o tiempos distintos dejan un tramo donde
     una ya se fue y la otra no ha llegado. */
  ::view-transition-old(pie), ::view-transition-old(lista) {
    animation: salir-contenido var(--dur-base) var(--ease-standard) both;
  }
  ::view-transition-new(pie), ::view-transition-new(lista) {
    animation: entrar-contenido var(--dur-base) var(--ease-standard) both;
  }

  /* ── LAS BANDAS DEL PIE NO SE DESLIZAN DE LADO ──────────────────────────
     El botón negro de abajo es EL MISMO OBJETO con otra etiqueta —de COMIDA a
     PESO solo cambia el texto de "AGREGAR COMIDA" a "REGISTRAR PESO"— y
     mandarlo de viaje 48 px a la izquierda lo trata como si se hubiera
     sustituido por otro. Es el mismo error que tenía el disco antes de darle
     continuidad, y en una pieza negra de 64 px de alto se nota el doble.

     Su grupo morfa —la cápsula se queda donde está— y solo la ETIQUETA se
     releva, rápido. Y cuando la banda de verdad desaparece, porque la pantalla
     nueva no tiene, se va HACIA ABAJO: es su borde, es por donde entró, y
     desvanecerse en el sitio no dice a dónde fue. */
  ::view-transition-old(banda-baja), ::view-transition-old(accion) {
    animation: bajar-saliendo var(--dur-aviso) var(--ease-gravity) both;
  }
  ::view-transition-new(banda-baja), ::view-transition-new(accion) {
    animation: subir-entrando var(--dur-base) var(--ease-decelerate) both;
  }
}

/* ── CAMBIAR DE MÉTRICA NO ES CAMBIAR DE PANTALLA ─────────────────────────
   Una View Transition captura SIEMPRE el documento entero, aunque lo único
   que se quiera mover sea un subrayado de 24×2. Sin esto, tocar un chip
   disparaba las reglas de arriba y la pantalla completa —pie, riel, listas,
   acción— se iba de lado y volvía. Se veía como pasar de página al tocar FIB
   o GRAS, que es justo lo contrario de lo que el gesto significa.

   Aquí se apaga todo lo demás mientras dura ese cambio. Se mueve el
   subrayado, y el disco lo anima `hoy.js` por su cuenta con el resorte
   medido: dos cosas, cada una con su mecanismo, ninguna arrastrando a la
   página con ella. */
.solo-subrayado::view-transition-group(root),
.solo-subrayado::view-transition-old(root),
.solo-subrayado::view-transition-new(root),
.solo-subrayado::view-transition-group(disco),
.solo-subrayado::view-transition-old(disco),
.solo-subrayado::view-transition-new(disco),
.solo-subrayado::view-transition-group(pie),
.solo-subrayado::view-transition-old(pie),
.solo-subrayado::view-transition-new(pie),
.solo-subrayado::view-transition-group(banda-baja),
.solo-subrayado::view-transition-old(banda-baja),
.solo-subrayado::view-transition-new(banda-baja),
.solo-subrayado::view-transition-group(lista),
.solo-subrayado::view-transition-old(lista),
.solo-subrayado::view-transition-new(lista),
.solo-subrayado::view-transition-group(accion),
.solo-subrayado::view-transition-old(accion),
.solo-subrayado::view-transition-new(accion) {
  animation: none !important;
}

/* ── Cambio de métrica — el subrayado se DESLIZA, no salta ───────────────── */

/* Un nombre de transición sobre el subrayado activo hace que el navegador
   interpole su posición entre un chip y otro. Es §8.3 al pie de la letra y
   cuesta una línea de CSS más una de JS. */
.metrica-item[aria-current="true"] .metrica-subrayado { view-transition-name: subrayado; }
::view-transition-group(subrayado) {
  animation-duration: var(--dur-base);
  animation-timing-function: var(--ease-standard);
}

/* ── CAMBIO DE MÉTRICA — el disco se encoge y vuelve a crecer ─────────────
   Sustituye al fundido cruzado, y el porqué está medido.

   Un fundido dice "esto se sustituyó por otra cosa". El encogimiento dice
   "sigue siendo el mismo disco, ahora enseña otra cifra" — que es LITERALMENTE
   lo que pasa: el objeto héroe es el día, y la métrica es qué cara enseña.

   Cambiar de PANTALLA sí es un deslizamiento lateral, porque HOY, PESO y
   ENTRENO son hermanas que están una al lado de otra. Cambiar de MÉTRICA no:
   no hay a dónde deslizarse, es el mismo objeto. Son dos gestos distintos y
   antes usaban la misma animación.

   Ver docs/referencia-movimiento.md §5. */
.hero, .hero-pie { transition: opacity var(--dur-quick) var(--ease-standard); }

.hero { transform-origin: 50% 50%; }

/* Salida: acelera hacia el punto, como si cayera. */
.cambiando-metrica .hero {
  transform: scale(var(--escala-punto));
  transition: transform var(--dur-encoger) var(--ease-gravity);
}
.cambiando-metrica .hero-pie { opacity: 0; }

/* Entrada: nace en el punto y crece frenando, con el sobrepaso medido. */
.hero--entrando {
  transform: scale(var(--escala-punto));
}
.hero--creciendo {
  transform: none;
  transition: transform var(--dur-crecer) var(--spring-golpe);
}

/* El relleno tarda más y con la curva de la referencia. */
.hero-relleno {
  transition: width var(--dur-relleno) var(--ease-relleno),
              height var(--dur-relleno) var(--ease-relleno);
}

/* ── Entradas ────────────────────────────────────────────────────────────── */

/* 150 ms medidos, no 360. La hoja no se presenta: llega. */
.hoja { animation: subir var(--dur-hoja) var(--ease-decelerate); }
@keyframes subir { from { transform: translateY(40px); } to { transform: none; } }

/* ── APERTURA DE LA APP ───────────────────────────────────────────────────
   280 ms de revelado circular desde el centro, frenando fuerte — medido de la
   referencia (docs/referencia-movimiento.md §7).

   El círculo empieza donde está el disco héroe y donde está el disco de la
   pantalla de arranque, que es el mismo sitio: así el salto entre la imagen
   estática que pone iOS y la app viva no se ve como un corte, sino como que
   el disco se abre.

   Solo en la PRIMERA pintada del documento. Al navegar entre pantallas el
   enrutador sustituye el cuerpo, y sin la clase esto no se vuelve a disparar:
   abrir la app es un momento, cambiar de pestaña no. */
@media (prefers-reduced-motion: no-preference) {
  .recien-abierta .pantalla {
    animation: abrir var(--dur-apertura) var(--ease-decelerate) both;
  }
}
@keyframes abrir {
  from { clip-path: circle(16% at 50% 50%); }
  to   { clip-path: circle(140% at 50% 50%); }
}

/* 167 ms medidos, no los 360 de --dur-base. Un aviso que aparece es
   información que LLEGA, no un objeto que se presenta: cuanto antes esté,
   antes se puede seguir. Ver docs/referencia-movimiento.md §6. */
.toast { animation: entrar-toast var(--dur-aviso) var(--ease-relleno); }
@keyframes entrar-toast { from { transform: translateY(-80px); } to { transform: none; } }

/* ── ACCESIBILIDAD ───────────────────────────────────────────────────────── */

button, a, [role="button"], input, select { min-height: var(--toque); }
.chip, .marca { min-height: auto; }

:focus-visible { outline: 2px solid var(--text-primary); outline-offset: 3px; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: var(--dur-quick) !important;
  }
}

/* El disco ya se encoge solo por el min() de --hero: la retícula no se
   reflowea, el objeto solo ocupa menos. Aquí queda únicamente el margen, que
   sí necesita un corte y no una interpolación. */
@media (max-width: 379px) {
  :root { --margen: 24px; }
}

/* La serie que se está corrigiendo, marcada mientras dura. Sin esto no hay
   forma de saber cuál se va a sobrescribir al pulsar guardar. */
.fila--corrigiendo {
  background: var(--bg-base);
  outline: 2px solid var(--text-primary);
  outline-offset: -2px;
}
