/* ══════════════════════════════════════════════════════════════════════════
   FORTE UI  ·  punto de entrada unico del sistema visual de FORTEONE
   ══════════════════════════════════════════════════════════════════════════
   ARTEFACTO GENERADO — no editar. Se edita src/ y se ejecuta:
       python app/static/design/forte/build.py

   Orden de capas (precedencia por orden de fuente):
     01 tokens        fuente unica de marca y producto
     02 foundations   base, foco, scroll, motion, utilidades
     03 materials     M1-M6 y el unico backdrop-filter del producto
     04 layout        topbar, sticky stack, dock, pie, pagina
     05 components    iconos, botones, campos, tablas, tarjetas, estados
     06 patterns      capas, chooser, formulario largo, modulo de la home
     07 responsive    adaptacion de los patrones
     08 accessibility foco, contraste, movimiento, objetivo tactil
     09 print         impresion de la INTERFAZ (los documentos: forte-doc.css)
     10 legacy-bridge puente temporal, siempre bajo .fr-shell / .fr-ui

   Regla de gobierno: NEVER SOLVE A REUSABLE VISUAL PROBLEM LOCALLY.
   ══════════════════════════════════════════════════════════════════════════ */

/* ===== src/01-tokens.css ===== */
/* ════════════════════════════════════════════════════════════════════════
   FORTE UI · 01 · TOKENS
   ════════════════════════════════════════════════════════════════════════
   FUENTE ÚNICA de todo valor visual reutilizable de FORTEONE.

   CONTRATO DE ORIGEN
   · BRAND_CANONICAL — Manual corporativo (docs/VISUAL FORTE/CANONICAL/).
     Paleta, tintas 80/60/40 e Inter. Aquí NO se inventa nada.
   · PRODUCT_DERIVED — todo lo demás (superficies, materiales, sombras,
     radios, spacing, motion, capas). Diseño propio del producto; NUNCA se
     presenta como norma del manual. GAP-14 = B_HYBRID_LOCKED.

   REGLA DURA: ningún otro fichero del repositorio puede (re)definir un
   token --fr-*. Los gates de tests/test_forte_ui_brand_contract.py fallan
   si aparece una segunda definición. Si falta un token, se añade AQUÍ.

   Origen de valores: docs/VISUAL FORTE/STRUCTURAL_PREMIUM_RENEWAL_MASTER/
   assets/fr.css §1 y §15.2 (owner-approved baseline).
   ════════════════════════════════════════════════════════════════════════ */

:root {
  /* ── 1.1 BRAND_CANONICAL · paleta corporativa exacta (CLAUDE.md §8.1) ── */
  --fr-ink:    #16080a;   /* Negro Forte   · Pantone Black 5 C            */
  --fr-cream:  #f0eee9;   /* Blanco Forte  · Cloud Dancer C               */
  --fr-red:    #c2402e;   /* Rojo Plastic  · 7619 C — acento identitario  */
  --fr-teal:   #00303b;   /* Azul Petróleo · 309 C — acento estructural   */
  --fr-olive:  #6b9560;   /* Verde Beyond  · 2264 C — firma de marca      */
  --fr-ink-rgb:   22, 8, 10;
  --fr-cream-rgb: 240, 238, 233;
  --fr-red-rgb:   194, 64, 46;
  --fr-teal-rgb:  0, 48, 59;
  --fr-olive-rgb: 107, 149, 96;

  /* Tintas del manual (sólidas, hacia blanco puro). Recurso gráfico:
     NO son fondos de UI arbitrarios. */
  --fr-red-80:   #ce6658;  --fr-red-60:   #da8c82;  --fr-red-40:   #e7b2ab;
  --fr-teal-80:  #335a62;  --fr-teal-60:  #668389;  --fr-teal-40:  #99acb0;
  --fr-olive-80: #89aa80;  --fr-olive-60: #a6c0a0;  --fr-olive-40: #c4d5c0;

  /* ── 1.2 PRODUCT_DERIVED · neutros cálidos (familia cream→ink) ───────── */
  --fr-n50:  #faf9f7;  --fr-n100: #f4f0ec;  --fr-n200: #e8e3de;
  --fr-n300: #d5cfc8;  --fr-n400: #bdb5ab;  --fr-n500: #8f8680;
  --fr-n600: #635b56;  --fr-n700: #3e3631;  --fr-n800: #251e1a;

  /* ── 1.3 Superficies (light) ────────────────────────────────────────── */
  --fr-canvas:   #f0eee9;         /* M0 — fondo de página (Blanco Forte)   */
  --fr-work:     #f7f6f2;         /* M1 — superficie de trabajo sólida     */
  --fr-raised:   #fbfaf8;         /* M2 — panel/card elevado sólido        */
  --fr-top:      #ffffff;         /* pico puntual (campo activo, celda)    */
  --fr-hover:    rgba(0, 48, 59, 0.045);
  --fr-active:   rgba(0, 48, 59, 0.09);
  --fr-selected: #dfe7e8;         /* selección persistente (tinte petróleo) */

  /* ── 1.4 Texto ──────────────────────────────────────────────────────── */
  --fr-text:       #16080a;
  --fr-text-2:     #453e3a;       /* secundario — AA sobre glass claro     */
  --fr-text-3:     #6f6762;       /* terciario: labels, metadata           */
  --fr-text-4:     #9a938c;       /* cuaternario: placeholder, disabled    */
  --fr-text-inv:   #f0eee9;
  --fr-text-inv-2: rgba(240, 238, 233, 0.74);
  --fr-link:       #0b4450;

  /* ── 1.5 Bordes y hairlines ─────────────────────────────────────────── */
  --fr-line-1:   rgba(22, 8, 10, 0.06);   /* hairline sutil               */
  --fr-line-2:   rgba(22, 8, 10, 0.12);   /* borde estándar               */
  --fr-line-3:   rgba(22, 8, 10, 0.22);   /* borde fuerte / control       */
  --fr-line-inv: rgba(240, 238, 233, 0.16);

  /* ── 1.6 MATERIALES M0–M7 ───────────────────────────────────────────── */
  /* M0 canvas y M1/M2 sólidos usan las superficies de §1.3. Aquí sólo las
     recetas translúcidas. NINGÚN otro fichero declara backdrop-filter. */
  /* M3 VEIL — glass sutil: dropdown, popover, tooltip rico                */
  --fr-m3-bg:    rgba(247, 246, 242, 0.86);
  --fr-m3-blur:  14px;
  --fr-m3-sat:   135%;
  --fr-m3-solid: #f6f4f0;
  /* M4 PANE — glass estructural: modal, drawer, sheet                     */
  --fr-m4-bg:    linear-gradient(180deg, rgba(252, 251, 248, 0.92), rgba(245, 243, 238, 0.82));
  --fr-m4-blur:  24px;
  --fr-m4-sat:   140%;
  --fr-m4-solid: #f9f8f5;
  /* M5 PLATE — glass oscuro: topbar, action dock, toolbar flotante        */
  --fr-m5-bg:    rgba(16, 7, 9, 0.82);
  --fr-m5-blur:  18px;
  --fr-m5-sat:   130%;
  --fr-m5-solid: rgba(16, 7, 9, 0.97);
  /* M6 SHEET-TECH — casi sólido: confirmación destructiva, modal técnico  */
  --fr-m6-bg:    rgba(253, 252, 250, 0.96);
  --fr-m6-blur:  10px;
  --fr-m6-sat:   120%;
  --fr-m6-solid: #fdfcfa;
  /* M7 THIN — lámina fina: el plano de contenido de la tarjeta de módulo.
     Es el ÚNICO translúcido de la Home (HOME V2 · R6B EQUILIBRADA) y lo que
     desenfoca es el plano de identidad que tiene JUSTO ENCIMA —el color de su
     propia tarjeta—, no un fondo lejano. Un desenfoque por tarjeta; ni uno más.
     10 px, no 18: a 18 lo de detrás es niebla y deja de ser espesor.
     saturate 1,14, no 1,7: por encima de 1,3 el material TIÑE, y teñir es
     efecto, no transmisión. Todo desenfoque necesita algo de `saturate` o el
     vidrio se come el color de lo que hay debajo. */
  --fr-m7-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.28), rgba(255, 255, 255, 0.08) 34%, rgba(255, 255, 255, 0.04)), rgba(255, 255, 255, 0.66);
  --fr-m7-blur:  10px;
  --fr-m7-sat:   114%;
  --fr-m7-solid: rgba(255, 255, 255, 0.94);

  /* Velo de capa (backdrop). Único valor autorizado. */
  /* El velo RETIRA la pagina, no la borra. A 0,38 la Home de debajo se leia
     como una mancha y la capa parecia una ventana sobre nada; a 0,32 el
     contexto sigue reconociendose —volver no desorienta— y el panel sigue
     destacando: 2,32 de contraste contra el fondo velado, frente a 2,73.
     Es el criterio que R6B dejo escrito para su propia capa. */
  --fr-veil-bg:      rgba(17, 8, 10, 0.32);
  --fr-veil-blur:    5px;
  --fr-veil-sat:     85%;
  --fr-veil-solid:   rgba(17, 8, 10, 0.55);

  /* ── 1.7 Edge system: 3 recursos, no más ────────────────────────────── */
  --fr-edge-out:      0 0 0 1px rgba(22, 8, 10, 0.10);              /* marco */
  --fr-edge-in:       inset 0 1px 0 rgba(255, 255, 255, 0.55);      /* luz superior */
  --fr-edge-rim:      inset 0 0 0 0.5px rgba(255, 255, 255, 0.30);  /* rim interior */
  --fr-edge-out-dark: 0 0 0 1px rgba(240, 238, 233, 0.12);
  --fr-edge-in-dark:  inset 0 1px 0 rgba(240, 238, 233, 0.10);

  /* ── 1.8 Shadow system: cada sombra con función ─────────────────────── */
  --fr-sh-contact: 0 1px 2px rgba(22, 8, 10, 0.10);                 /* apoyo   */
  --fr-sh-card:    0 1px 2px rgba(22, 8, 10, 0.08), 0 8px 20px -8px rgba(22, 8, 10, 0.14);
  --fr-sh-float:   0 1px 3px rgba(22, 8, 10, 0.10), 0 18px 44px -16px rgba(22, 8, 10, 0.24);
  --fr-sh-menu:    0 2px 6px rgba(22, 8, 10, 0.08), 0 12px 32px -12px rgba(22, 8, 10, 0.26);
  --fr-sh-modal:   0 2px 4px rgba(22, 8, 10, 0.08), 0 32px 72px -24px rgba(22, 8, 10, 0.34);
  --fr-sh-press:   0 0.5px 1px rgba(22, 8, 10, 0.12);               /* pressed */

  /* ── 1.9 Radius system: escalera 4/8/12/16 ──────────────────────────── */
  --fr-r-ctl:   4px;    /* botones, inputs, chips, celdas editables        */
  --fr-r-card:  8px;    /* cards, paneles, toolbar                         */
  --fr-r-float: 12px;   /* menú, popover, drawer, dock                     */
  --fr-r-modal: 16px;   /* modal, sheet                                    */

  /* ── 1.10 Spacing: base 4px, denso primero ──────────────────────────── */
  --fr-s1: 2px;  --fr-s2: 4px;  --fr-s3: 6px;  --fr-s4: 8px;
  --fr-s5: 12px; --fr-s6: 16px; --fr-s7: 20px; --fr-s8: 24px;
  --fr-s9: 32px; --fr-s10: 40px; --fr-s11: 48px;

  /* ── 1.11 Tipografía · Inter, 4 pesos autorizados ───────────────────── */
  /* "ForteInter" es el asset variable ya servido por el runtime desde
     app/static/fonts/inter/ (declarado en css/forte-fonts.css). El sistema
     NO vuelve a declarar @font-face: reutiliza el que ya existe. */
  --fr-font: "ForteInter", "Inter", "Helvetica Neue", -apple-system, system-ui, sans-serif;
  /* Identificadores técnicos —nombre de tabla, de columna, tipo SQL, código
     de artículo—. No es una segunda tipografía de marca: es la forma de
     decir «esto es un literal y se lee carácter a carácter». La pila la
     repetían a mano seis plantillas y cinco hojas de módulo; la decisión
     tipográfica vive donde tiene que vivir. */
  --fr-font-mono: "SF Mono", "Fira Code", "Consolas", "Monaco", "Menlo", "Ubuntu Mono", monospace;
  --fr-fw-light: 300; --fr-fw-reg: 400; --fr-fw-med: 500; --fr-fw-bold: 700;
  --fr-t-micro: 0.6875rem;  /* 11 — metadata, kicker                       */
  --fr-t-mini:  0.75rem;    /* 12 — labels, unidades, celdas densas        */
  --fr-t-small: 0.8125rem;  /* 13 — cuerpo denso, tabla estándar           */
  --fr-t-base:  0.875rem;   /* 14 — cuerpo UI                              */
  --fr-t-lead:  1rem;       /* 16 — cuerpo destacado, input táctil         */
  /* Los tres cuerpos grandes son FLUIDOS y los cinco pequenos no. Un
     cuerpo de 13 px es legibilidad, y escalarlo con la ventana solo consigue
     que la misma tabla se lea distinta en dos monitores. Un titulo de pagina
     si es composicion: a 375 px pide 20 y a 1.920 pide 24. El techo del
     `clamp` es lo que impide que en 4K se conviertan en un cartel.        */
  --fr-t-h3:    clamp(1rem, 0.18vw + 0.955rem, 1.125rem);   /* 16 → 18     */
  --fr-t-h2:    clamp(1.25rem, 0.28vw + 1.18rem, 1.375rem); /* 20 → 22     */
  --fr-t-h1:    clamp(1.5rem, 0.55vw + 1.36rem, 1.875rem);  /* 24 → 30     */
  --fr-lh-dense: 1.35;
  --fr-lh-body:  1.5;
  --fr-ls-wide:  0.08em;

  /* ── 1.12 Estados funcionales ───────────────────────────────────────── */
  /* GAP-14 B_HYBRID_LOCKED: derivados accesibles, NO colores corporativos.
     El error NUNCA es Rojo Plastic; el rojo de marca es acento identitario. */
  --fr-ok:      #2e6b3e;  --fr-ok-soft:     rgba(46, 107, 62, 0.12);
  --fr-warn:    #8a5800;  --fr-warn-soft:   rgba(138, 88, 0, 0.12);
  --fr-danger:  #9e2b1f;  --fr-danger-soft: rgba(158, 43, 31, 0.12);
  --fr-info:    #155a68;  --fr-info-soft:   rgba(21, 90, 104, 0.11);

  /* ── 1.13 Focus: anillo doble, visible en cream / ink / glass ───────── */
  --fr-focus:       0 0 0 2px var(--fr-cream), 0 0 0 4px var(--fr-teal);
  --fr-focus-inset: inset 0 0 0 2px var(--fr-teal);
  --fr-focus-ring:  rgba(0, 48, 59, 0.16);   /* halo de control enfocado   */

  /* ── 1.14 Motion: 3 duraciones, 1 curva, sin rebote ─────────────────── */
  --fr-ease:      cubic-bezier(0.2, 0, 0, 1);
  --fr-d-micro:   120ms;   /* hover, pressed, toggle                       */
  --fr-d-state:   200ms;   /* selección, expandir, tab                     */
  --fr-d-overlay: 260ms;   /* modal, drawer, sheet                         */
  --fr-d-topbar:  160ms;   /* cruce de capas de la topbar                  */

  /* ── 1.15 CONTRATO DE CAPAS · escala única y pequeña ────────────────── */
  /* Un solo lugar decide alturas. Ningún módulo escribe z-index literal.
     El apilado real lo garantiza .fr-layer con isolation:isolate: entre
     familias sólo compite este número. */
  --fr-z-base:     0;
  --fr-z-thead:    10;
  --fr-z-toolbar:  30;
  --fr-z-dock:     40;
  --fr-z-fab:      60;
  /* El pie identitario es shell, no dock de página: cuando está fijo vive en
     el plano de la barra, no en el zócalo de acciones de una pantalla. Los
     dos valores son los que el shell YA tenía en producción, y su orden
     importa: el menú de usuario cuelga de la barra, y si ambas capas
     empatasen el pie lo taparía por ser posterior en el DOM. Entre 1000 y
     1030 no hay ninguna otra capa del producto; por debajo del pie están la
     banda y la línea de 95 dvh de `indicadores_proceso` (998 y 999), que
     hoy quedan bajo él y deben seguir quedando. */
  --fr-z-footer:   1000;
  --fr-z-topbar:   1030;
  --fr-z-dropdown: 1100;
  --fr-z-menu:     1100;   /* alias de dropdown                            */
  --fr-z-drawer:   1200;
  --fr-z-sheet:    1300;
  --fr-z-modal:    1400;
  --fr-z-chooser:  1500;   /* se abre DESDE un modal: siempre por encima    */
  --fr-z-critical: 1600;
  --fr-z-toast:    1700;
  --fr-z-tooltip:  1800;

  /* ── 1.15b SAFE AREA · los insets del dispositivo, con nombre ────────
     El recorte del telefono —hora, cobertura, bateria, Dynamic Island, notch,
     barra de gestos— no es un numero que se pueda escribir. `env()` lo sirve
     y hasta ahora se consumia A MANO en 44 sitios del producto: no habia
     forma de auditarlo, de simularlo en un arnes ni de saber quien lo
     respetaba. Con nombre, si.

     El respaldo `0px` va DENTRO de `env()`, no fuera: en un navegador de
     escritorio el inset no existe y sin respaldo la declaracion entera seria
     invalida, no cero.

     REGLA DE CONSUMO — `max()`, NUNCA suma. `padding: calc(16px + safe)` da
     16 px de mas cuando el inset vale 0, que es el caso de TODAS las pantallas
     de escritorio. `padding: max(16px, safe)` da 16 cuando no hay recorte y
     el recorte cuando lo hay. Sumar solo es correcto donde el relleno base es
     estructural y el inset ADEMAS tiene que caber: la barra fija, por ejemplo,
     donde la fila de controles necesita su propio alto ENTERO por debajo del
     notch. Cada uso declara cual de las dos cosas es.                      */
  --fr-safe-top:    env(safe-area-inset-top, 0px);
  --fr-safe-right:  env(safe-area-inset-right, 0px);
  --fr-safe-bottom: env(safe-area-inset-bottom, 0px);
  --fr-safe-left:   env(safe-area-inset-left, 0px);

  /* ── 1.16 STICKY STACK · offsets derivados, nunca literales ─────────── */
  /* STICKY_STACK_SPEC.md. Prohibido `top: 83px` en CSS de módulo: toda
     barra pegajosa deriva su posición de estas variables. */
  /* RESERVA del shell: lo que el body compensa y de lo que cuelga el stack. */
  --fr-topbar-h:      44px;
  /* SUMA, no `max()`: la fila de controles necesita su alto entero POR
     DEBAJO del recorte. Es el caso estructural de §1.15b. */
  --fr-topbar-offset: calc(var(--fr-topbar-h) + var(--fr-safe-top));
  /* CAJA PINTADA de la barra: padding + alto útil de su fila de controles.
     No coincide con la reserva —hoy son 38,2 px dentro de 44— y esa holgura
     de 5,8 px es la que el producto lleva desde R1.3. Se conserva tal cual:
     alinearlas es una decisión visual, no un efecto colateral de migrar. */
  --fr-topbar-pad:    0.1rem;
  --fr-topbar-box:    34px;
  /* Objetivo táctil de los controles del shell: 32 px con ratón, 44 con
     dedo. Una sola definición para el botón de tema, el menú de usuario y
     el lockup (SHELL-UX-01 · S1). */
  --fr-topbar-target: 32px;
  --fr-toolbar-h:     0px;
  --fr-sticky-top:    calc(var(--fr-topbar-offset) + var(--fr-toolbar-h));
  --fr-dock-h:        0px;
  /* Alto del pie GLOBAL cuando está fijo. En táctil y en tablet pasa a
     flujo (OD-2 · A) y esta reserva se libera. */
  --fr-footer-h:      28px;
  --fr-menu-maxh:     calc(100vh - var(--fr-topbar-offset) - 16px);
  --fr-menu-maxh:     calc(100dvh - var(--fr-topbar-offset)
                      - var(--fr-safe-bottom) - 16px);
  --fr-scrollbar-w:   6px;
  /* Alto de la cabecera de tabla, MEDIDO: 7 + 7 de relleno, la caja del
     rótulo y el filete inferior dan 31 px en la densidad estándar y 27 en la
     densa. Existe como token porque el `scroll-padding` del scroller de
     tabla necesita descontarlo (04-layout §4.3) y ahí no hay forma de
     preguntarlo. 34 deja el margen óptico de la fila que llega debajo. */
  --fr-thead-h:       34px;

  /* ── 1.17 Layout · REGIMENES, LIENZO Y RITMO ────────────────────────
     Los seis regimenes del sistema. NO son dispositivos: son los anchos donde
     el CONTENIDO cambia de forma. Una @media no puede leer una variable, asi
     que estos numeros se escriben a mano en 07-responsive.css; viven aqui
     para que haya UN sitio donde consultarlos y para que nadie invente un
     septimo corte por su cuenta.

         COMPACT      <=  599    una columna, la tarjeta es el ancho
         MOBILE        600– 767  una columna con aire
         TABLET        768–1023  dos columnas, gutter claramente mayor
         TABLET_L     1024–1279  tres columnas
         DESKTOP      1280–1599  cuatro columnas
         WIDE         1600–1919  cuatro, el lienzo empieza a sobrar
         ULTRAWIDE    >=1920     la composicion se CENTRA y deja de crecer

     LIENZO. El ancho util no crece indefinidamente: a partir de
     `--fr-content-max` la composicion se centra y el resto es margen. Medido
     antes de esta fase: a 3.840 px las familias con `--wide` servian tarjetas
     de 3.720 px de ancho. Eso no es una tarjeta, es una pancarta.

     GUTTER. `clamp(20px, 6vw, 56px)`. El suelo de 20 es lo que un telefono
     necesita para que el texto no roce el borde; el 6vw hace que un iPad
     reciba 46–49 y no los 26 que daba el `4vw` anterior —la queja de
     «escritorio comprimido» empezaba ahi—; el techo de 56 evita que un
     monitor ancho se coma el lienzo por los lados.                        */
  --fr-content-max: 1720px;
  --fr-page-gutter: clamp(20px, 6vw, 56px);

  /* ISLA EN 4K. El techo de 1.720 arregló la pancarta —a 3.840 px una tarjeta
     medía 3.720— pero se pasó de frenada por el otro lado: en un monitor de
     3.840 dejaba 2.120 px de margen y la aplicación entera parecía una isla
     en mitad del escritorio. «Deja de crecer» no es «se queda del tamaño de
     un portátil»: el techo SUBE por escalones con la pantalla, y sigue
     teniendo tope. Los escalones están al final de este bloque. */
  /* Una pagina `--wide` es MAS ancha que la estandar, no infinita. */
  --fr-wide-max:    2280px;
  /* Nombres anteriores. Siguen vivos, apuntando a la geometria nueva y no a
     una copia: los consumen `.fr-page` (max-width) y la navegacion de
     formulario en 07-responsive, que sangra y compensa con el gutter. Tres
     reglas en total; ninguna hoja de modulo los usa. */
  --fr-page-max:  var(--fr-content-max);
  --fr-page-pad:  var(--fr-page-gutter);
  --fr-touch-min: 44px;      /* objetivo táctil mínimo, no negociable      */

  /* RITMO RESPONSIVE. La escala `--fr-s*` es FIJA y se queda fija: sus pasos
     pequenos (2, 4, 6, 8) son detalle optico y una tabla industrial densa
     depende de que no se muevan. Lo que si tiene que respirar es el ritmo de
     COMPOSICION, y para eso hay tokens propios. Un modulo que quiera aire
     consume estos; no redefine la escala.                                 */
  --fr-gap-grid:    clamp(14px, 1.9vw + 5px, 26px);   /* entre tarjetas    */
  --fr-gap-stack:   clamp(16px, 1.5vw + 8px, 28px);   /* entre bloques     */
  --fr-gap-section: clamp(22px, 2.4vw + 10px, 44px);  /* entre secciones   */
  --fr-pad-card:    clamp(16px, 1.1vw + 10px, 26px);  /* dentro de tarjeta */
  --fr-pad-modal:   clamp(16px, 1.4vw + 9px, 30px);   /* dentro de capa    */
  --fr-gap-form:    clamp(14px, 1.2vw + 8px, 22px);   /* entre campos      */
  /* Banda del pie que `.fr-page--app` reserva SIEMPRE. No deriva de
     --fr-footer-h a propósito: ese token vale 0 cuando el pie vuelve al
     flujo, y ahí la banda sigue haciendo falta porque la ocupa el pie al
     final del documento. Es la altura con la que los CRUD están medidos
     desde F-J-01; derivarla del pie fijo les cambiaría el alto en táctil. */
  --fr-page-foot: 28px;

  /* ── 1.17b RITMO DEL CROMO SUPERIOR ──────────────────────────────────
     El envoltorio del shell es `main.container-fluid py-3` (base.html), o
     sea 1 rem arriba y otro abajo escritos por Bootstrap. En un monitor eso
     es el ritmo correcto; en un iPhone 15 vertical son 32 px de los 852 que
     hay, y en apaisado 32 de 393.

     Se nombra aquí por dos razones. La primera es que `.fr-page--app`
     DESCUENTA ese relleno de su altura calculada, y tenerlo escrito a mano
     en dos sitios significa que el día que uno cambie el otro se queda con
     una altura falsa y aparece un scroll exterior. La segunda es que sin
     nombre no se puede compactar por régimen sin repetir el número.       */
  --fr-shell-pad-block: 1rem;
}

/* MÓVIL Y MÓVIL APAISADO. El cromo se compacta; el CONTENIDO no (§27 del
   encargo: compactar cromo antes que contenido). 8 px arriba y abajo siguen
   separando la primera pieza de la barra sin que parezca pegada. */
@media (max-width: 700px), (max-height: 500px) {
  :root { --fr-shell-pad-block: 0.5rem; }
}

/* ── 1.17c COMPACT HEIGHT · el régimen de POCA ALTURA ────────────────────
   FORTE_IOS_IPADOS_HARDENING_02B.

   El sistema tenía siete regímenes y los siete se deciden por ANCHO. Pero la
   altura también es un recurso, y en algunos sitios es EL recurso: un iPhone
   apaisado da 393 px, y con la barra táctil, un título, una franja de mando y
   un selector de nivel el contenido empezaba en el píxel 309 de esos 393.

   Hasta ahora eso se corregía con `@media (max-height: 500px)` sueltos —cinco
   en el sistema y tres más en la hoja del Registro de Inyección—, cada uno
   con su número y ninguno con nombre. Aquí el régimen se declara UNA vez y lo
   que cambia son TOKENS, así que cualquier pieza que consuma el ritmo de
   composición se compacta sola sin escribir una excepción propia.

   NO ES UN CORTE DE DISPOSITIVO. 500 px de alto es la altura por debajo de la
   cual una composición vertical deja de caber, y le pasa a un teléfono
   apaisado, a una tableta con el teclado abierto y a una ventana de
   escritorio baja. Es el mismo umbral que el contrato responsive ya declara
   (§8.6, «≤ 500px de alto · móvil apaisado»): se le pone nombre, no se
   inventa uno nuevo.

   Sólo baja el AIRE ENTRE BLOQUES. Ni el objetivo táctil, ni el cuerpo de
   texto, ni el relleno de una tarjeta que contiene datos: eso es contenido, y
   §3 del encargo lo deja fuera.                                          */
@media (max-height: 500px) {
  :root {
    --fr-compact-height: 1;      /* testigo legible para el arnés y los gates */
    --fr-gap-stack:   12px;      /* entre bloques    · desde clamp(16…28)     */
    --fr-gap-section: 16px;      /* entre secciones  · desde clamp(22…44)     */
  }
}

/* El dedo pide más barra y más zócalo que el ratón. 2 + 44 + 2 de padding y
   1,5 de borde ≈ 49,5 px: se reservan 50. Todo lo que deriva de la altura
   —la compensación del body y la altura calculada de los CRUD— se recalcula
   solo; no hay ninguna altura de barra escrita a mano. */
@media (pointer: coarse) {
  :root {
    --fr-topbar-h:      50px;
    --fr-topbar-box:    44px;
    --fr-topbar-pad:    2px;
    --fr-topbar-target: var(--fr-touch-min);
  }
}
/* El pie suelta el zócalo en cuanto la pantalla es estrecha o se toca con el
   dedo: ahí abajo va el action dock (OD-2 · A). Mismo umbral que usa el
   componente en 04-layout.css §4.4. */
@media (max-width: 991.98px), (pointer: coarse) {
  :root { --fr-footer-h: 0px; }
}

/* ── 1.18 Acentos de módulo · COLOR GOVERNANCE (OD-3 · A) ──────────────
   El campo `tarjeta.color` de BD se conserva intacto (05A no escribe en
   base de datos). La PRESENTACIÓN se normaliza: el adaptador de
   app/services/forte_color_governance.py resuelve cualquier valor de BD a
   una de estas familias curadas. Un HEX desconocido cae en `neutral`.
   Ver docs/VISUAL FORTE/PRODUCTION_UI_SYSTEM/COLOR_GOVERNANCE_CONTRACT.md */
:root {
  --fr-acc-teal-ink:    #00303b;  --fr-acc-teal-soft:    rgba(0, 48, 59, 0.08);
  --fr-acc-red-ink:     #9e2b1f;  --fr-acc-red-soft:     rgba(158, 43, 31, 0.10);
  --fr-acc-olive-ink:   #3f6437;  --fr-acc-olive-soft:   rgba(107, 149, 96, 0.14);
  --fr-acc-ink-ink:     #16080a;  --fr-acc-ink-soft:     rgba(22, 8, 10, 0.06);
  --fr-acc-slate-ink:   #3e3631;  --fr-acc-slate-soft:   rgba(62, 54, 49, 0.09);
  --fr-acc-neutral-ink: #635b56;  --fr-acc-neutral-soft: rgba(99, 91, 86, 0.08);
}

/* ── 1.20 Familias de módulo · identidad cromática de la Home (HOME V2) ─
   PRODUCT_DERIVED. NO son colores de marca y no se presentan como tales:
   GAP-14 = B_HYBRID_LOCKED rige aquí igual que para `--fr-danger`.

   ORIGEN. La escala clara es la de R6B · V2 EQUILIBRADA, la variante que el
   propietario aprobó para implantar. No se reinterpreta ni un valor: son los
   de `HOME_R6B_REFINEMENT/assets/r6b-paleta.css` (P16 SERENA), diseñados en
   CIE LCh con recorte de gamut por bisección sobre el CROMA —nunca por canal,
   que mueve el tono y es lo que hace que una paleta «se vaya a rosa»—.

   TRES PAPELES, no una rampa. Cada rol tiene un sitio y una superficie:

     plane      plano de identidad, PLANO   L 84–91 · C 8–25 · ~57 % de la tarjeta
     edge-rgb   filete inferior de familia  1 px al 34 %
     accent     placa · filete de la cifra · fondo del CTA al 10 %
     graphite   tinta del CTA
     ink        tinta del título
     plate-ink  glifo de la placa — se elige por CONTRASTE, no se supone

   POR QUÉ NO BASTA `--fr-acc-*`. El gobierno de color (OD-3 · A) resuelve
   cualquier HEX de BD a seis familias curadas, y ahí `#008cd7` (Personal) y
   `#4db6ac` (Equipos) caen los DOS en `teal`. Para el acento de un control eso
   es correcto y no se toca; para la identidad de dos módulos contiguos
   significa que no se distinguen. Esta escala resuelve otro problema
   —identificar el módulo— y por eso CONVIVE con la otra, no la sustituye.

     RECALIBRACION (RESPONSIVE_GLOBAL_01). CONTROL OPERATIVO deja de ser
     ocre / arena / mostaza por decision del propietario y pasa a AMARILLO
     VERDOSO — lima tecnico —. El ocre vivia en h 93; el jade de Equipos en
     h 182. El lima se situa en h 111: inequivocamente amarillo-verdoso, a 71
     grados del jade y a 18 del ocre que sustituye. Croma MEDIO (30 en el
     plano, 28 servido): un lima de croma 60 es un rotulador. dE2000 frente al
     ocre 8,0 —se nota—, frente al jade 17,7 —no se confunden—.

     Y no se movio sola. Medir las cinco juntas, que es lo que el encargo
     exige, destapo que el par mas debil de la matriz NO era Control con
     nadie: era PERSONAL con DESARROLLO, dE2000 8,7, los dos azules y sin mas
     diferencia que el croma. Personal baja a L 83,5 y sube a croma 21;
     Desarrollo baja a croma 3,7 y se enfria a h 254 para leerse como GRIS con
     sesgo. El peor par pasa a 13,0 en claro y de 6,5 a 9,7 en oscuro.

     ACABADO (V2E). La cabecera deja de ser un `background-color` opaco: se
     sirve al 94 % sobre el lienzo, y para eso hace falta el RGB de cada
     plano. Es lo que separa un vidrio tintado de un rectángulo pastel.

     Y el ESCÁNER se mueve catorce grados de tono —de h 22 a h 36— bajando
     tres puntos de croma: de rosa a terracota clara. Seguía siendo el plano
     más «de caramelo» de los cinco, y su franja ocupa todo el ancho. El
     techo del desplazamiento lo pone `control`, que ya es arena: a h 36 la
     separación con él es de dE2000 17,4, de sobra.

     AFINADO EN PANTALLA REAL (V2D). Los planos bajan entre un 12 y un 28 %
     de croma conservando tono y claridad. Dos motivos, los dos visibles sólo
     al verlo a tamaño real y no en la maqueta de 1.180 px:

       · el plano del ESCÁNER era el más saturado de los cinco y por tanto lo
         primero que el ojo iba a buscar — justo lo contrario de lo que V2
         decidió. A todo el ancho de la banda, pesaba. Baja de croma 21,0 a
         15,4, que es la corrección mayor;
       · sobre 1.584 px de fila el color ocupa mucha más superficie que sobre
         la maqueta, y tres puntos de croma de más se notan.

     Comprobado que no rompe nada: la separación entre familias sigue en
     dE2000 9,06 en el peor par y los contrastes siguen sobre AA.

   MEDIDO sobre las superficies reales del sistema, no estimado:
     título/plano ≥ 8,09:1 · placa/acento ≥ 4,38:1 · CTA/fondo ≥ 5,54:1
     dE2000 entre familias ≥ 9,84 (claro) · ≥ 6,54 (oscuro)                  */
:root {
  --fr-mod-scan-plane:      #f5d6d1;
  --fr-mod-scan-plane-rgb:  245, 214, 209;
  --fr-mod-scan-edge-rgb:   236, 156, 144;
  --fr-mod-scan-accent:     #ba6552;
  --fr-mod-scan-accent-rgb: 186, 101, 82;
  --fr-mod-scan-graphite:   #77493f;
  --fr-mod-scan-ink:        #43302d;
  --fr-mod-scan-plate-ink:  var(--fr-ink);

  --fr-mod-personal-plane:      #a6d7f3;
  --fr-mod-personal-plane-rgb:  166, 215, 243;
  --fr-mod-personal-edge-rgb:   0, 173, 226;
  --fr-mod-personal-accent:     #0081a8;
  --fr-mod-personal-accent-rgb: 0, 129, 168;
  --fr-mod-personal-graphite:   #065c79;
  --fr-mod-personal-ink:        #253741;
  --fr-mod-personal-plate-ink:  var(--fr-ink);

  --fr-mod-equipos-plane:      #b8e3db;
  --fr-mod-equipos-plane-rgb:  184, 227, 219;
  --fr-mod-equipos-edge-rgb:   81, 185, 169;
  --fr-mod-equipos-accent:     #008c7e;
  --fr-mod-equipos-accent-rgb: 0, 140, 126;
  --fr-mod-equipos-graphite:   #1e5e56;
  --fr-mod-equipos-ink:        #263835;
  --fr-mod-equipos-plate-ink:  var(--fr-ink);

  --fr-mod-control-plane:      #dfe4aa;
  --fr-mod-control-plane-rgb:  223, 228, 170;
  --fr-mod-control-edge-rgb:   184, 194, 106;
  --fr-mod-control-accent:     #7f8a27;
  --fr-mod-control-accent-rgb: 127, 138, 39;
  --fr-mod-control-graphite:   #60652a;
  --fr-mod-control-ink:        #3c3b29;
  --fr-mod-control-plate-ink:  var(--fr-ink);

  --fr-mod-dev-plane:      #e1e7ed;
  --fr-mod-dev-plane-rgb:  225, 231, 237;
  --fr-mod-dev-edge-rgb:   164, 181, 196;
  --fr-mod-dev-accent:     #4e7188;
  --fr-mod-dev-accent-rgb: 78, 113, 136;
  --fr-mod-dev-graphite:   #465763;
  --fr-mod-dev-ink:        #30353a;
  --fr-mod-dev-plate-ink:  var(--fr-cream);

  /* Alias por defecto. Una tarjeta cuya familia no esté declarada NO puede
     quedarse en CADENA VACÍA: en R6B ese fallo dejó las placas sin fondo y el
     glifo invisible, y no lo denunció ni el navegador ni ningún gate. */
  /* TRANSMISION del plano de identidad sobre el lienzo. NO es una
     constante: el mismo tinte no pesa lo mismo a 260 px que ocupando el
     ancho entero de un telefono, donde la cabecera deja de ser una tarjeta y
     pasa a ser una banda. 07-responsive.css la baja en el regimen compacto.
     Medido en los dos extremos: 0 fallos de contraste en ambos.          */
  --fr-mod-tint:       0.94;
  --fr-mod-plane:      var(--fr-n100);
  --fr-mod-plane-rgb:  244, 240, 236;
  --fr-mod-edge-rgb:   var(--fr-ink-rgb);
  --fr-mod-accent:     var(--fr-acc-neutral-ink);
  --fr-mod-accent-rgb: 99, 91, 86;
  --fr-mod-graphite:   var(--fr-acc-neutral-ink);
  --fr-mod-ink:        var(--fr-text);
  --fr-mod-plate-ink:  var(--fr-cream);
}

/* ══════════════════════════════════════════════════════════════════════
   1.18b · LIENZO EN PANTALLAS MUY GRANDES (ver ISLA EN 4K)
   ══════════════════════════════════════════════════════════════════════
   Sólo cambian los DOS techos del lienzo. Ni el gutter, ni el ritmo, ni la
   escala de espaciado: una tabla industrial densa depende de que no se
   muevan. Medido en la Home: 2.560 px pasa del 67% al 80% de ancho útil y
   3.840 px del 45% al 63%. */
@media (min-width: 2200px) {
  :root { --fr-content-max: 2040px; --fr-wide-max: 2600px; }
}
@media (min-width: 3000px) {
  :root { --fr-content-max: 2400px; --fr-wide-max: 3000px; }
}

/* ══════════════════════════════════════════════════════════════════════
   1.19 · TEMA OSCURO (OD-4 · A — entra en la foundation)
   ══════════════════════════════════════════════════════════════════════
   PRODUCT_DERIVED: el manual corporativo no define modo oscuro.
   Se enganchan los DOS contratos: `data-fr-theme` (propio del sistema) y
   `data-theme` (el que el runtime de FORTEONE ya escribe en <html>), para
   que el toggle existente siga gobernando sin JavaScript adicional. */
[data-fr-theme="dark"],
[data-theme="dark"] {
  --fr-canvas:   #120709;
  --fr-work:     #1a0e10;
  --fr-raised:   #221316;
  --fr-top:      #2a1a1d;
  --fr-hover:    rgba(240, 238, 233, 0.05);
  --fr-active:   rgba(240, 238, 233, 0.09);
  --fr-selected: #1e2f33;
  --fr-text:     rgba(240, 238, 233, 0.94);
  --fr-text-2:   rgba(240, 238, 233, 0.72);
  --fr-text-3:   rgba(240, 238, 233, 0.50);
  --fr-text-4:   rgba(240, 238, 233, 0.30);
  --fr-link:     #8ec4d0;
  --fr-line-1:   rgba(240, 238, 233, 0.06);
  --fr-line-2:   rgba(240, 238, 233, 0.12);
  --fr-line-3:   rgba(240, 238, 233, 0.24);
  --fr-m3-bg:    rgba(26, 13, 15, 0.86);
  --fr-m3-solid: #211114;
  --fr-m4-bg:    linear-gradient(180deg, rgba(34, 19, 22, 0.92), rgba(24, 12, 14, 0.84));
  --fr-m4-solid: #201013;
  --fr-m6-bg:    rgba(30, 16, 19, 0.96);
  --fr-m6-solid: #241317;
  --fr-m7-bg: linear-gradient(180deg, rgba(240, 238, 233, 0.07), rgba(240, 238, 233, 0.025) 34%, rgba(240, 238, 233, 0.012)), rgba(18, 9, 11, 0.58);
  --fr-m7-solid: #1d1013;
  --fr-edge-out: var(--fr-edge-out-dark);
  --fr-edge-in:  var(--fr-edge-in-dark);
  --fr-edge-rim: inset 0 0 0 0.5px rgba(240, 238, 233, 0.10);
  --fr-sh-contact: 0 1px 2px rgba(0, 0, 0, 0.40);
  --fr-sh-card:    0 1px 2px rgba(0, 0, 0, 0.35), 0 8px 20px -8px rgba(0, 0, 0, 0.45);
  --fr-sh-float:   0 1px 3px rgba(0, 0, 0, 0.40), 0 18px 44px -16px rgba(0, 0, 0, 0.55);
  --fr-sh-menu:    0 2px 6px rgba(0, 0, 0, 0.35), 0 12px 32px -12px rgba(0, 0, 0, 0.55);
  --fr-sh-modal:   0 2px 4px rgba(0, 0, 0, 0.40), 0 32px 72px -24px rgba(0, 0, 0, 0.65);
  --fr-ok: #9cc4a4; --fr-warn: #d9b36a; --fr-danger: #d99a91; --fr-info: #8ec4d0;
  --fr-focus:      0 0 0 2px var(--fr-ink), 0 0 0 4px #8ec4d0;
  --fr-focus-ring: rgba(142, 196, 208, 0.22);
  --fr-acc-teal-ink:    #8ec4d0;  --fr-acc-teal-soft:    rgba(142, 196, 208, 0.12);
  --fr-acc-red-ink:     #d99a91;  --fr-acc-red-soft:     rgba(217, 154, 145, 0.12);
  --fr-acc-olive-ink:   #a6c0a0;  --fr-acc-olive-soft:   rgba(166, 192, 160, 0.12);
  --fr-acc-ink-ink:     #e8e3de;  --fr-acc-ink-soft:     rgba(240, 238, 233, 0.07);
  --fr-acc-slate-ink:   #bdb5ab;  --fr-acc-slate-soft:   rgba(189, 181, 171, 0.10);
  --fr-acc-neutral-ink: #8f8680;  --fr-acc-neutral-soft: rgba(143, 134, 128, 0.10);

  /* Familias de módulo (§1.20) en oscuro. R6B NO resolvió dark; esto se
     deriva con el mismo instrumento —CIE LCh, recorte por croma— conservando
     el TONO y la PROPORCIÓN de croma entre familias, y cambiando sólo la
     claridad: plano L 22 · filete L 58 · acento L 72 · grafito L 80.

     LA PRIMERA DERIVACIÓN SE TUVO QUE TIRAR. Con croma FIJO para las cinco,
     Personal (h 243,5) y Desarrollo (h 250,6) medían dE2000 1,85: son casi el
     mismo azul y en claro sólo los separa el croma —21,0 contra 8,0—. Igualarlo
     borra la única diferencia que tenían. Escalado, el peor par queda en 6,54. */
  --fr-mod-scan-plane:      #482e2d;
  --fr-mod-scan-plane-rgb:  72, 46, 45;
  --fr-mod-scan-edge-rgb:   204, 113, 113;
  --fr-mod-scan-accent:     #fa9493;
  --fr-mod-scan-accent-rgb: 250, 148, 147;
  --fr-mod-scan-graphite:   #f5b7b5;
  --fr-mod-scan-ink:        var(--fr-text);
  --fr-mod-scan-plate-ink:  var(--fr-ink);

  --fr-mod-personal-plane:      #153949;
  --fr-mod-personal-plane-rgb:  21, 57, 73;
  --fr-mod-personal-edge-rgb:   0, 150, 197;
  --fr-mod-personal-accent:     #6abae0;
  --fr-mod-personal-accent-rgb: 106, 186, 224;
  --fr-mod-personal-graphite:   #a5cce2;
  --fr-mod-personal-ink:        var(--fr-text);
  --fr-mod-personal-plate-ink:  var(--fr-ink);

  --fr-mod-equipos-plane:      #203935;
  --fr-mod-equipos-plane-rgb:  32, 57, 53;
  --fr-mod-equipos-edge-rgb:   37, 156, 142;
  --fr-mod-equipos-accent:     #5dc1b2;
  --fr-mod-equipos-accent-rgb: 93, 193, 178;
  --fr-mod-equipos-graphite:   #9ed0c7;
  --fr-mod-equipos-ink:        var(--fr-text);
  --fr-mod-equipos-plate-ink:  var(--fr-ink);

  --fr-mod-control-plane:      #343620;
  --fr-mod-control-plane-rgb:  52, 54, 32;
  --fr-mod-control-edge-rgb:   135, 146, 61;
  --fr-mod-control-accent:     #acb858;
  --fr-mod-control-accent-rgb: 172, 184, 88;
  --fr-mod-control-graphite:   #c6ca95;
  --fr-mod-control-ink:        var(--fr-text);
  --fr-mod-control-plate-ink:  var(--fr-ink);

  --fr-mod-dev-plane:      #313539;
  --fr-mod-dev-plane-rgb:  49, 53, 57;
  --fr-mod-dev-edge-rgb:   123, 142, 158;
  --fr-mod-dev-accent:     #95b5cb;
  --fr-mod-dev-accent-rgb: 149, 181, 203;
  --fr-mod-dev-graphite:   #b8c9d6;
  --fr-mod-dev-ink:        var(--fr-text);
  --fr-mod-dev-plate-ink:  var(--fr-ink);

  --fr-mod-plane:      var(--fr-raised);
  --fr-mod-plane-rgb:  34, 19, 22;
  --fr-mod-edge-rgb:   240, 238, 233;
  --fr-mod-accent:     var(--fr-acc-neutral-ink);
  --fr-mod-accent-rgb: 143, 134, 128;
  --fr-mod-graphite:   var(--fr-acc-neutral-ink);
  --fr-mod-ink:        var(--fr-text);
  --fr-mod-plate-ink:  var(--fr-ink);
}

/* ===== src/02-foundations.css ===== */
/* ════════════════════════════════════════════════════════════════════════
   FORTE UI · 02 · FOUNDATIONS
   ════════════════════════════════════════════════════════════════════════
   Base tipográfica, foco, scroll, selección y utilidades mínimas.

   ÁMBITO: todo lo de este fichero cuelga de `.fr-app`. Ninguna regla toca
   `html`, `body` ni selectores de elemento desnudos: cargar el sistema en
   una pantalla no migrada NO puede cambiar su aspecto. Esa es la condición
   que permite servir forte-ui.css globalmente sin big-bang (§33).
   ════════════════════════════════════════════════════════════════════════ */

.fr-app {
  font-family: var(--fr-font);
  font-size: var(--fr-t-base);
  line-height: var(--fr-lh-body);
  color: var(--fr-text);
  background: var(--fr-canvas);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
.fr-app *,
.fr-app *::before,
.fr-app *::after { box-sizing: border-box; }

/* ── Enlaces ────────────────────────────────────────────────────────────
   OJO CON LA ESPECIFICIDAD (defecto real, corregido en 05B · ola 1):
   `.fr-app a` vale (0,2,0) y `.fr-btn--primary` vale (0,1,0). Sin la
   exclusión de abajo, CUALQUIER enlace con aspecto de componente —un
   `<a class="fr-btn fr-btn--primary">`, que es como se escribe media
   navegación del producto— perdía el color de texto de su intención y
   heredaba el del enlace: crema sobre tinta pasaba a petróleo sobre
   tinta, es decir, ilegible. Medido en la migración de F06 y F08.

   Un enlace que ES un componente no es un enlace de texto: su color y su
   subrayado los decide el componente. La lista es cerrada y corta a
   propósito; si aparece un componente nuevo que pueda ser `<a>`, se añade
   aquí y no se parchea en el módulo. */
.fr-app a:not(.fr-btn):not(.fr-iconbtn):not(.fr-picker):not(.fr-module):not(.fr-modulerow):not(.fr-menu-item):not(.fr-chip):not(.fr-tab):not(.fr-chooser-opt):not(.fr-modcard-link) {
  color: var(--fr-link);
  text-decoration: none;
}
.fr-app a:not(.fr-btn):not(.fr-iconbtn):not(.fr-picker):not(.fr-module):not(.fr-modulerow):not(.fr-menu-item):not(.fr-chip):not(.fr-tab):not(.fr-chooser-opt):not(.fr-modcard-link):hover {
  text-decoration: underline;
  text-underline-offset: 2px;
}
.fr-app a.fr-btn,
.fr-app a.fr-iconbtn,
.fr-app a.fr-picker,
.fr-app a.fr-menu-item,
.fr-app a.fr-chip,
.fr-app a.fr-tab,
.fr-app a.fr-chooser-opt,
.fr-app a.fr-modcard-link { text-decoration: none; }
.fr-app ::selection { background: rgba(0, 48, 59, 0.18); }

/* ── `hidden` gana a cualquier componente ───────────────────────────────
   `[hidden]{display:none}` es una regla del NAVEGADOR, y cualquier
   componente que declare su propio `display` la derrota: un `.fr-empty`
   (flex), un `.fr-btn` (inline-flex) o una `.fr-row` marcados como
   `hidden` seguían viéndose. El sistema ya lo resolvía para `.fr-layer`;
   aquí se generaliza, con ámbito y sin `!important`. Ocultar algo es
   estado, no aspecto: si el marcado dice `hidden`, no se pinta. */
.fr-app [hidden],
body.fr-ui [hidden] { display: none; }

/* ── Foco · una sola definición para todo el producto ───────────────────
   El anillo doble se ve sobre cream, sobre ink y sobre vidrio. Ningún
   componente define su propio foco: si necesita otro, extiende éste. */
.fr-app :focus-visible {
  outline: none;
  box-shadow: var(--fr-focus);
  border-radius: var(--fr-r-ctl);
}

/* ── Números técnicos: SIEMPRE tabulares ────────────────────────────────
   Una cifra de proceso que baila al actualizarse no es legible en planta. */
.fr-num,
.fr-table td.fr-td-num,
.fr-kpi-value,
.fr-unit-input input { font-variant-numeric: tabular-nums lining-nums; font-feature-settings: "tnum" 1; }

/* ── Scroll local · barra fina visible ──────────────────────────────────
   Primitiva de scroll. Ver docs PRODUCTION_UI_SYSTEM/RESPONSIVE.md para
   cuándo usar page scroll, local scroll o scroll de capa. */
.fr-scroll { scrollbar-width: thin; scrollbar-color: rgba(22, 8, 10, 0.22) transparent; }
.fr-scroll::-webkit-scrollbar { width: var(--fr-scrollbar-w); height: var(--fr-scrollbar-w); }
.fr-scroll::-webkit-scrollbar-thumb { background: rgba(22, 8, 10, 0.18); border-radius: 3px; }
.fr-scroll::-webkit-scrollbar-thumb:hover { background: rgba(22, 8, 10, 0.30); }
[data-fr-theme="dark"] .fr-scroll,
[data-theme="dark"] .fr-scroll { scrollbar-color: rgba(240, 238, 233, 0.22) transparent; }
[data-fr-theme="dark"] .fr-scroll::-webkit-scrollbar-thumb,
[data-theme="dark"] .fr-scroll::-webkit-scrollbar-thumb { background: rgba(240, 238, 233, 0.18); }

/* Scroll contenido: el gesto no se propaga al documento de detrás. */
.fr-scroll--contain { overscroll-behavior: contain; }

/* ── Motion · sólo las cuatro entradas autorizadas ──────────────────────
   Sin rebote, sin spring, sin filtros animados. */
.fr-enter-overlay { animation: fr-overlay-in var(--fr-d-overlay) var(--fr-ease); }
@keyframes fr-overlay-in {
  from { opacity: 0; transform: translateY(8px) scale(0.995); }
  to   { opacity: 1; transform: none; }
}
.fr-enter-menu { animation: fr-menu-in var(--fr-d-micro) var(--fr-ease); }
@keyframes fr-menu-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}
.fr-enter-sheet { animation: fr-sheet-in var(--fr-d-overlay) var(--fr-ease); }
@keyframes fr-sheet-in {
  from { transform: translateY(24px); opacity: 0.4; }
  to   { transform: none; opacity: 1; }
}
@keyframes fr-spin { to { transform: rotate(360deg); } }

/* ── Tipografía de apoyo ────────────────────────────────────────────────
   La Línea Beyond (BRAND_CANONICAL) sólo se construye acompañando
   tipografía: grosor derivado del asta de la I mayúscula de Inter, borde
   inferior en la línea base. Cualquier otra línea del producto usa los
   filetes PRODUCT_DERIVED de abajo y JAMÁS se llama «Beyond». */
.fr-beyond-lockup { display: flex; align-items: baseline; gap: 0.44em; }
.fr-beyond-lockup .word { font-weight: var(--fr-fw-bold); letter-spacing: 0.02em; line-height: 1; }
.fr-beyond-lockup .line {
  flex: 0 0 auto; align-self: baseline;
  height: 0.0925em;
  transform: translateY(-0.005em);
  background: currentColor; border: 0;
}
.fr-divider    { height: 1px; background: var(--fr-line-2); border: 0; margin: var(--fr-s6) 0; }
.fr-hairline   { height: 1px; background: var(--fr-line-1); border: 0; }
.fr-rule-strong{ height: 1px; background: var(--fr-line-3); border: 0; }

/* ── Utilidades mínimas ─────────────────────────────────────────────────
   Deliberadamente pocas: el sistema no es una librería de utilidades. Cada
   una existe porque sustituye a un `style=""` que aparecía repetido en el
   marcado, no porque «pueda hacer falta». */
.fr-row       { display: flex; align-items: center; gap: var(--fr-s4); flex-wrap: wrap; }
.fr-stack     { display: flex; flex-direction: column; gap: var(--fr-s4); }
.fr-grow      { flex: 1 1 auto; min-width: 0; }
.fr-muted     { color: var(--fr-text-3); }
.fr-small     { font-size: var(--fr-t-mini); }
.fr-right     { text-align: right; }
.fr-mono-num  { font-variant-numeric: tabular-nums; }
/* `color: inherit` no es decorativo: Bootstrap pinta `code` de rosa con un
   selector de ELEMENTO, y `.fr-mono` ganaba la tipografía pero no el color,
   así que un `<code class="fr-mono">` salía rosa dentro de un párrafo
   apagado. Un identificador técnico no elige su color: lo hereda. */
.fr-mono      { font-family: var(--fr-font-mono); font-size: 0.94em; color: inherit; }
/* Color semántico sobre TEXTO — una cifra de diagnóstico, el valor de una
   celda, el resultado de una comprobación. Existen porque sustituyen a
   cuarenta `style="color: …"` escritos a mano en las pantallas de
   administración de datos, cada uno con su propio HEX. Nunca son colores
   corporativos: son los derivados funcionales (GAP-14 = B_HYBRID_LOCKED). */
.fr-ok-text     { color: var(--fr-ok); }
.fr-warn-text   { color: var(--fr-warn); }
.fr-danger-text { color: var(--fr-danger); }
.fr-info-text   { color: var(--fr-info); }
.fr-pad       { padding: var(--fr-s5); }
.fr-pad-tight { padding: var(--fr-s4); }
.fr-rounded   { border-radius: var(--fr-r-card); }
.fr-mb        { margin-bottom: var(--fr-s5); }
.fr-tight     { margin: 0; }
.fr-on-dark   { color: var(--fr-text-inv-2); }
.fr-sr,
.fr-visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.fr-kbd {
  display: inline-block; min-width: 18px; padding: 1px 5px; text-align: center;
  border: 1px solid var(--fr-line-2); border-bottom-width: 2px; border-radius: 3px;
  background: var(--fr-n50); font-size: 0.625rem; font-weight: var(--fr-fw-med);
  color: var(--fr-text-3); font-family: var(--fr-font);
}

/* ===== src/03-materials.css ===== */
/* ════════════════════════════════════════════════════════════════════════
   FORTE UI · 03 · MATERIALES
   ════════════════════════════════════════════════════════════════════════
   REGLA ABSOLUTA DEL SISTEMA (§12 del encargo 05A):

       `backdrop-filter` NO puede declararse fuera de este fichero.

   Toda translucidez del producto —modal, drawer, sheet, menú, topbar,
   velo— compone su material aquí. Un módulo que necesite vidrio aplica una
   clase `.fr-m3…m6`; no escribe su propia receta. El gate
   tests/test_forte_ui_material_contract.py falla si aparece una nueva.

   Excepciones vivas y auditadas: la topbar (04-layout.css) es la única
   pieza que compone DOS capas con brightness previo, porque su problema
   —tinta corporativa sobre lienzo claro— no se resuelve con un material
   genérico; está documentada en su sitio y cuenta como material del
   sistema, no como isla.

   ESCALERA
     M0 canvas   fondo de página            → var(--fr-canvas)
     M1 work     superficie de trabajo      → .fr-m1   (sólido)
     M2 raised   panel / card elevado       → .fr-m2   (sólido)
     M3 veil     dropdown, popover, tooltip → .fr-m3   (glass sutil)
     M4 pane     modal, drawer, sheet       → .fr-m4   (glass estructural)
     M5 plate    topbar, dock, toolbar      → .fr-m5   (glass oscuro)
     M6 sheet-tech confirmación destructiva → .fr-m6   (casi sólido)
     M7 thin     lámina fina de tarjeta    → .fr-m7   (glass fino)

   WORK = SOLID · FLOATING = SELECTIVE PREMIUM LIQUID GLASS · PRINT = SOLID
   (CLAUDE.md §8.3)
   ════════════════════════════════════════════════════════════════════════ */

.fr-m1 {
  background: var(--fr-work);
  box-shadow: var(--fr-edge-out);
}

.fr-m2 {
  background: var(--fr-raised);
  border-radius: var(--fr-r-card);
  box-shadow: var(--fr-edge-out), var(--fr-sh-card);
}

.fr-m3 {
  background: var(--fr-m3-bg);
  -webkit-backdrop-filter: blur(var(--fr-m3-blur)) saturate(var(--fr-m3-sat));
  backdrop-filter: blur(var(--fr-m3-blur)) saturate(var(--fr-m3-sat));
  border-radius: var(--fr-r-float);
  box-shadow: var(--fr-edge-out), var(--fr-edge-in), var(--fr-sh-menu);
}

.fr-m4 {
  background: var(--fr-m4-bg);
  -webkit-backdrop-filter: blur(var(--fr-m4-blur)) saturate(var(--fr-m4-sat));
  backdrop-filter: blur(var(--fr-m4-blur)) saturate(var(--fr-m4-sat));
  border-radius: var(--fr-r-modal);
  box-shadow: var(--fr-edge-out), var(--fr-edge-in), var(--fr-edge-rim), var(--fr-sh-modal);
}

.fr-m5 {
  background: var(--fr-m5-bg);
  -webkit-backdrop-filter: blur(var(--fr-m5-blur)) saturate(var(--fr-m5-sat));
  backdrop-filter: blur(var(--fr-m5-blur)) saturate(var(--fr-m5-sat));
  color: var(--fr-text-inv);
  box-shadow: var(--fr-edge-in-dark), 0 2px 12px rgba(0, 0, 0, 0.25);
}

/* M7 THIN — la lámina fina de la tarjeta de módulo (HOME V2).
   ES DISTINTO DE M3, y la diferencia no es el número de píxeles de
   desenfoque: M3 flota SOBRE la página y transmite un fondo lejano; M7 es
   una capa INTERIOR de una pieza y transmite el plano de identidad que tiene
   justo encima —el color de su propia tarjeta—. Por eso lleva la mitad de
   desenfoque, casi nada de saturación y su borde superior es un ESCALÓN DE
   VALOR de dos píxeles (una línea de tinta al 8 % y una de luz al 70 % justo
   debajo) en vez de una sombra: a 1:1 se lee como un canto real y a media
   escala desaparece. Eso es la microprofundidad, y cuesta dos declaraciones.

   No lleva radio ni sombra propios: los pone la pieza que lo consume. */
/* El desenfoque NO sube. Lo que mejora es el CANTO: la frontera entre el
   plano de identidad y el de contenido pasa de dos líneas a tres —un pelo
   del acento de la familia, la línea de tinta y la luz—, de modo que el
   escalón tiene color propio en vez de ser un corte neutro. Sigue midiendo
   dos píxeles y sigue desapareciendo a media escala. */
.fr-m7 {
  background: var(--fr-m7-bg);
  -webkit-backdrop-filter: blur(var(--fr-m7-blur)) saturate(var(--fr-m7-sat));
  backdrop-filter: blur(var(--fr-m7-blur)) saturate(var(--fr-m7-sat));
  box-shadow:
    inset 0 1px 0 rgba(var(--fr-mod-accent-rgb, var(--fr-ink-rgb)), 0.10),
    inset 0 2px 0 -1px rgba(var(--fr-ink-rgb), 0.07),
    inset 0 3px 0 -1px rgba(255, 255, 255, 0.72);
}

.fr-m6 {
  background: var(--fr-m6-bg);
  -webkit-backdrop-filter: blur(var(--fr-m6-blur)) saturate(var(--fr-m6-sat));
  backdrop-filter: blur(var(--fr-m6-blur)) saturate(var(--fr-m6-sat));
  border-radius: var(--fr-r-modal);
  box-shadow: var(--fr-edge-out), var(--fr-edge-in), var(--fr-sh-modal);
}

/* ── Velo de capa ───────────────────────────────────────────────────────
   Único backdrop del producto. Vive SIEMPRE dentro de un `.fr-layer`
   (ver 06-patterns.css): su altura la da el contexto de apilado de la
   capa, nunca un z-index propio. */
.fr-veil {
  position: absolute; inset: 0; z-index: 0;
  background: var(--fr-veil-bg);
  -webkit-backdrop-filter: blur(var(--fr-veil-blur)) saturate(var(--fr-veil-sat));
  backdrop-filter: blur(var(--fr-veil-blur)) saturate(var(--fr-veil-sat));
}

/* ── Fallback duro sin backdrop-filter ──────────────────────────────────
   La jerarquía se mantiene con sólidos de la misma familia. Nunca «caja
   gris»: el producto sigue siendo legible en un navegador sin soporte. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .fr-m3   { background: var(--fr-m3-solid); }
  .fr-m4   { background: var(--fr-m4-solid); }
  .fr-m5   { background: var(--fr-m5-solid); }
  .fr-m6   { background: var(--fr-m6-solid); }
  .fr-m7   { background: var(--fr-m7-solid); }
  .fr-veil { background: var(--fr-veil-solid); }
}

/* ── REDUCED_EFFECTS ────────────────────────────────────────────────────
   `.fr-nobf` fuerza el fallback en QA. `[data-fr-effects="reduced"]` es el
   modo de rendimiento del producto: equipos de planta antiguos donde el
   desenfoque cuesta scroll. La jerarquía no cambia, sólo el coste. */
.fr-nobf .fr-m3, [data-fr-effects="reduced"] .fr-m3 { background: var(--fr-m3-solid); -webkit-backdrop-filter: none; backdrop-filter: none; }
.fr-nobf .fr-m4, [data-fr-effects="reduced"] .fr-m4 { background: var(--fr-m4-solid); -webkit-backdrop-filter: none; backdrop-filter: none; }
.fr-nobf .fr-m5, [data-fr-effects="reduced"] .fr-m5 { background: var(--fr-m5-solid); -webkit-backdrop-filter: none; backdrop-filter: none; }
.fr-nobf .fr-m6, [data-fr-effects="reduced"] .fr-m6 { background: var(--fr-m6-solid); -webkit-backdrop-filter: none; backdrop-filter: none; }
.fr-nobf .fr-m7, [data-fr-effects="reduced"] .fr-m7 { background: var(--fr-m7-solid); -webkit-backdrop-filter: none; backdrop-filter: none; }
.fr-nobf .fr-veil, [data-fr-effects="reduced"] .fr-veil { background: var(--fr-veil-solid); -webkit-backdrop-filter: none; backdrop-filter: none; }
[data-fr-effects="reduced"] .fr-m2 { box-shadow: var(--fr-edge-out), var(--fr-sh-contact); }

/* ===== src/04-layout.css ===== */
/* ════════════════════════════════════════════════════════════════════════
   FORTE UI · 04 · LAYOUT
   ════════════════════════════════════════════════════════════════════════
   Shell (topbar · toolbar · dock · footer), contenedores de página y las
   primitivas de sticky. Aquí vive la ÚNICA topbar del producto y los
   ÚNICOS offsets pegajosos: ningún módulo escribe `top: 83px`.
   ════════════════════════════════════════════════════════════════════════ */

/* ══════════════════════════════════════════════════════════════════════
   4.1 · TOPBAR
   ══════════════════════════════════════════════════════════════════════
   MATERIAL DE NEGRO FORTE, NO LAVADO GRIS.

   Diagnóstico del defecto que corrige (aritmética, no opinión): una barra
   `rgba(16,7,9,.72)` sobre lienzo Blanco Forte compone
       0,72·(22,8,10) + 0,28·(240,238,233) = (83,74,73) → #534A49
   es decir, un GRIS. Ningún desenfoque devuelve ese color a la familia de
   la tinta.

   Corrección: apagar el fondo ANTES de componer. `backdrop-filter` admite
   `brightness()`, así que lo que atraviesa la barra entra ya oscurecido y
   saturado; el compuesto se queda en familia ink incluso con alfas bajas y
   el fondo SIGUE LEYÉNDOSE, tintado.
       T2 · 0,82·(22,8,10) + 0,18·(132,131,128) = (42,30,31) → #2A1E1F

   Materialidad, no opacidad: la barra es una PLACA — degradado vertical
   corto cuya media es exactamente la tinta corporativa. Sin glow, sin
   lechosidad, sin sombra grande.

   Estados: reposo y desplazamiento son DOS capas que se cruzan por
   opacidad. El desenfoque no se interpola nunca: la capa desplazada nace
   con el filtro puesto pero a opacidad 0, luego es invisible en t=0.

   DOSIS ACTIVA: T2 (OD-R3-2, decidida por el propietario). t1/t3 se
   conservan como atributo para QA y para una eventual revisión, no como
   sistema paralelo.

   ── FIJADO, NO PEGADO (05B · ola 1B) ──────────────────────────────────
   `position: sticky` falla en Safari iOS cuando el elemento es hijo directo
   de un `body { display:flex; flex-direction:column }` sin altura explícita:
   ese contenedor no crea un scroll container estable. `fixed` ancla la barra
   al viewport siempre, y el body compensa con `--fr-topbar-offset`.      */

.fr-topbar {
  --tb-rest: 0.975;   /* alfa de la capa en reposo                        */
  --tb-a:    0.84;    /* alfa de la capa desplazada — T2                  */
  --tb-blur: 20px;
  --tb-sat:  165%;
  --tb-dim:  0.52;    /* cuánto se apaga el fondo ANTES de componer       */

  position: fixed; top: 0; left: 0; right: 0; width: 100%;
  z-index: var(--fr-z-topbar);
  /* La altura la fija la fila de controles, no una constante: ver
     --fr-topbar-box en 01-tokens.css §1.16. Sin padding lateral — el 2 %
     de margen lo aportan el slot de marca y el bloque de acciones, que se
     posicionan contra la barra. */
  padding-block: var(--fr-topbar-pad);
  padding-inline: 0;
  display: flex; align-items: stretch;

  background: transparent;
  isolation: isolate;
  border-bottom: 1px solid rgba(var(--fr-red-rgb), 0.34);
  box-shadow:
    inset 0 1px 0 rgba(var(--fr-cream-rgb), 0.085),   /* luz de arista superior */
    inset 0 -1px 0 rgba(var(--fr-cream-rgb), 0.045),  /* canto óptico inferior  */
    0 1px 0 rgba(var(--fr-ink-rgb), 0.10);
  transition: box-shadow var(--fr-d-topbar) var(--fr-ease),
              border-color var(--fr-d-topbar) var(--fr-ease);
}
/* iOS con viewport-fit=cover no descuenta la barra de estado: la Dynamic
   Island se comería la fila de controles. El notch se suma UNA vez, aquí;
   el body compensa esa misma cantidad con --fr-topbar-offset. */
/* Sin `@supports`: el token de §1.15b ya lleva el respaldo `0px` dentro de
   `env()`, asi que en un navegador sin recorte la declaracion vale
   `calc(pad + 0px)` y no hay doble relleno. La envoltura solo servia para
   protegerse de una sintaxis que hoy entiende todo el parque. */
.fr-topbar { padding-top: calc(var(--fr-topbar-pad) + var(--fr-safe-top)); }
/* La fila de controles: es quien da altura a la barra y quien crea el
   contexto contra el que se posicionan el lockup y las acciones. */
.fr-topbar-inner {
  position: relative; z-index: 1;
  flex: 1 1 auto; min-width: 0;
  min-height: var(--fr-topbar-box);
}
/* La reserva del espacio que la barra fija deja de ocupar en el flujo. La
   emite `body.fr-shell`, que Jinja escribe para todo usuario autenticado:
   determinista, sin depender del soporte de `:has()` en el navegador. La
   página de acceso no la lleva y no reserva nada. */
body.fr-shell { padding-top: var(--fr-topbar-offset); }
/* El relleno vertical del envoltorio del shell pasa a salir del token
   (§1.17b) en vez de quedarse en el `py-3` de Bootstrap. En escritorio vale
   exactamente lo mismo —1 rem— y no cambia nada; en móvil se compacta a la
   mitad, y `.fr-page--app` sigue descontando la cifra correcta porque
   descuenta el mismo token, no un número copiado. */
body.fr-shell > main {
  padding-top: var(--fr-shell-pad-block);
  padding-bottom: var(--fr-shell-pad-block);
}

/* Las tres dosis varían UNA cosa: cuánta tinta hay delante. Con --tb-dim
   constante, el fondo que atraviesa es exactamente 1 − alfa, y la
   progresión es real: 8 % → 16 % → 26 %. */
.fr-topbar[data-fr-tb="t1"] { --tb-a: 0.92; --tb-blur: 14px; --tb-sat: 145%; }
.fr-topbar[data-fr-tb="t2"] { --tb-a: 0.84; --tb-blur: 20px; --tb-sat: 165%; }
.fr-topbar[data-fr-tb="t3"] { --tb-a: 0.74; --tb-blur: 28px; --tb-sat: 190%; }

/* Las dos capas de material. La media del degradado es Negro Forte exacto. */
.fr-topbar::before,
.fr-topbar::after {
  content: ""; position: absolute; inset: 0; z-index: 0;
  pointer-events: none;
  transition: opacity var(--fr-d-topbar) var(--fr-ease);
}
.fr-topbar::before {                        /* REPOSO — tinta, sin filtro  */
  background: linear-gradient(180deg,
    rgba(30, 15, 18, var(--tb-rest)) 0%,
    rgba(22,  8, 10, var(--tb-rest)) 58%,
    rgba(13,  5,  7, var(--tb-rest)) 100%);
  opacity: 1;
}
.fr-topbar::after {                         /* DESPLAZADA — vidrio tintado */
  background: linear-gradient(180deg,
    rgba(30, 15, 18, var(--tb-a)) 0%,
    rgba(22,  8, 10, var(--tb-a)) 58%,
    rgba(13,  5,  7, var(--tb-a)) 100%);
  opacity: 0;
}
.fr-topbar[data-fr-topbar="scrolled"]::before { opacity: 0; }
.fr-topbar[data-fr-topbar="scrolled"]::after {
  opacity: 1;
  -webkit-backdrop-filter: blur(var(--tb-blur)) saturate(var(--tb-sat)) brightness(var(--tb-dim));
  backdrop-filter: blur(var(--tb-blur)) saturate(var(--tb-sat)) brightness(var(--tb-dim));
}
.fr-topbar[data-fr-topbar="scrolled"] {
  border-bottom-color: rgba(var(--fr-red-rgb), 0.28);
  box-shadow:
    inset 0 1px 0 rgba(var(--fr-cream-rgb), 0.10),
    inset 0 -1px 0 rgba(var(--fr-cream-rgb), 0.06),
    0 1px 0 rgba(var(--fr-ink-rgb), 0.08),
    0 8px 18px -16px rgba(var(--fr-ink-rgb), 0.45);  /* caída corta, no sombra */
}
/* Con una capa modal abierta la barra vuelve a sólida: dos desenfoques
   superpuestos ensucian el lockup y no aportan profundidad. */
.fr-topbar[data-fr-topbar="over-layer"]::before { opacity: 1; }
.fr-topbar[data-fr-topbar="over-layer"]::after {
  opacity: 0; -webkit-backdrop-filter: none; backdrop-filter: none;
}

/* El contenido vive por encima de las dos capas. */
.fr-topbar > * { position: relative; z-index: 1; }

/* ── ANATOMÍA · HEADER-BRAND-01 + SHELL-UX-01 ──────────────────────────
   Las dos mitades se posicionan CONTRA la fila, no se reparten con flex:
   así el menú de usuario puede fijarse al viewport en móvil sin quedar
   anclado a un bloque derecho estrecho, y ninguna de las dos altera la
   altura de la barra al crecer.

   El lockup conserva la relación 8000:1071 del PNG corporativo: a 320 px
   ocupa 142×19, y progresa hasta 210×28 en escritorio. Nunca crea una
   altura de barra distinta de la que fija --fr-topbar-box.             */
.fr-topbar-brand-slot {
  position: absolute;
  left: max(2%, var(--fr-safe-left));
  top: 50%;
  z-index: var(--fr-z-topbar);
  width: calc(50% - 8px);
  transform: translateY(-50%);
}
.fr-topbar-brand {
  display: flex; align-items: center;
  /* `max-content` ciñe el área pulsable al logotipo: con `width: 100%` el
     enlace se estiraba hasta el borde de su slot aunque el dibujo fuese
     más estrecho, y a 430 px llegaba a tocar el botón de tema. */
  width: max-content; max-width: min(210px, 100%);
  min-height: var(--fr-topbar-target);
  margin: 0; padding: 0; line-height: 1;
}
.fr-topbar-brand-img {
  display: block;
  width: clamp(142px, 42vw, 210px);
  max-width: 100%; height: auto;
  object-fit: contain; object-position: left center;
}

/* El bloque derecho se alinea SIN transform: un `translateY` crearía un
   bloque de contención y devolvería el menú `position: fixed` al ancho del
   bloque, que es justo lo que se quiere evitar en móvil. */
.fr-topbar-actions {
  position: absolute;
  top: 0; bottom: 0;
  right: max(2%, var(--fr-safe-right));
  z-index: var(--fr-z-topbar);
  display: flex; align-items: center; gap: 8px;
  width: max-content; max-width: calc(50% - 8px);
}
.fr-topbar-nav {
  display: flex; align-items: center;
  flex: 1 1 auto; min-width: 0;
  margin: 0; padding: 0; list-style: none;
}
.fr-topbar-nav > li { min-width: 0; }

.fr-topbar-btn,
.fr-topbar-user-btn {
  box-sizing: border-box;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: var(--fr-topbar-target); min-height: var(--fr-topbar-target);
  border: 0; cursor: pointer;
  background: transparent;
  transition: background var(--fr-d-micro) var(--fr-ease), color var(--fr-d-micro) var(--fr-ease);
}
/* El radio va acotado a la barra a propósito: `a:focus-visible` de
   css/lg-tokens.css (0,1,1) impone 4 px a cualquier ancla enfocada, y un
   selector de una sola clase perdería contra él justo al recibir el foco. */
.fr-topbar .fr-topbar-btn,
.fr-topbar .fr-topbar-user-btn { border-radius: 8px; }
.fr-topbar-btn {
  gap: 6px; padding: 0 var(--fr-s4);
  color: var(--fr-text-inv-2);
  font: inherit; font-size: var(--fr-t-small); font-weight: var(--fr-fw-med);
}
.fr-topbar-btn:hover  { background: rgba(240, 238, 233, 0.10); color: var(--fr-cream); }
.fr-topbar-btn:active { background: rgba(240, 238, 233, 0.16); }
/* El glifo del control de tema se dibuja con una fuente de iconos hasta que
   la migración iconográfica lo sustituya por el símbolo del sprite. */
.fr-topbar-btn .bi { font-size: 1.1rem; line-height: 1; }

/* El control de usuario. El padding vertical de un `nav-link` es lo que le
   hacía sobresalir 2,2 px por encima del borde de la barra: su zona alta
   quedaba fuera del área pulsable real. La altura la da min-height.
   No declara tipografía: hereda la del documento, que es con la que el
   nombre está medido. El color y el peso los fija el apagado de
   css/user_levels.css en css/lg-forte-tokens.css §20. */
.fr-topbar-user-btn { min-width: 0; padding: 0 8px; white-space: nowrap; text-decoration: none; }
.fr-topbar-user-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.fr-topbar-user-btn::after,
.fr-topbar-user-btn > .bi { flex-shrink: 0; }
.fr-topbar-user-btn > .bi { margin-right: 4px; }

/* Anillo de foco del shell: Rojo Plastic sobre la placa de tinta. El anillo
   general del sistema es teal y sobre este fondo pierde contraste. No se
   toca `box-shadow`: el halo que cada pantalla ya aporta se conserva.

   EL `!important` NO ES NUEVO: es el mismo que el shell llevaba desde R1-4
   en css/lg-forte-tokens.css, y viaja aquí con el resto del componente. Lo
   necesita porque NUEVE hojas de módulo —index-dashboard, lg-atbd, lg-cebd,
   lg-despiece, lg-efbd, lg-extrusionsoplado, lg-funciones-soporte,
   lg-garrasbd y lg-maqbd-inventario— declaran un `*:focus-visible`
   con `!important` que alcanza al shell entero, y contra un `!important`
   ninguna especificidad gana.

   RETIRADA: cuando esas hojas acoten su foco a su propia pantalla al
   migrar. Hasta entonces, quitarlo aquí no arregla nada: sólo hace que el
   anillo de la barra cambie de color según en qué pantalla estés. */
.fr-topbar :focus-visible {
  outline: 2px solid var(--fr-red) !important;
  outline-offset: 2px;
}

@media (max-width: 575.98px) {
  /* El nombre completo llegaba a consumir 181 px de una barra de 393. Se
     oculta A LA VISTA, no al lector de pantalla: el control conserva su
     nombre accesible, el enlace su `title`, y el nombre sigue impreso en la
     cabecera del propio menú. Ninguna acción deja de estar disponible. */
  .fr-topbar-user-name {
    position: absolute; width: 1px; height: 1px;
    padding: 0; margin: -1px; border: 0;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }
  /* Sin texto a la derecha, la separación del icono sobra. */
  .fr-topbar-user-btn > .bi { margin-right: 0; }
}

/* SEGURIDAD ÓPTICA DE LA MARCA (PRODUCT_DERIVED).
   El asset oficial no se toca y NO se le pone caja: la legibilidad sobre
   un fondo que deja pasar contenido se resuelve con una sombra de 1 px que
   sigue el canal alfa del propio logotipo. Sin recuadro, sin halo, sin
   alterar su zona de protección. */
.fr-topbar-brand-img { transition: filter var(--fr-d-topbar) var(--fr-ease); }
.fr-topbar[data-fr-topbar="scrolled"] .fr-topbar-brand-img {
  filter: drop-shadow(0 1px 1.5px rgba(0, 0, 0, 0.55));
}
.fr-topbar[data-fr-tb="t3"][data-fr-topbar="scrolled"] .fr-topbar-brand-img {
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.68));
}

/* Sin backdrop-filter la barra NO se aligera: la legibilidad manda sobre
   el efecto y el fallback nunca puede quedar en gris. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .fr-topbar[data-fr-topbar="scrolled"]::after { --tb-a: 0.975; }
}
.fr-nobf .fr-topbar[data-fr-topbar="scrolled"]::after,
[data-fr-effects="reduced"] .fr-topbar[data-fr-topbar="scrolled"]::after {
  --tb-a: 0.975;
  -webkit-backdrop-filter: none; backdrop-filter: none;
}

/* El objetivo táctil de 44 px lo aporta --fr-topbar-target, que ya cambia
   con `pointer: coarse` en 01-tokens.css: los controles y el lockup no
   necesitan repetir el umbral. */
@media (max-width: 576px) {
  /* El pulgar tapa la barra y el fondo cambia con cada gesto: media dosis.
     La marca tiene que quedarse quieta aunque debajo pase de todo. */
  .fr-topbar,
  .fr-topbar[data-fr-tb="t2"] { --tb-a: 0.90; }
  .fr-topbar[data-fr-tb="t1"] { --tb-a: 0.95; }
  .fr-topbar[data-fr-tb="t3"] { --tb-a: 0.84; }
}

/* ══════════════════════════════════════════════════════════════════════
   4.2 · PÁGINA
   ══════════════════════════════════════════════════════════════════════ */
/* GUTTER Y SAFE AREA. El aire lateral sale de `--fr-page-gutter` (§1.17) y
   el recorte del dispositivo entra por `max()`, NUNCA sumado: en un monitor
   el inset vale 0 y sumarlo daria el doble de margen del decidido. En un
   telefono apaisado el inset lateral vale 59 px y ahi manda el.

   Por abajo, el recorte se SUMA a la reserva: la barra de gestos del
   telefono se come esa banda y el contenido tiene que terminar por encima. */
.fr-page {
  padding-top: calc(var(--fr-topbar-offset) + var(--fr-gap-stack));
  padding-left: max(var(--fr-page-gutter), var(--fr-safe-left));
  padding-right: max(var(--fr-page-gutter), var(--fr-safe-right));
  padding-bottom: calc(var(--fr-gap-section) + var(--fr-safe-bottom));
  max-width: var(--fr-page-max); margin: 0 auto;
}
.fr-page--flush { padding-top: var(--fr-topbar-offset); }

/* ── LIENZO DE MÓDULO · el recorte lateral deja de ser cosa de la pantalla
   ────────────────────────────────────────────────────────────────────────
   `.fr-page` resuelve el recorte por su cuenta y esa geometría está medida y
   validada. Lo que no lo estaba es la pantalla que compone su PROPIO lienzo
   dentro del shell: el Registro de Inyección, el kiosco de Indicadores,
   Calciny, Secuenciación y Turnos cuelgan de `main.container-fluid`, que es
   de Bootstrap y da 12 px fijos. En un teléfono APAISADO el recorte vale 59
   y esos 12 dejaban controles bajo la cámara: medido a 852×393, el buscador
   y dos botones del Registro de Inyección y una tarjeta de Indicadores.

   Seis pantallas, una causa. Rehacerlas para que usen `.fr-page` no es de
   esta fase —y cambiaría su composición, que es lo que el encargo prohíbe—,
   así que lo que hace el sistema es EXPONER la primitiva y que la consuman.

   `--modcanvas-pad` es el relleno que la pantalla ya tenía; la primitiva
   sólo garantiza el suelo del recorte. `max()`, NUNCA suma: en un monitor el
   inset vale 0, gana el relleno de diseño y no cambia un píxel.          */
.fr-modcanvas {
  padding-left:  max(var(--modcanvas-pad, 0px), var(--fr-safe-left));
  padding-right: max(var(--modcanvas-pad, 0px), var(--fr-safe-right));
}
/* Una pagina ANCHA es mas ancha que la estandar, no infinita. Medido antes de
   esta fase: a 3.840 px las seis familias con `--wide` servian una
   composicion de 3.720 px. Una tabla de 3.720 px no se lee: se recorre. */
.fr-page--wide  { max-width: var(--fr-wide-max); }

/* ── Página PÚBLICA · una tarjeta centrada, sin shell ────────────────────
   El sistema daba por hecho que toda pantalla vive bajo la barra: `.fr-page`
   reserva `--fr-topbar-offset` por arriba. Una pantalla que se sirve SIN
   autenticar —recuperar contraseña, restablecerla— no tiene barra ni pie, y
   con `.fr-page` aparecía un hueco de 44 px reservado para algo que no
   existe. Tampoco es un listado: es UNA tarjeta que se lee centrada.

   `margin: auto` sobre el hijo y no `justify-content: center`: cuando la
   tarjeta es más alta que la ventana —contraseña con cinco requisitos en un
   móvil apaisado— los márgenes automáticos se colapsan y el borde superior
   sigue siendo alcanzable. Con `center` el contenido desborda por los dos
   extremos y la parte de arriba queda fuera del scroll.

   Consumidores: auth/forgot_password.html. El día que `reset_password`
   vuelva a renderizar (ver F12_CONSUMER_EXCEPTIONS.csv, EX-01) es el
   segundo, y por eso vive aquí y no en la plantilla.                     */
.fr-page--public {
  padding: var(--fr-s8) max(var(--fr-page-gutter), var(--fr-safe-left));
  padding-right: max(var(--fr-page-gutter), var(--fr-safe-right));
  padding-bottom: calc(var(--fr-s8) + var(--fr-safe-bottom));
  min-height: 100vh;
  min-height: 100dvh;
  max-width: none;
  display: flex;
}
.fr-page--public > * {
  margin: auto;
  width: 100%;
  max-width: var(--fr-public-max, 460px);
  min-width: 0;
}
body.fr-shell .fr-page--public { padding-top: var(--fr-s8); }

/* Cabecera de una pantalla pública: marca, icono de la operación, título y
   explicación, centrados. El centrado pertenece a la primitiva —una página
   pública lo está por definición— y así no vuelve a escribirse como
   `style="text-align:center"` en cada plantilla. El formulario que va
   debajo NO se centra: los campos se leen alineados a la izquierda. */
.fr-public-head { text-align: center; margin-bottom: var(--fr-s7); }
.fr-public-head > * { margin-inline: auto; }
/* La tipografía se declara aquí porque un `<h1>` desnudo dentro de `.fr-app`
   se lo queda Bootstrap —`calc(1.375rem + 1.5vw)`— y el título de la
   tarjeta salía al doble de la escala del producto. */
.fr-public-head h1 {
  margin: 0; font-size: var(--fr-t-h2); font-weight: var(--fr-fw-bold);
  letter-spacing: -0.01em; line-height: var(--fr-lh-dense); color: var(--fr-text);
}
.fr-public-head h2 {
  margin: 0; font-size: var(--fr-t-h3); font-weight: var(--fr-fw-bold);
  line-height: var(--fr-lh-dense); color: var(--fr-text);
}
.fr-public-head p  { margin: var(--fr-s3) 0 0; }
.fr-public-head .fr-icon,
.fr-public-head .fr-ico-box,
.fr-public-head > svg { margin-bottom: var(--fr-s4); }

/* El shell autenticado ya reserva la barra en el `body` (§4.1). Cuando la
   página vive dentro de él, reservarla otra vez la duplica: medido, 44 px
   del body + 60 px de la página = 104 px de hueco donde el diseño pide 60.
   La página conserva su ritmo superior; la reserva la pone el shell, que es
   quien conoce la barra. */
body.fr-shell .fr-page        { padding-top: var(--fr-s6); }
body.fr-shell .fr-page--flush { padding-top: 0; }

/* ── Página de aplicación ────────────────────────────────────────────────
   La cabecera se queda quieta y sólo el cuerpo scrollea. Es la forma de las
   pantallas de listado de FORTEONE: 18 CRUD la usan desde F-J-01, y hasta
   ahora la resolvía `.lg-crud-shell` en el CSS de módulo. El idioma ya
   existía dentro del sistema —`.fr-modal-body`, `.fr-drawer-body` y
   `.fr-chooser-body` son el mismo `flex:1 1 0; min-height:0; overflow`—;
   lo que faltaba era exponerlo a nivel de página.

   `window.scrollY` vale 0 en estas pantallas: quien scrollea es el cuerpo.
   `FR.topbar.watch()` ya lo contempla escuchando en fase de captura, y una
   pantalla puede además declararlo con `data-fr-scroller`.               */
.fr-page--app {
  height: calc(100vh - var(--fr-topbar-offset) - var(--fr-page-foot));
  height: calc(100dvh - var(--fr-topbar-offset) - var(--fr-page-foot));
  display: flex; flex-direction: column;
  /* `auto`, no `hidden`: es la red de seguridad que el contenedor legacy ya
     había aprendido (F-K-01). Si el cálculo se desajusta en algún módulo, el
     contenido aparece con scroll en vez de recortarse sin remedio. */
  overflow-y: auto; overflow-x: hidden;
  padding-bottom: 0;
}
/* El envoltorio del shell añade su relleno vertical arriba y abajo. La
   página de aplicación calcula su alto sobre el hueco REAL, así que ese
   relleno se descuenta: sin esto el documento pide 32 px más de los que hay
   y aparece un scroll exterior por encima del scroll del cuerpo. El
   contenedor legacy lo resolvía tirando hacia arriba con `margin-top: -1rem`;
   descontarlo del alto deja el borde inferior en el mismo sitio sin recurrir
   a un margen negativo.

   Sale del TOKEN, no del `2rem` escrito a mano que había aquí: en móvil el
   envoltorio se compacta a la mitad (§1.17b) y una constante copiada dejaría
   la página 16 px más corta de lo que hay, con banda muerta al final. */
body.fr-shell .fr-page--app {
  height: calc(100vh - var(--fr-topbar-offset) - var(--fr-page-foot)
               - var(--fr-shell-pad-block) * 2);
  height: calc(100dvh - var(--fr-topbar-offset) - var(--fr-page-foot)
               - var(--fr-shell-pad-block) * 2);
}
@media (max-width: 768px) {
  /* El zócalo del sistema se descuenta aquí porque el body sólo compensa la
     inserción inferior cuando el pie sigue fijo, y en táctil ya no lo está. */
  .fr-page--app,
  body.fr-shell .fr-page--app {
    height: calc(100dvh - var(--fr-topbar-offset) - var(--fr-page-foot)
                 - var(--fr-shell-pad-block) * 2 - var(--fr-safe-bottom));
  }
}
/* El único hijo que scrollea. Sin `min-height: 0` un hijo flexible se niega
   a encogerse por debajo de su contenido y el scroll se va a la ventana. */
.fr-page-body {
  flex: 1 1 0; min-height: 0;
  overflow-y: auto; overflow-x: hidden;
  overscroll-behavior: contain;
  padding-bottom: var(--fr-s7);
}
/* Lo que acompaña al cuerpo dentro de la página de aplicación no se encoge:
   cabecera, franja de filtros y paginación conservan su alto. */
.fr-page--app > :not(.fr-page-body) { flex: 0 0 auto; }

/* Shell de pantalla completa: el pie identitario se apoya en el fondo de
   la ventana aunque el contenido sea corto. */
.fr-app--shell { min-height: 100vh; min-height: 100dvh; display: flex; flex-direction: column; }
/* Un contenedor centrado con `margin: 0 auto` dentro de un flex en columna
   pierde el estirado del eje transversal y se dimensiona por su contenido:
   en móvil eso convierte cualquier pieza ancha en scroll horizontal de
   TODA la página. width:100% lo devuelve al ancho del contenedor sin tocar
   el max-width ni el centrado en pantallas grandes. */
.fr-app--shell > * { width: 100%; min-width: 0; }
.fr-app--shell > .fr-page,
.fr-app--shell > main { flex: 1 1 auto; }

/* ── Pantalla del producto SIN barra superior (05B · ola 11) ─────────────
   El sistema daba por hecho que toda pantalla interna vive bajo la topbar:
   `.fr-page` reserva `--fr-topbar-offset` por arriba, el stack pegajoso
   cuelga de ahí y la reserva del pie fijo la emite `body.fr-shell`.

   Hay pantallas que son STANDALONE —no heredan de base.html, así que no
   reciben ni la barra ni la clase de shell— y aun así SON la aplicación:
   las tres de Nuevos Moldes. Antes de esto la única forma de decirlo era
   redefinir `--fr-topbar-h` en la hoja del módulo, que es justo lo que
   prohíbe el gate de tokens y con razón: la altura de la barra es del
   sistema, no de una pantalla.

   `--nobar` lo declara UNA vez y todo lo derivado cae solo en su sitio: el
   relleno superior de la página, el `top` de las barras pegajosas, la
   cabecera pegajosa de las tablas y la reserva del pie. El notch se sigue
   sumando porque vive en `--fr-topbar-offset`, no en la altura.

   NO se confunde con `.fr-page--public`, que sirve a las pantallas sin
   autenticar: aquéllas no llevan pie y son UNA tarjeta centrada; ésta es
   una pantalla interna completa que sí lo lleva. */
.fr-app--nobar { --fr-topbar-h: 0px; }
body.fr-app--nobar {
  padding-bottom: calc(var(--fr-footer-h) + var(--fr-safe-bottom));
}
@media (max-width: 991.98px), (pointer: coarse) {
  /* Mismo umbral que libera --fr-footer-h y devuelve el pie al flujo: la
     reserva sobra y dejaría un hueco muerto. */
  body.fr-app--nobar { padding-bottom: 0; }
}

/* ══════════════════════════════════════════════════════════════════════
   4.3 · STICKY STACK
   ══════════════════════════════════════════════════════════════════════
   Tres niveles y ni uno más. Todos derivan de --fr-sticky-top, que a su
   vez deriva de la altura real de la topbar y de la toolbar. Cambiar la
   altura de la barra reposiciona el stack entero sin tocar módulos.

     L1 topbar   fija, --fr-z-topbar
     L2 toolbar  sticky bajo la topbar, --fr-z-toolbar
     L3 dock     sticky al zócalo, --fr-z-dock
     thead       sticky bajo el stack, --fr-z-thead                       */

.fr-toolbar {
  position: sticky; top: var(--fr-topbar-offset); z-index: var(--fr-z-toolbar);
  display: flex; align-items: center; gap: var(--fr-s4); flex-wrap: wrap;
  padding: var(--fr-s4) var(--fr-s5);
  background: var(--fr-work);
  border: 1px solid var(--fr-line-1);
  border-radius: var(--fr-r-card);
  box-shadow: var(--fr-sh-contact);
}
.fr-toolbar-spacer { flex: 1 1 auto; }
/* ── EL RECUENTO VIVE EN LA TOOLBAR ──────────────────────────────────────
   VU-02. Un maestro contesta al buscador con cuántos registros quedan
   («2467 moldes», «65 máquinas de 155»): el estado se lee al lado de lo
   que lo produce, no en la cabecera ni en un chip junto al título. Es el
   patrón de Moldes, promovido a sistema para que lo consuma la familia.  */
.fr-toolbar-status {
  margin-left: auto; font-size: var(--fr-t-mini); color: var(--fr-text-3);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.fr-toolbar-status strong { color: var(--fr-text); font-weight: var(--fr-fw-bold); }
.fr-toolbar-status.is-filtered { color: var(--fr-teal); font-weight: var(--fr-fw-bold); }
/* Segunda línea de una franja de mando: filtros, chips, selects. Ocupa el
   ancho entero y envuelve; en la franja estrecha del sistema (≤700 px)
   hereda la tira horizontal, como el resto de piezas.                    */
.fr-toolbar-row {
  flex: 1 1 100%; min-width: 0;
  display: flex; align-items: center; gap: var(--fr-s3); flex-wrap: wrap;
}

/* ── DENTRO DE UN SCROLLER, LA REFERENCIA ES EL SCROLLER ─────────────────
   `--fr-topbar-offset` describe el STACK DE LA VENTANA: la barra fija más el
   recorte del dispositivo. Dentro de `.fr-page--app` ese hueco NO existe —el
   scrollport empieza en el borde de la caja, ya por debajo de la barra— y el
   navegador aplica el desplazamiento igual: la franja se coloca esa misma
   cantidad POR DEBAJO de su sitio y tapa lo que viene detrás.

   Es el mismo defecto que el sistema ya corrigió para `thead` dentro de
   `.fr-table-scroller` (05-components §5.4), y lleva vivo en las veinte
   pantallas CRUD desde entonces: 50 px de desplazamiento con ratón, que no
   llegaban a solaparse por poco. Con el recorte de un teléfono son 109 y sí:
   medido en el catálogo técnico a 768×1024 y 820×1180, la franja se montaba
   5 px sobre el primer control de la migas. Lo destapó simular el notch, no
   mirar la pantalla.                                                       */
.fr-page--app .fr-toolbar,
.fr-page--app .fr-sticky-top { top: 0; }
/* Y la CABECERA DE TABLA es el tercer caso, por la misma razón exacta. Se
   corrigió para el scroller horizontal (05-components §5.4) y para las dos
   franjas de arriba, pero una tabla servida directamente dentro del cuerpo
   de una página de aplicación seguía llevando el offset de la ventana: el
   scrollport es `.fr-page-body`, que empieza ya por debajo de la barra.
   Aquí no hay ninguna hoy —el inventario las sirve todas con scroller— y
   por eso no aparece en la medición; se escribe para que la siguiente que
   se escriba nazca bien, que es de lo que trata tener un sistema.       */
.fr-page--app .fr-table th { top: 0; }

/* Primitiva genérica: cualquier barra que deba pegarse bajo el stack. */
.fr-sticky-top { position: sticky; top: var(--fr-sticky-top); z-index: var(--fr-z-toolbar); }
/* Cualquier destino de scroll (ancla, campo con error) debe respetar el
   stack por arriba y el dock por abajo. */
.fr-scroll-target {
  scroll-margin-block-start: calc(var(--fr-sticky-top) + 12px);
  scroll-margin-block-end: calc(var(--fr-dock-h) + var(--fr-safe-bottom) + 12px);
}

/* ── EL SCROLLPORT SABE LO QUE TIENE ENCIMA ─────────────────────────────
   `.fr-scroll-target` es de ADHESIÓN: sólo protege a quien se apunta. Y el
   navegador desplaza por su cuenta muchas más veces de las que una clase
   puede cubrir —`scrollIntoView()`, `focus()`, un `#ancla`, Tab, la
   navegación por teclado de una tabla, el salto a un campo con error— y en
   todas ellas deja el destino pegado al borde del scrollport, que aquí es
   justo donde está la barra fija.

   `scroll-padding` lo resuelve del lado correcto: lo declara el CONTENEDOR,
   una vez, y vale para cualquier desplazamiento que ocurra dentro. Es la
   mitad que faltaba de la pareja `scroll-margin` / `scroll-padding`.

   Dos scrollports y dos referencias distintas, que es la regla de §4.3: la
   VENTANA descuenta la barra fija más la franja de mando; el CUERPO de una
   página de aplicación empieza ya por debajo de la barra y sólo descuenta lo
   que se pega dentro de él.

   No cambia un píxel de la composición: `scroll-padding` no ocupa espacio,
   sólo mueve el punto donde se detiene un desplazamiento.               */
html {
  scroll-padding-block-start: calc(var(--fr-sticky-top) + 12px);
  scroll-padding-block-end: calc(var(--fr-dock-h) + var(--fr-footer-h)
                                 + var(--fr-safe-bottom) + 12px);
}
.fr-page-body,
.fr-modal-body,
.fr-drawer-body {
  scroll-padding-block-start: calc(var(--fr-toolbar-h) + 12px);
  scroll-padding-block-end: calc(var(--fr-dock-h) + var(--fr-safe-bottom) + 12px);
}
/* Dentro de un scroller de tabla la cabecera se pega en `top: 0`, así que lo
   que hay que descontar es su alto REAL. No hay token para eso —depende de
   la densidad de cada tabla— y por eso se publica desde el componente. */
.fr-table-scroller,
.fr-table-wrap { scroll-padding-block-start: var(--fr-thead-h, 34px); }

/* Action dock inferior (L3) */
.fr-dock {
  position: sticky; bottom: 0; z-index: var(--fr-z-dock);
  display: flex; align-items: center; justify-content: flex-end; gap: var(--fr-s4);
  padding: var(--fr-s4) var(--fr-s6);
  padding-bottom: calc(var(--fr-s4) + var(--fr-safe-bottom));
  margin-block: var(--fr-s6) 0;
  margin-inline: calc(-1 * max(var(--fr-page-gutter), var(--fr-safe-left)))
                 calc(-1 * max(var(--fr-page-gutter), var(--fr-safe-right)));
  border-top: 1px solid rgba(240, 238, 233, 0.10);
}
.fr-dock .fr-dock-meta { margin-right: auto; color: var(--fr-text-inv-2); font-size: var(--fr-t-mini); }
/* Dock anclado a la ventana: para formularios largos cuyo dock no vive
   dentro del contenedor con scroll. El alto real lo publica forte-ui.js en
   --fr-dock-h para que scroll-margin y padding lo respeten.

   F07_DEBT_01 — el dock se apoya SOBRE el pie identitario, no debajo. Con
   `bottom: 0` quedaba tapado: el pie fijo vive en --fr-z-footer (1000), muy
   por encima de --fr-z-dock, y cada pantalla con dock fijo acababa sumando a
   mano los 28 px del pie en sus paddings. El offset es del sistema y sale
   del mismo token que posiciona el pie, así que en cuanto el pie vuelve al
   flujo (<992px o táctil) --fr-footer-h vale 0 y el dock baja solo. */
.fr-dock--fixed {
  position: fixed; left: 0; right: 0; margin: 0;
  bottom: var(--fr-footer-h, 0px);
  z-index: var(--fr-z-dock);
}
.fr-app--dock {
  padding-bottom: calc(var(--fr-dock-h, 56px) + var(--fr-footer-h, 0px) + var(--fr-safe-bottom));
}
/* Un dock FIJO va de borde a borde de la ventana, así que los cuatro lados
   miran al recorte: abajo el indicador de inicio, a los costados la cámara
   de un teléfono apaisado. Con `max()`, que en un monitor no cambia nada. */
.fr-dock--fixed {
  padding-left: max(var(--fr-s6), var(--fr-safe-left));
  padding-right: max(var(--fr-s6), var(--fr-safe-right));
}
@media (max-width: 700px) {
  /* Por lados, NO abreviado: `padding: a b` repone también el inferior y se
     llevaba por delante la reserva de la barra de gestos que `.fr-dock`
     declara arriba, dejando los botones del dock justo debajo del indicador
     de inicio. Mismo defecto de abreviado que el marco del modal en
     07-responsive. FORTE_IOS_IPADOS_HARDENING_02. */
  .fr-dock--fixed {
    flex-wrap: wrap; gap: var(--fr-s3);
    padding-top: var(--fr-s3);
    padding-left: max(var(--fr-s5), var(--fr-safe-left));
    padding-right: max(var(--fr-s5), var(--fr-safe-right));
    padding-bottom: calc(var(--fr-s3) + var(--fr-safe-bottom));
  }
  .fr-dock--fixed .fr-dock-meta { width: 100%; margin-right: 0; }
  .fr-dock--fixed .fr-btn { flex: 1 1 auto; }
}

/* ══════════════════════════════════════════════════════════════════════
   4.4 · FOOTER IDENTITARIO (OD-2 · A — zócalo táctil liberado)
   ══════════════════════════════════════════════════════════════════════
   El footer va EN FLUJO por defecto. En escritorio puede fijarse con
   `.fr-footer--fixed` (franja fina de 24 px). En móvil y tablet nunca se
   fija: el zócalo queda para el action dock. Eso recupera 24 px
   permanentes y elimina el acoplamiento «toda barra debe conocer la altura
   del footer», que ya mordió en PARAMREG.

   NOTA DE MARCADO: este componente se escribe con `div`, no con `<footer>`.
   El runtime tenía un selector de elemento `footer{position:fixed}` global
   en css/lg-shell.css que arrancaba de su contenedor a cualquier `<footer>`
   anidado. Ese selector se ha RETIRADO con la migración del shell (05B ·
   ola 1B); la regla de marcado se mantiene porque la propiedad que protege
   —que un componente no dependa del nombre de un elemento global— sigue
   valiendo. Ver GOVERNANCE.md.                                          */
.fr-footer {
  display: flex; align-items: center; justify-content: space-between; gap: 3px;
  width: 100%;
  padding: 4px 3% 0;
  padding-bottom: var(--fr-safe-bottom);
  background: rgba(var(--fr-ink-rgb), 0.88);
  border-top: 0.5px solid rgba(var(--fr-red-rgb), 0.28);
  -webkit-backdrop-filter: blur(12px) saturate(140%);
  backdrop-filter: blur(12px) saturate(140%);
}
/* Sin desenfoque la legibilidad manda: el pie sube a sólido. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .fr-footer { background: rgba(var(--fr-ink-rgb), 0.98); }
}
.fr-nobf .fr-footer,
[data-fr-effects="reduced"] .fr-footer {
  -webkit-backdrop-filter: none; backdrop-filter: none;
  background: rgba(var(--fr-ink-rgb), 0.98);
}
[data-theme="dark"] .fr-footer  { background: rgba(13, 3, 4, 0.92); border-top-color: rgba(var(--fr-red-rgb), 0.35); }
[data-theme="forte"] .fr-footer { background: rgba(10, 2, 2, 0.94); border-top-color: rgba(var(--fr-red-rgb), 0.40); }

/* Tipografía del pie. Los tres niveles de voz que ya tenía: identidad,
   coletilla y aviso legal. El blanco de la primera línea es el valor que el
   pie lleva desde LG y se conserva verbatim: alinearlo con Blanco Forte es
   una decisión de marca, no un efecto colateral de mover el componente. */
.fr-footer-line { font-size: 0.70rem; font-weight: 700; color: rgba(255, 255, 255, 0.92); }
.fr-footer-brand { color: rgba(255, 255, 255, 0.92); }
.fr-footer-tagline { color: rgba(var(--fr-cream-rgb), 0.72); }
.fr-footer-secondary { font-size: 0.62rem; color: rgba(var(--fr-cream-rgb), 0.50); }
.fr-footer strong { color: var(--fr-cream); font-weight: var(--fr-fw-bold); letter-spacing: 0.02em; }
[data-theme="forte"] .fr-footer-line,
[data-theme="forte"] .fr-footer-brand { color: rgba(var(--fr-cream-rgb), 0.85); }
[data-theme="forte"] .fr-footer-tagline { color: rgba(var(--fr-cream-rgb), 0.50); }

/* FIJADO: sólo con ratón y con sitio de sobra. El umbral es el mismo que
   libera --fr-footer-h en 01-tokens.css, así que la reserva del body y la
   posición del pie no pueden desincronizarse. */
@media (min-width: 992px) and (pointer: fine) {
  .fr-footer--fixed {
    position: fixed; bottom: 0; left: 0;
    width: 100%;
    height: calc(var(--fr-footer-h) + var(--fr-safe-bottom));
    z-index: var(--fr-z-footer);
  }
}
/* En flujo el pie respira un poco más y absorbe él mismo el zócalo del
   sistema operativo (OD-2 · A: se recuperan 24 px permanentes). */
@media (max-width: 991.98px), (pointer: coarse) {
  .fr-footer {
    padding-top: 6px;
    padding-bottom: max(6px, var(--fr-safe-bottom));
  }
}
@media (max-width: 768px) {
  /* La coletilla no cabe en una barra estrecha sin provocar desbordamiento. */
  .fr-footer-tagline { display: none; }
}
/* Reserva del pie fijo. Va en el mismo ámbito que la de la barra: el pie
   sólo existe para el usuario autenticado. */
body.fr-shell { padding-bottom: calc(var(--fr-footer-h) + var(--fr-safe-bottom)); }
@media (max-width: 991.98px), (pointer: coarse) {
  /* El pie vuelve al flujo: la reserva sobra y dejaría un hueco muerto. */
  body.fr-shell { padding-bottom: 0; }
}

/* ══════════════════════════════════════════════════════════════════════
   4.5 · REJILLAS Y BLOQUES DE CONTENIDO
   ══════════════════════════════════════════════════════════════════════ */
.fr-hero { display: flex; align-items: flex-end; gap: var(--fr-s6); flex-wrap: wrap; margin-bottom: var(--fr-s6); }
.fr-hero-block { flex: 1 1 auto; min-width: 0; }
/* VU-02 · El icono de dominio se centra en la ALTURA del bloque de título:
   con kicker, título y meta (tres líneas) `align-items: flex-end` lo dejaba
   colgando a la altura de la meta, lejos del título que identifica. */
.fr-hero > .fr-ico-box { align-self: center; }
.fr-hero-kicker { font-size: var(--fr-t-micro); font-weight: var(--fr-fw-bold); letter-spacing: var(--fr-ls-wide); text-transform: uppercase; color: var(--fr-text-3); }
.fr-hero h1 { margin: 2px 0 0; font-size: var(--fr-t-h2); font-weight: var(--fr-fw-bold); letter-spacing: -0.01em; }
.fr-hero .fr-hero-meta { margin-top: 3px; font-size: var(--fr-t-mini); color: var(--fr-text-2); }
.fr-hero-actions { display: flex; gap: var(--fr-s4); align-items: center; flex-wrap: wrap; }
/* ── EL KICKER ES NAVEGACIÓN ─────────────────────────────────────────────
   VU-02. La línea superior de la cabecera dice DÓNDE está el usuario
   («Equipos de Producción · Máquinas de Inyección → Robots») y cada tramo
   que tiene destino es un enlace: es la vuelta contextual, visible en todos
   los regímenes, y sustituye al botón «Volver al Dashboard» que las
   cabeceras de CRUD arrastraban. Hereda color y tamaño del kicker; el
   separador y el tramo actual sólo cambian de tinta.                     */
.fr-hero-kicker a { color: inherit; text-decoration: none; }
.fr-hero-kicker a:hover { color: var(--fr-link); text-decoration: underline; }
.fr-hero-kicker .sep { margin: 0 0.35em; color: var(--fr-text-4); font-weight: var(--fr-fw-med); }
.fr-hero-kicker .current { color: var(--fr-text-2); }
/* Intercambio de acciones de la cabecera. En pantalla estrecha tres botones
   con texto no caben en una línea, y envolverlos come dos filas del listado
   —que es lo que se ha venido a mirar—. La pantalla estrecha ofrece un solo
   disparador que abre las MISMAS acciones. Sustituye a
   `.lg-actions-desktop` / `.lg-actions-mobile`, que hacían esto mismo en el
   CSS de módulo con un umbral propio de 768 px; aquí el umbral es el de
   composición del sistema (RESPONSIVE.md). */
.fr-hero-actions--mobile { display: none; }

.fr-section { margin: 0 0 var(--fr-gap-section); }
.fr-section > .hd {
  display: flex; align-items: baseline; gap: var(--fr-s4);
  padding-bottom: var(--fr-s3); margin-bottom: var(--fr-s5);
  border-bottom: 1px solid var(--fr-line-2);
}
.fr-section > .hd h2,
.fr-section > .hd h3 {
  margin: 0; font-size: var(--fr-t-small); font-weight: var(--fr-fw-bold);
  letter-spacing: var(--fr-ls-wide); text-transform: uppercase; color: var(--fr-text-2);
}
.fr-section > .hd .meta { font-size: var(--fr-t-micro); color: var(--fr-text-3); }

/* `clip` y no `hidden`, por la misma razón que en `.fr-table-wrap`: el panel
   recorta su radio pero NO debe convertirse en contenedor de scroll. Con
   `hidden` se quedaba la referencia de cualquier `position: sticky` de
   dentro, y ahí vive la mitad de las tablas del producto. Medido tras
   corregir el envoltorio: la cabecera de `Gestión de usuarios` seguía
   tapando 22 px de la primera fila, ahora colgando del panel en vez del
   envoltorio. Un defecto en dos capas se corrige en las dos. */
.fr-panel { border: 1px solid var(--fr-line-2); border-radius: var(--fr-r-card); background: var(--fr-raised); overflow: clip; }
@supports not (overflow: clip) {
  .fr-panel { overflow: hidden; }
}
.fr-panel > .hd { display: flex; align-items: center; gap: var(--fr-s4); padding: var(--fr-s4) var(--fr-s5); border-bottom: 1px solid var(--fr-line-1); }
.fr-panel > .hd h2 { margin: 0; flex: 1 1 auto; font-size: var(--fr-t-micro); font-weight: var(--fr-fw-bold); letter-spacing: var(--fr-ls-wide); text-transform: uppercase; color: var(--fr-text-3); }
/* `flex-wrap` + `min-width: 0` en TODOS los hijos: una fila de dato lleva
   una etiqueta y un valor, y el valor puede ser un correo de 30 caracteres.
   Sin esto la etiqueta se encogía a cero y su texto salía por encima del
   valor —el panel lo recortaba con su `overflow: hidden`, así que el
   documento no desbordaba y ninguna métrica lo delataba—. Con el ajuste,
   cuando no cabe, el valor baja a la línea siguiente. Medido en
   `auth/profile.html` a 393 px (05B · ola 6). */
.fr-panel-row { display: flex; align-items: center; gap: var(--fr-s4); flex-wrap: wrap; overflow-wrap: anywhere; padding: var(--fr-s4) var(--fr-s5); border-bottom: 1px solid var(--fr-line-1); font-size: var(--fr-t-small); }
.fr-panel-row:last-child { border-bottom: 0; }
.fr-panel-row > * { min-width: 0; }
.fr-panel-row .g { flex: 1 1 auto; min-width: 0; }
/* Un enlace dentro de una fila de dato ES el objetivo de esa fila —el correo
   del usuario, el registro relacionado— y mide lo que mide su línea de
   texto: 20 px. Es el mismo hueco que la ola 4 resolvió en la tabla con
   `.fr-cell-link`, y el mismo tratamiento que `.fr-crumbs a` ya recibe.
   Con ratón la geometría no cambia. */
@media (pointer: coarse) {
  .fr-panel-row a { display: inline-flex; align-items: center; min-height: var(--fr-touch-min); }
}

/* Rejilla de tarjetas / módulos: auto-fit, sin breakpoints propios. */
/* ── LA REJILLA · tres correcciones, ninguna cosmetica ────────────────
   `auto-fill` RESERVA las pistas que caben aunque no haya hijos que las
   ocupen. Medido a 1.920 px: `.fr-grid--wide` declaraba CINCO columnas de
   307 px para UNA sola tarjeta —1.277 px de hueco reservado y una tarjeta
   encogida—. En F17 y F18 pasaba en todos los viewports por encima de 1.180.
   `auto-fit` colapsa las pistas vacias a cero y el contenido ocupa lo suyo.

   `min(..., 100%)` en el minimo: sin el, una rejilla de minimo 300 px dentro
   de un contenedor de 260 desborda la PAGINA entera. Es el defecto clasico
   de `minmax` en movil y es la causa de la mitad de los scrolls horizontales
   accidentales que se ven en cualquier sistema.

   `--fr-grid-cap` es el techo del HIJO, no de la pista, y la diferencia
   importa. Con `auto-fit` y `1fr`, una rejilla de UNA tarjeta la estira hasta
   el borde: la pancarta de 1.584 px, que es el defecto contrario al hueco
   fantasma y no mejor. La primera correccion puso el techo en la PISTA, y
   entonces una rejilla de diez tarjetas se quedaba en dos columnas de 560
   dejando 460 px muertos a la derecha —medido en F17 a 1.920—. El techo tiene
   que limitar a la tarjeta, no al reparto: con hijos de sobra las pistas
   reparten y llenan, y con un hijo suelto la tarjeta conserva su anchura util
   y se alinea al principio.                                              */
.fr-grid {
  display: grid;
  gap: var(--fr-gap-grid);
  grid-template-columns: repeat(auto-fit,
      minmax(min(var(--fr-grid-min, 232px), 100%), 1fr));
}
/* Va aqui, en la PRIMERA declaracion del sistema que toca a un hijo de
   rejilla, para que cualquier `max-width` posterior —el de `.fr-modcard`, el
   de una hoja de modulo— gane por orden de cascada sin necesitar peso. */
.fr-grid > * { max-width: var(--fr-grid-cap, none); }
.fr-grid--wide { --fr-grid-min: 300px; --fr-grid-cap: 560px; }
.fr-grid--tight { --fr-grid-min: 172px; gap: var(--fr-s4); }

/* ===== src/05-components.css ===== */
/* ════════════════════════════════════════════════════════════════════════
   FORTE UI · 05 · COMPONENTES
   ════════════════════════════════════════════════════════════════════════
   La familia única de controles del producto. Si un módulo necesita un
   botón, un campo, una tabla, un chip o un estado, LO CONSUME DE AQUÍ.

   NEVER SOLVE A REUSABLE VISUAL PROBLEM LOCALLY: si falta una variante,
   se añade a este fichero, se testea, se documenta y se consume. Nunca se
   redefine en el CSS del módulo.
   ════════════════════════════════════════════════════════════════════════ */

/* ══════════════════════════════════════════════════════════════════════
   5.1 · ICONOGRAFÍA (OD-R3-1 · A — Material Symbols)
   ══════════════════════════════════════════════════════════════════════
   El sistema ELIGE una familia profesional; no dibuja la suya. El manual
   corporativo no define iconografía, y de ahí no se sigue que haya que
   inventarla.

   IMPLEMENTACIÓN: sprite SVG local `icons/forte-icons.svg` con los glifos
   ORIGINALES de Material Symbols (Google · Apache-2.0 · outlined w300) más
   tres símbolos industriales propios —machine, mold, hotrunner— que
   ninguna librería comercial dibuja. Se sirve como sprite y no como
   webfont por tres razones medidas: el producto usa ~40 conceptos (la
   fuente variable completa son 3.600 glifos), el repositorio vendoriza
   todo su estático (STATIC-VENDOR-01) y un sprite no tiene FOUT ni
   depende de un CDN externo. Contrapartida aceptada y documentada: los
   ejes variables (FILL/wght/opsz) quedan congelados en el corte outlined
   w300; el peso óptico se gobierna con la escala de tamaños de abajo.

   `.fr-icon` es sólo el portador: los glifos son PATH RELLENO, así que
   aquí se gobierna tamaño, alineación y color heredado. El dibujo no se
   toca.

   MÉTRICA — el tamaño lo elige la línea de texto, nunca el hueco:
     texto 11-12 px → 14-16 · texto 13-14 px → 18-20
     título 18-22 px → 24    · identidad de módulo → 28-32                */
.fr-icon {
  --fr-icon-size: 20px;
  width: var(--fr-icon-size); height: var(--fr-icon-size);
  display: inline-block; flex: 0 0 auto;
  vertical-align: -0.18em;
  fill: currentColor; stroke: none;
  color: inherit; pointer-events: none;
}
.fr-icon--14 { --fr-icon-size: 14px; }
.fr-icon--16 { --fr-icon-size: 16px; }
.fr-icon--18 { --fr-icon-size: 18px; }
.fr-icon--20 { --fr-icon-size: 20px; }
.fr-icon--24 { --fr-icon-size: 24px; }
.fr-icon--28 { --fr-icon-size: 28px; }
.fr-icon--32 { --fr-icon-size: 32px; }
.fr-icon--40 { --fr-icon-size: 40px; }
/* El icono nunca elige su propio color: lo hereda o lo recibe. */
.fr-icon--muted  { color: var(--fr-text-3); }
.fr-icon--accent { color: var(--fr-teal); }
.fr-icon--ok     { color: var(--fr-ok); }
.fr-icon--warn   { color: var(--fr-warn); }
.fr-icon--danger { color: var(--fr-danger); }
.fr-icon--brand  { color: var(--fr-red); }
:disabled .fr-icon,
[aria-disabled="true"] .fr-icon,
.is-disabled .fr-icon { opacity: 0.45; }
/* Etiqueta con icono: manda la línea base del texto, no la caja del glifo. */
.fr-icon-label { display: inline-flex; align-items: center; gap: 7px; min-width: 0; }
.fr-icon-label > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

/* Botón de icono: el objetivo táctil es del botón, nunca del glifo.
   `flex: 0 0 auto` NO es decorativo. Un `.fr-iconbtn` dentro de un
   contenedor flex —una fila de fichero, una barra de acciones, una celda—
   hereda `flex-shrink: 1`, así que su `width` es sólo una BASE: en cuanto
   el hermano de al lado lleva un texto largo, el botón se comprime. Medido
   en la fila de ficheros de F18 a 393 px: el mismo botón que declara 44×44
   se quedaba en 18×44 cuando el nombre del CSV ocupaba la fila (05B · ola
   10). El alto cumplía, el ancho no, y ninguna métrica de desbordamiento lo
   veía porque nada desbordaba: simplemente el objetivo táctil dejaba de
   existir. Un botón de icono no negocia su tamaño con el texto de al lado. */
.fr-iconbtn {
  display: inline-flex; align-items: center; justify-content: center;
  flex: 0 0 auto;
  width: 30px; height: 30px; padding: 0;
  border: 1px solid transparent; border-radius: var(--fr-r-ctl);
  background: transparent; color: var(--fr-text-3); cursor: pointer;
  transition: background var(--fr-d-micro) var(--fr-ease),
              color var(--fr-d-micro) var(--fr-ease),
              border-color var(--fr-d-micro) var(--fr-ease);
}
.fr-iconbtn:hover  { background: var(--fr-hover); color: var(--fr-text); }
.fr-iconbtn:active { background: var(--fr-active); transform: translateY(0.5px); }
.fr-iconbtn--danger:hover { background: var(--fr-danger-soft); color: var(--fr-danger); }
.fr-iconbtn--on-dark { color: var(--fr-text-inv-2); }
.fr-iconbtn--on-dark:hover { background: rgba(240, 238, 233, 0.12); color: var(--fr-cream); }
@media (pointer: coarse) { .fr-iconbtn { width: var(--fr-touch-min); height: var(--fr-touch-min); } }

/* Contenedor de icono — EXCEPCIÓN, no defecto: sólo identifica una entidad
   o un módulo. Nunca acompaña a una acción. */
.fr-ico-box {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: var(--fr-r-card);
  background: var(--fr-n100); color: var(--fr-teal);
  border: 1px solid var(--fr-line-1);
}
.fr-ico-box--lg     { width: var(--fr-touch-min); height: var(--fr-touch-min); }
.fr-ico-box--accent { background: rgba(0, 48, 59, 0.08); }
.fr-ico-box--danger { background: var(--fr-danger-soft); color: var(--fr-danger); }
.fr-ico-box--ok     { background: var(--fr-ok-soft); color: var(--fr-ok); }

/* ══════════════════════════════════════════════════════════════════════
   5.2 · BOTONES — mecánicos, precisos
   ══════════════════════════════════════════════════════════════════════ */
.fr-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  min-height: 32px; padding: 0 var(--fr-s5);
  border: 1px solid transparent; border-radius: var(--fr-r-ctl);
  font-family: var(--fr-font); font-size: var(--fr-t-small); font-weight: var(--fr-fw-med);
  line-height: 1; letter-spacing: 0.01em; white-space: nowrap;
  cursor: pointer; -webkit-user-select: none; user-select: none;
  transition: background var(--fr-d-micro) var(--fr-ease),
              border-color var(--fr-d-micro) var(--fr-ease),
              box-shadow var(--fr-d-micro) var(--fr-ease),
              transform var(--fr-d-micro) var(--fr-ease);
}
.fr-btn:active   { transform: translateY(0.5px); box-shadow: var(--fr-sh-press); }
.fr-btn:disabled { opacity: 0.45; cursor: not-allowed; transform: none; }

.fr-btn--primary       { background: var(--fr-ink); color: var(--fr-cream); box-shadow: var(--fr-sh-contact); }
.fr-btn--primary:hover { background: #241417; }
.fr-btn--primary:active{ background: #0d0304; }
.fr-btn--teal          { background: var(--fr-teal); color: var(--fr-cream); box-shadow: var(--fr-sh-contact); }
.fr-btn--teal:hover    { background: #0b4450; }
.fr-btn--secondary {
  background: var(--fr-raised); color: var(--fr-text);
  border-color: var(--fr-line-3); box-shadow: var(--fr-sh-contact);
}
.fr-btn--secondary:hover { background: var(--fr-top); border-color: rgba(22, 8, 10, 0.34); }
.fr-btn--ghost           { background: transparent; color: var(--fr-text-2); }
.fr-btn--ghost:hover     { background: var(--fr-hover); color: var(--fr-text); }
.fr-btn--danger          { background: var(--fr-danger); color: #fff; box-shadow: var(--fr-sh-contact); }
.fr-btn--danger:hover    { background: #86251b; }
.fr-btn--danger-soft     { background: var(--fr-danger-soft); color: var(--fr-danger); border-color: rgba(158, 43, 31, 0.30); }
.fr-btn--danger-soft:hover { background: rgba(158, 43, 31, 0.18); }
.fr-btn--on-dark         { background: rgba(240, 238, 233, 0.12); color: var(--fr-cream); }
.fr-btn--on-dark:hover   { background: rgba(240, 238, 233, 0.20); }
/* La acción PRINCIPAL sobre material oscuro. `--on-dark` resuelve las
   secundarias, pero la principal no era expresable: `--primary` es tinta
   sobre tinta encima de un dock petróleo, y `--secondary` la degrada de
   rango. Sin esta variante, una barra de acciones oscura sólo podía pintar
   su botón principal en el CSS del módulo — que es exactamente la isla que
   este sistema impide. No es un componente nuevo: es la inversa de
   `--primary` dentro de la familia `--on-dark` que ya existía.
   Consumidor: el dock del formulario de F07 (05B · ola 3).             */
.fr-btn--primary-on-dark       { background: var(--fr-cream); color: var(--fr-ink); box-shadow: var(--fr-sh-contact); }
.fr-btn--primary-on-dark:hover { background: #fffdf9; }

.fr-btn--compact { min-height: 26px; padding: 0 var(--fr-s4); font-size: var(--fr-t-mini); }
.fr-btn--lg      { min-height: 40px; padding: 0 var(--fr-s7); font-size: var(--fr-t-base); }
.fr-btn--icon    { min-width: 32px; padding: 0; }
.fr-btn--icon.fr-btn--compact { min-width: 26px; }
.fr-btn--block   { width: 100%; }
.fr-btn.is-selected { background: var(--fr-selected); color: var(--fr-teal); border-color: rgba(0, 48, 59, 0.34); }
.fr-btn.is-loading  { pointer-events: none; opacity: 0.75; }
.fr-btn.is-loading::before {
  content: ""; width: 12px; height: 12px; border-radius: 50%;
  border: 2px solid currentColor; border-right-color: transparent;
  animation: fr-spin 0.7s linear infinite;
}
.fr-btn .fr-icon { margin-inline-start: -2px; }
.fr-btn--icon .fr-icon,
.fr-iconbtn .fr-icon { margin: 0; }

@media (pointer: coarse) {
  .fr-btn, .fr-btn--compact { min-height: var(--fr-touch-min); padding: 0 var(--fr-s6); font-size: var(--fr-t-base); }
  .fr-btn--icon, .fr-btn--icon.fr-btn--compact { min-width: var(--fr-touch-min); padding: 0; }
}

/* Split y segmented */
.fr-split { display: inline-flex; }
.fr-split .fr-btn:first-child { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.fr-split .fr-btn:last-child  { border-top-left-radius: 0; border-bottom-left-radius: 0; margin-left: 1px; padding: 0 var(--fr-s3); }

.fr-segmented {
  display: inline-flex; padding: 2px; gap: 2px;
  background: var(--fr-n200); border-radius: calc(var(--fr-r-ctl) + 2px);
}
.fr-segmented button {
  border: 0; background: transparent; border-radius: var(--fr-r-ctl);
  min-height: 26px; padding: 0 var(--fr-s5); cursor: pointer;
  font-family: var(--fr-font); font-size: var(--fr-t-mini); font-weight: var(--fr-fw-med);
  color: var(--fr-text-2);
  transition: background var(--fr-d-micro) var(--fr-ease), color var(--fr-d-micro) var(--fr-ease);
}
.fr-segmented button[aria-pressed="true"],
.fr-segmented button.is-active { background: var(--fr-raised); color: var(--fr-text); }
/* SYS_GAP_06 (MOLDNEW-UX-03R-S2) — LA OPCION ELEGIDA TAMBIEN RECIBE FOCO.
   El anillo del sistema (`.fr-app :focus-visible`) es un `box-shadow`, y esta
   regla declaraba otro con especificidad MAYOR -(0,2,1) por el selector de
   atributo, contra (0,2,0)-. Resultado: la opcion activa de un segmentado era
   el unico control de la pagina que NO cambiaba de aspecto al tabular sobre
   el, en TODOS sus consumidores. Medido en la consola de incidencias sobre
   produccion: `box-shadow` identico con foco y sin foco, mientras las otras
   tres pastillas del mismo grupo si lo mostraban.
   La marca de elegido no se pierde: vuelve en cuanto el foco se va, y
   mientras tanto quedan el fondo y la tinta, que esta regla conserva. */
.fr-segmented button[aria-pressed="true"]:not(:focus-visible),
.fr-segmented button.is-active:not(:focus-visible) { box-shadow: var(--fr-sh-contact); }
@media (pointer: coarse) { .fr-segmented button { min-height: var(--fr-touch-min); } }

/* ══════════════════════════════════════════════════════════════════════
   5.3 · CAMPOS
   ══════════════════════════════════════════════════════════════════════ */
.fr-field { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.fr-label {
  display: flex; align-items: center; gap: 5px; flex-wrap: wrap;
  font-size: var(--fr-t-mini); font-weight: var(--fr-fw-med);
  color: var(--fr-text-3); letter-spacing: 0.015em;
}
.fr-label .req { color: var(--fr-danger); }
.fr-help      { font-size: var(--fr-t-micro); color: var(--fr-text-3); }
.fr-error-msg { font-size: var(--fr-t-micro); color: var(--fr-danger); font-weight: var(--fr-fw-med); }

.fr-input, .fr-select, .fr-textarea {
  font-family: var(--fr-font); font-size: var(--fr-t-small);
  color: var(--fr-text);
  /* UN CAMPO NO DECIDE SU ANCHO: lo decide su contenedor. Un `<input>` trae
     un ancho intrinseco de fabrica —el atributo `size`, unos 177 px— y en una
     columna mas estrecha SE SALE: no desborda la pagina porque el contenedor
     lo recorta, pero se monta encima del campo de al lado. Medido en la
     familia de extrusion-soplado: tres campos de 136 px dentro de columnas de
     100, solapandose 36 px entre si en TODOS los viewports, tambien a 1.366.
     `max-width` en vez de `width: 100%` a proposito: impide desbordar sin
     obligar a estirar, que cambiaria las filas donde un campo convive con un
     boton. `min-width: 0` es lo que permite encogerse dentro de un flex. */
  max-width: 100%; min-width: 0;
  min-height: 32px; padding: 0 var(--fr-s4);
  background: var(--fr-raised);
  border: 1px solid var(--fr-line-3);
  border-radius: var(--fr-r-ctl);
  transition: border-color var(--fr-d-micro) var(--fr-ease),
              background var(--fr-d-micro) var(--fr-ease),
              box-shadow var(--fr-d-micro) var(--fr-ease);
}
.fr-textarea { padding: var(--fr-s3) var(--fr-s4); min-height: 72px; resize: vertical; }
.fr-input::placeholder, .fr-textarea::placeholder { color: var(--fr-text-4); }
.fr-input:hover, .fr-select:hover, .fr-textarea:hover { border-color: rgba(22, 8, 10, 0.34); }
.fr-input:focus-visible, .fr-select:focus-visible, .fr-textarea:focus-visible {
  outline: none; background: var(--fr-top);
  border-color: var(--fr-teal); box-shadow: 0 0 0 3px var(--fr-focus-ring);
}
.fr-input[readonly] { background: var(--fr-n100); color: var(--fr-text-2); border-style: dashed; }
.fr-input:disabled, .fr-select:disabled { background: var(--fr-n100); color: var(--fr-text-4); cursor: not-allowed; }
.fr-field.has-error .fr-input,
.fr-field.has-error .fr-select { border-color: var(--fr-danger); background: #fdf6f5; }
.fr-field.has-error .fr-input:focus-visible { box-shadow: 0 0 0 3px rgba(158, 43, 31, 0.14); }
.fr-field.has-warn .fr-input { border-color: var(--fr-warn); }
/* Campo calculado o precargado: el dato que pone el sistema, no el operario. */
.fr-field--calc .fr-input,
.fr-field--calc .fr-unit-input {
  background: var(--fr-n50); border-style: solid; border-color: var(--fr-line-2); color: var(--fr-text-2);
}
.fr-field--calc .fr-label::after {
  content: "calc."; margin-left: 6px; padding: 0 4px;
  font-size: 0.625rem; font-weight: var(--fr-fw-med); letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--fr-text-3);
  background: var(--fr-n200); border-radius: 2px;
}
.fr-field :is(input, select, textarea) {
  scroll-margin-block-start: calc(var(--fr-sticky-top) + 12px);
  scroll-margin-block-end: calc(var(--fr-dock-h) + var(--fr-safe-bottom) + 12px);
}

/* ── Relleno del gestor de contraseñas ──────────────────────────────────
   Chromium pinta el campo autorrellenado con un azul claro propio y fija
   el color del texto por su cuenta: no es una regla de autor, así que
   ganarle con `background` no sirve. Los dos recursos que sí funcionan
   —y que no necesitan `!important`— son un `box-shadow` interior que
   cubre el fondo del navegador y `-webkit-text-fill-color`, que es la
   propiedad que el propio navegador está usando.

   Sin esto, en tema oscuro el campo queda AZUL CLARO CON TEXTO NEGRO
   dentro de una superficie de tinta: medido en la puerta de entrada, y no
   era deuda de esa pantalla sino un hueco del sistema — le pasa a
   cualquier `.fr-input` del producto con el tema oscuro activo.

   La transición de 100.000 s es el recurso conocido para que el navegador
   no reponga su fondo tras el primer pintado; no anima nada visible. */
.fr-input:-webkit-autofill,
.fr-select:-webkit-autofill,
.fr-textarea:-webkit-autofill {
  -webkit-text-fill-color: var(--fr-text);
  caret-color: var(--fr-text);
  -webkit-box-shadow: inset 0 0 0 100px var(--fr-top);
  border-color: var(--fr-line-3);
  transition: background-color 100000s ease-in-out 0s;
}
.fr-input:-webkit-autofill:focus,
.fr-select:-webkit-autofill:focus,
.fr-textarea:-webkit-autofill:focus {
  -webkit-box-shadow: inset 0 0 0 100px var(--fr-top), 0 0 0 3px var(--fr-focus-ring);
  border-color: var(--fr-teal);
}
@media (pointer: coarse) {
  /* 16px = antizoom iOS SOLO en campos. No se aplica globalmente: un
     `input{font-size:16px !important}` global infla cualquier tabla densa
     en móvil, que es la trampa que este sistema evita. */
  .fr-input, .fr-select { min-height: var(--fr-touch-min); font-size: var(--fr-t-lead); }
}

/* Input de unidad — patrón crítico de FORTEONE */
.fr-unit-input {
  display: flex; align-items: stretch; min-width: 0;
  border: 1px solid var(--fr-line-3); border-radius: var(--fr-r-ctl);
  background: var(--fr-raised); overflow: hidden;
  transition: border-color var(--fr-d-micro) var(--fr-ease), box-shadow var(--fr-d-micro) var(--fr-ease);
}
.fr-unit-input input {
  flex: 1 1 auto; min-width: 0; min-height: 30px; border: 0; background: transparent;
  padding: 0 var(--fr-s2) 0 var(--fr-s4); text-align: right;
  font-family: var(--fr-font); font-size: var(--fr-t-small); color: var(--fr-text);
}
.fr-unit-input input:focus { outline: none; }
/* LA UNIDAD NO ES OTRO CAMPO (MOLDNEW-UX-03R).
   Llevaba fondo propio (`--fr-n100`) y un filete a la izquierda: dos señales
   de LÍMITE dentro de un control que es uno solo, y el resultado se leía
   como un input pegado a otro input. Una magnitud es «550 mm», no «550» al
   lado de «mm». El shell conserva su borde —ahí sí hay un límite, el del
   control— y la unidad pasa a ser lo que es: el sufijo del número, en tinta
   terciaria. La señal de fuera de rango SÍ conserva su fondo: eso es estado,
   no decoración. */
.fr-unit-input .unit {
  display: flex; align-items: center; padding: 0 var(--fr-s4) 0 0;
  background: transparent; border-left: 0;
  font-family: var(--fr-font);
  font-size: var(--fr-t-mini); font-weight: var(--fr-fw-med); color: var(--fr-text-3);
  white-space: nowrap;
}
.fr-unit-input:focus-within { border-color: var(--fr-teal); box-shadow: 0 0 0 3px var(--fr-focus-ring); background: var(--fr-top); }
.fr-unit-input.is-out-of-range { border-color: var(--fr-warn); }
.fr-unit-input.is-out-of-range .unit { color: var(--fr-warn); background: var(--fr-warn-soft); }
@media (pointer: coarse) { .fr-unit-input input { min-height: var(--fr-touch-min); font-size: var(--fr-t-lead); } }

/* Check / radio / switch */
.fr-check { display: inline-flex; align-items: center; gap: 8px; font-size: var(--fr-t-small); cursor: pointer; min-height: 24px; }
.fr-check input {
  appearance: none; width: 16px; height: 16px; margin: 0;
  border: 1.5px solid var(--fr-line-3); border-radius: 4px;
  background: var(--fr-raised); cursor: pointer; display: grid; place-content: center;
  transition: background var(--fr-d-micro) var(--fr-ease), border-color var(--fr-d-micro) var(--fr-ease);
}
.fr-check input[type="radio"] { border-radius: 50%; }
.fr-check input:checked { background: var(--fr-teal); border-color: var(--fr-teal); }
.fr-check input[type="checkbox"]:checked::before {
  content: ""; width: 9px; height: 5px;
  border: 2px solid var(--fr-cream); border-top: 0; border-right: 0;
  transform: rotate(-45deg) translate(0.5px, -1px);
}
.fr-check input[type="radio"]:checked::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--fr-cream); }
@media (pointer: coarse) { .fr-check { min-height: var(--fr-touch-min); } .fr-check input { width: 20px; height: 20px; } }
/* SYS_GAP_03 — casilla SIN rotulo visible. Sin texto que ocupe ancho, la
   etiqueta se queda del tamano del cuadrito y el objetivo tactil se pierde:
   la variante le da el area de un boton de icono (30 px raton / 44 dedo) sin
   pintar borde ni fondo. Antes se lograba apilando `.fr-iconbtn .fr-check`. */
.fr-check--bare {
  justify-content: center; gap: 0; flex: 0 0 auto;
  width: 30px; height: 30px; min-height: 30px; padding: 0;
  border-radius: var(--fr-r-ctl); background: transparent;
  transition: background var(--fr-d-micro) var(--fr-ease);
}
.fr-check--bare:hover { background: var(--fr-hover); }
@media (pointer: coarse) {
  .fr-check--bare { width: var(--fr-touch-min); height: var(--fr-touch-min); min-height: var(--fr-touch-min); }
}

.fr-switch { position: relative; display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: var(--fr-t-small); min-height: 24px; }
.fr-switch input {
  appearance: none; width: 34px; height: 20px; margin: 0; border-radius: 999px;
  background: var(--fr-n300); cursor: pointer; transition: background var(--fr-d-micro) var(--fr-ease);
}
.fr-switch input::before {
  content: ""; position: absolute; width: 16px; height: 16px; border-radius: 50%;
  background: #fff; margin: 2px; box-shadow: var(--fr-sh-contact);
  transition: transform var(--fr-d-micro) var(--fr-ease);
}
.fr-switch input:checked { background: var(--fr-teal); }
.fr-switch input:checked::before { transform: translateX(14px); }
/* Un interruptor SIN rótulo visible no puede tomar el ancho de un texto
   que no existe: se queda en los 34 px de su propia pastilla y falla el
   objetivo táctil por el eje ancho mientras el alto cumple —el mismo
   defecto que la ola 10 midió en `.fr-iconbtn` dentro de un flex, y por la
   misma razón: nada desborda, así que ninguna métrica lo ve—. Con rótulo,
   la etiqueta ya es más ancha y `min-width` no cambia nada.
   Medido en 05B · ola 11 sobre los cuatro conmutadores de la ficha de
   molde (34×44 en `pointer: coarse`). */
@media (pointer: coarse) {
  .fr-switch { min-height: var(--fr-touch-min); min-width: var(--fr-touch-min); }
}

/* Búsqueda, stepper, grupo, fichero */
.fr-search { position: relative; display: flex; align-items: center; min-width: 0; }
.fr-search .fr-input { padding-left: 32px; width: 100%; }
.fr-search .fr-icon,
.fr-field-ico > .fr-icon {
  position: absolute; left: 9px; top: 50%; transform: translateY(-50%);
  color: var(--fr-text-4); z-index: 1;
}
.fr-field-ico { position: relative; display: flex; align-items: center; min-width: 0; }
.fr-field-ico > .fr-input { padding-left: 32px; width: 100%; }

.fr-stepper { display: flex; align-items: stretch; border: 1px solid var(--fr-line-3); border-radius: var(--fr-r-ctl); background: var(--fr-raised); overflow: hidden; }
.fr-stepper input {
  flex: 1 1 auto; min-width: 0; min-height: 30px; border: 0; background: transparent;
  padding: 0 var(--fr-s3); text-align: right; font-family: var(--fr-font);
  font-size: var(--fr-t-small); font-variant-numeric: tabular-nums; color: var(--fr-text);
}
.fr-stepper input:focus { outline: none; }
.fr-stepper button {
  flex: 0 0 auto; width: 26px; border: 0; border-left: 1px solid var(--fr-line-2);
  background: var(--fr-n100); color: var(--fr-text-2); cursor: pointer; font: inherit;
  display: grid; place-items: center;
}
.fr-stepper button:hover { background: var(--fr-n200); color: var(--fr-text); }
.fr-stepper:focus-within { border-color: var(--fr-teal); box-shadow: 0 0 0 3px var(--fr-focus-ring); }
@media (pointer: coarse) { .fr-stepper input { min-height: var(--fr-touch-min); font-size: var(--fr-t-lead); } .fr-stepper button { width: var(--fr-touch-min); } }

.fr-inputgroup { display: flex; align-items: stretch; min-width: 0; }
.fr-inputgroup > .fr-input, .fr-inputgroup > .fr-select { border-radius: var(--fr-r-ctl) 0 0 var(--fr-r-ctl); flex: 1 1 auto; min-width: 0; }
.fr-inputgroup > .fr-btn { border-radius: 0 var(--fr-r-ctl) var(--fr-r-ctl) 0; margin-left: -1px; }
.fr-inputgroup > .fr-segmented { margin-left: var(--fr-s3); }

.fr-file {
  display: flex; align-items: center; gap: var(--fr-s4);
  padding: var(--fr-s4) var(--fr-s5);
  border: 1px dashed var(--fr-line-3); border-radius: var(--fr-r-ctl);
  background: var(--fr-work); color: var(--fr-text-2); font-size: var(--fr-t-small);
}
.fr-file .name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--fr-text); }

/* ══════════════════════════════════════════════════════════════════════
   5.4 · TABLAS
   ══════════════════════════════════════════════════════════════════════
   Cuatro densidades sobre la misma anatomía, no cuatro tablas. La cabecera
   se pega bajo --fr-sticky-top, es decir, bajo el stack real. */
/* ── EL ENVOLTORIO RECORTA, PERO NO ES UN SCROLLPORT ────────────────────
   `overflow: hidden` recortaba el radio de la tarjeta —para eso estaba— y de
   paso convertía el envoltorio en CONTENEDOR DE SCROLL. Y `position: sticky`
   se resuelve contra el scroll container más cercano, así que la cabecera
   dejaba de mirar a la ventana y pasaba a mirar a esta caja, que no scrollea
   nunca: la restricción «el borde superior a `--fr-sticky-top` del borde del
   scrollport» se cumplía EMPUJÁNDOLA HACIA ABAJO esa misma cantidad, desde el
   primer píxel y sin que nadie hubiese desplazado nada.

   Medido en `Gestión de usuarios`, la única familia del inventario que sirve
   la tabla sin scroller: la cabecera tapaba exactamente `top` píxeles de la
   primera fila —44 con ratón, 74 en un iPad, 109 en un iPhone vertical— en
   los SEIS viewports, escritorio incluido. No era un defecto de iOS: el
   recorte del teléfono sólo lo hacía más grande. Comparten causa
   `auth/admin_tarjetas.html` y `auth/admin_subtarjetas.html`.

   `overflow: clip` recorta igual —radio incluido— y NO establece contenedor
   de scroll, así que la cabecera vuelve a colgar de la ventana y se pega
   bajo la barra, que es lo que el componente prometía. El respaldo mantiene
   el comportamiento anterior donde `clip` no exista.                      */
.fr-table-wrap { background: var(--fr-raised); border: 1px solid var(--fr-line-2); border-radius: var(--fr-r-card); overflow: clip; }
@supports not (overflow: clip) {
  .fr-table-wrap { overflow: hidden; }
}
/* ── Y CUANDO LA TABLA NO CABE, SE RECORRE ──────────────────────────────
   `clip` devuelve la cabecera a la ventana, pero sigue RECORTANDO lo que
   sobra por el costado. Medido en `Gestión de usuarios`: 450 px de tabla
   fuera de alcance a 820×1180 y a 852×393, y 90 px a 1194×834. Son
   columnas enteras —nivel, último acceso, acciones— que en una tableta
   sencillamente no existían. A 1366 y a 1920 la tabla cabe y el exceso es 0.

   Las dos cosas que pide el encargo entran en tensión sólo aquí, y no hay
   CSS que las dé a la vez: `position: sticky` se resuelve contra el
   contenedor de scroll más cercano, así que en cuanto el envoltorio
   scrollea en horizontal deja de haber forma de que la cabecera se pegue a
   la ventana. Se reparte por RÉGIMEN, que es donde el problema existe:

     ≥ 1280   la tabla cabe · el envoltorio recorta · la cabecera se pega
              bajo la barra, que es lo que §8 pide
     < 1280   la tabla no cabe · el envoltorio se recorre · la cabecera se
              queda en su sitio (`top: 0`), que es lo que §21 pide

   En los dos casos la cabecera NO tapa ninguna fila, que era el defecto.
   El corte es el techo de TABLET_L en §1.17; no es un dispositivo.     */
@media (max-width: 1279.98px) {
  .fr-table-wrap { overflow-x: auto; overflow-y: clip; overscroll-behavior-x: contain; }
  .fr-table-wrap > .fr-table th { top: 0; }
}
/* ── Y CUANDO NO CABE POR ENCIMA DE ESE CORTE, TAMBIÉN ──────────────────
   VU-02. El reparto por régimen daba por hecho que a partir de 1280 la tabla
   cabe. Una tabla de once columnas de dominio —Despieces— mide 1.393 px, así
   que entre 1.280 y ~1.460 el envoltorio la RECORTABA sin forma de
   alcanzarla: dos columnas inexistentes en un portátil de 1.366, que es el
   ancho más usado en planta. Es justo lo que el contrato de SCROLL
   INTENCIONAL prohíbe.

   Quién sabe si cabe es el navegador, no una media query: `FR.tables` marca
   el envoltorio que desborda y sólo ése se recorre; el que cabe conserva el
   recorte y su cabecera pegada. Sin `min-width: max-content`, que calcula el
   ancho sobre el contenido e infla la tabla hasta dejar una sola columna a
   la vista (medido: 8.366 px en Colorantes).                              */
.fr-table-wrap.is-scrollable { overflow-x: auto; overflow-y: clip; overscroll-behavior-x: contain; }
.fr-table-wrap.is-scrollable > .fr-table th { top: 0; }
@supports not (overflow: clip) {
  .fr-table-wrap.is-scrollable { overflow-x: auto; overflow-y: hidden; }
}
@supports not (overflow: clip) {
  @media (max-width: 1279.98px) {
    .fr-table-wrap { overflow-x: auto; overflow-y: hidden; }
  }
}
.fr-table { width: 100%; border-collapse: collapse; font-size: var(--fr-t-small); }
.fr-table th {
  position: sticky; top: var(--fr-sticky-top); z-index: var(--fr-z-thead);
  background: var(--fr-work);
  text-align: left; padding: 7px 10px;
  font-size: var(--fr-t-micro); font-weight: var(--fr-fw-bold);
  letter-spacing: 0.05em; text-transform: uppercase; color: var(--fr-text-3);
  border-bottom: 1px solid var(--fr-line-3);
  white-space: nowrap;
}
.fr-table th.sortable { cursor: pointer; }
.fr-table th.sortable:hover { color: var(--fr-text); }
.fr-table th .dir { color: var(--fr-teal); font-weight: var(--fr-fw-bold); }
.fr-table td { padding: 6px 10px; border-bottom: 1px solid var(--fr-line-1); vertical-align: middle; }
.fr-table tbody tr { transition: background var(--fr-d-micro) var(--fr-ease); }
.fr-table tbody tr:hover { background: var(--fr-hover); }
.fr-table tbody tr.is-selected { background: var(--fr-selected); }
.fr-table tbody tr.is-selected td:first-child { box-shadow: inset 2px 0 0 var(--fr-teal); }
.fr-table td.fr-td-num, .fr-table th.fr-th-num { text-align: right; }
.fr-table td .unit { font-size: var(--fr-t-micro); color: var(--fr-text-3); margin-left: 3px; }
.fr-table td.fr-td-id { font-weight: var(--fr-fw-med); white-space: nowrap; }
.fr-table td.fr-td-actions { text-align: right; white-space: nowrap; }
/* VU-02 · Una celda de acciones ya trae su alto: `.fr-iconbtn` mide 30 px y
   el relleno vertical de la celda (6+6) lo sumaba encima, dejando la fila en
   43 px frente a los 35 de una fila sin acciones. La misma corrección que el
   sistema hacía sólo en `pointer: coarse` —donde el botón mide 44— vale con
   ratón: el relleno se reduce a 2 px y las catorce listas de Equipos de
   Producción comparten densidad con Moldes y Máquinas. */
.fr-table td.fr-td-actions { padding-top: 2px; padding-bottom: 2px; }
/* VU-02 · Texto largo en una celda: se recorta con elipsis y el valor
   completo viaja en `title`. El recorte vive en un bloque INTERIOR
   (`.fr-clip`), no en la celda: el algoritmo de tabla ignora `max-width` en
   un `<td>` cuando la tabla se dimensiona a su contenido máximo (scroller),
   y un título de 80 caracteres sin envolver ensanchaba la columna hasta
   dejar el resto fuera de la vista. Medido en Despieces a 1920: dos
   columnas visibles de doce. La cabecera de la columna de acciones no lleva
   rótulo visible: su nombre accesible va en `.fr-visually-hidden`.        */
.fr-table td.fr-td-clip { white-space: nowrap; }
.fr-table .fr-clip { display: block; max-width: 34ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* VU-02 · Una etiqueta dentro de una celda no parte la fila en dos: el texto
   largo se recorta DENTRO del chip. Sin esto, «PE ALTA DENSIDAD 2ª/ALTA
   FLUIDEZ >45» envolvía y esa fila medía el doble que sus vecinas. */
.fr-table td > .fr-chip {
  display: inline-block; vertical-align: middle;
  max-width: 24ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fr-table th.fr-th-actions { width: 1%; }
.fr-table tfoot td { background: var(--fr-work); font-weight: var(--fr-fw-bold); border-top: 1px solid var(--fr-line-3); }
.fr-table--dense th { padding: 5px 8px; }
.fr-table--dense td { padding: 3px 8px; font-size: var(--fr-t-mini); }
.fr-table--relaxed td { padding: 10px; font-size: var(--fr-t-base); }
@media (pointer: coarse) { .fr-table td { padding-top: 9px; padding-bottom: 9px; } }

/* ── Objetivos de fila que NO son botones ───────────────────────────────
   Una tabla de datos tiene dos controles que el sistema no expresaba, y que
   son la causa de 725 objetivos por debajo de 44 px en la familia de
   administración de datos:

     · el ENLACE DE IDENTIDAD de la fila — el nombre de la tabla, del molde,
       del registro — que abre su detalle;
     · la FILA ENTERA cuando ella misma es el control (`<tr onclick>`).

   Ninguno puede resolverse subiendo el alto del control y sumándolo al
   relleno de la celda: la fila crecería dos veces. El enlace RENUNCIA al
   relleno vertical de su celda y lo ocupa él, de modo que en `pointer:
   coarse` la fila mide exactamente el objetivo táctil y el área sensible
   coincide con la celda — sin pseudo-elementos, sin márgenes negativos y
   sin invadir la fila vecina.

   En puntero fino la geometría no cambia: la densidad de escritorio es la
   razón de ser de estas tablas.                                          */
.fr-table td.fr-td-link { padding-top: 0; padding-bottom: 0; }
.fr-table td.fr-td-link > .fr-cell-link {
  display: inline-flex; align-items: center; gap: 6px;
  max-width: 100%; min-height: 31px;
}
.fr-table--dense td.fr-td-link > .fr-cell-link { min-height: 23px; }
.fr-table--relaxed td.fr-td-link > .fr-cell-link { min-height: 39px; }
.fr-table tbody tr.fr-tr-nav { cursor: pointer; }
@media (pointer: coarse) {
  .fr-table td.fr-td-link > .fr-cell-link,
  .fr-table--dense td.fr-td-link > .fr-cell-link,
  .fr-table--relaxed td.fr-td-link > .fr-cell-link { min-height: var(--fr-touch-min); }
  /* `height` sobre una celda actúa como mínimo de la fila: la fila navegable
     alcanza el objetivo táctil sin tocar el relleno de sus celdas. */
  .fr-table tbody tr.fr-tr-nav > td { height: var(--fr-touch-min); }
  /* Una celda de acciones contiene botones, y en táctil un botón YA mide 44
     px. Sumarle el relleno vertical de la celda da una fila de 62 px: el
     objetivo se cumple dos veces y el listado pierde un tercio de las filas
     visibles. Medido en el panel de Admin DB: 63 px por fila. */
  .fr-table td.fr-td-actions { padding-top: 0; padding-bottom: 0; }
}

/* Scroller horizontal local: con affordance y columna identificadora. La
   tabla ancha scrollea DENTRO de su caja; la página nunca. */
.fr-table-scroller { overflow-x: auto; position: relative; }
.fr-table-scroller .fr-table { min-width: max-content; }
/* `--fr-sticky-top` describe el STACK DE LA VENTANA: topbar más franja de
   mando. Dentro de un scroller ese hueco no existe —el scrollport empieza en
   el borde de la caja—, y el navegador aplica el desplazamiento igual: la
   cabecera se coloca 50 px POR DEBAJO de su sitio y tapa la primera fila del
   listado. Medido en el panel de Admin DB (thead 291→341 con scrollY = 0).
   Dentro del scroller la referencia es el propio scroller.               */
.fr-table-scroller .fr-table th { top: 0; }
.fr-table-scroller .fr-col-sticky {
  position: sticky; left: 0; z-index: 3;
  background: var(--fr-raised);
  box-shadow: 1px 0 0 var(--fr-line-2);
}
.fr-table-scroller thead .fr-col-sticky { background: var(--fr-work); z-index: 4; }
.fr-table-scroller tbody tr:hover .fr-col-sticky { background: #f2efe9; }

/* Matriz densa de parámetros: la unidad vive en la CABECERA, nunca
   repetida en cada celda. Ese es el ahorro real. */
.fr-matrix { width: 100%; border-collapse: collapse; font-size: var(--fr-t-mini); }
.fr-matrix th {
  padding: 4px 6px; text-align: right; white-space: nowrap;
  font-size: var(--fr-t-micro); font-weight: var(--fr-fw-bold);
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--fr-text-3);
  border-bottom: 1px solid var(--fr-line-2); background: var(--fr-work);
}
.fr-matrix th:first-child { text-align: left; }
.fr-matrix th .u { display: block; font-weight: var(--fr-fw-reg); text-transform: none; letter-spacing: 0; color: var(--fr-text-4); }
.fr-matrix td { padding: 2px 4px; border-bottom: 1px solid var(--fr-line-1); }
.fr-matrix td:first-child { padding-left: 6px; font-weight: var(--fr-fw-med); color: var(--fr-text-2); white-space: nowrap; }
.fr-matrix input {
  width: 100%; min-width: 46px; min-height: 28px; padding: 0 5px;
  text-align: right; font-family: var(--fr-font); font-size: var(--fr-t-small);
  font-variant-numeric: tabular-nums; color: var(--fr-text);
  background: var(--fr-top); border: 1px solid var(--fr-line-2); border-radius: 3px;
}
.fr-matrix input:focus-visible { outline: none; border-color: var(--fr-teal); box-shadow: 0 0 0 2px var(--fr-focus-ring); }
.fr-matrix input[readonly] { background: var(--fr-n50); color: var(--fr-text-2); }
.fr-matrix .is-out input { border-color: var(--fr-warn); background: #fdfaf2; }
/* Select dentro de una matriz densa: control de celda, no campo de
   formulario. La regla base va ANTES del media query táctil para que 44 px
   gane a 28 px con la misma especificidad. */
.fr-matrix select, .fr-matrix .fr-select {
  min-height: 28px; padding: 0 4px; font-size: var(--fr-t-mini);
  border-radius: 3px; border-color: var(--fr-line-2); background: var(--fr-top);
}
@media (pointer: coarse) {
  .fr-matrix input { min-height: var(--fr-touch-min); font-size: var(--fr-t-lead); }
  .fr-matrix select, .fr-matrix .fr-select { min-height: var(--fr-touch-min); font-size: var(--fr-t-base); }
}

/* Paginación */
.fr-pagination { display: flex; align-items: center; gap: var(--fr-s2); padding: var(--fr-s4) 0; flex-wrap: wrap; }
.fr-pagination .info { margin-right: auto; font-size: var(--fr-t-mini); color: var(--fr-text-3); }
/* Paginar es navegar: en FORTEONE cada página es una URL, así que el control
   es un `<a href>` y no un `<button>`. El componente cubre los dos — un
   `<span>` sirve para el estado no navegable. Como `.fr-pagination a` vale
   (0,2,0) igual que `.fr-app a` pero llega después en el bundle, el color de
   enlace no le alcanza y la cadena `:not()` de 02-foundations.css no
   necesita una décima excepción. */
.fr-pagination button,
.fr-pagination a,
.fr-pagination > span {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 28px; min-height: 28px; padding: 0 6px;
  border: 1px solid var(--fr-line-2); border-radius: var(--fr-r-ctl);
  background: var(--fr-raised); color: var(--fr-text-2); font: inherit; font-size: var(--fr-t-mini);
  text-decoration: none;
  cursor: pointer;
  transition: border-color var(--fr-d-micro) var(--fr-ease), color var(--fr-d-micro) var(--fr-ease);
}
.fr-pagination button:hover,
.fr-pagination a:hover { border-color: var(--fr-line-3); color: var(--fr-text); text-decoration: none; }
.fr-pagination button.is-current,
.fr-pagination a.is-current,
.fr-pagination > span.is-current { background: var(--fr-ink); color: var(--fr-cream); border-color: var(--fr-ink); }
.fr-pagination > span.disabled,
.fr-pagination button:disabled { color: var(--fr-text-4); background: transparent; cursor: default; }
.fr-pagination > span.info { border: 0; background: transparent; padding: 0; min-width: 0; }
@media (pointer: coarse) {
  .fr-pagination button,
  .fr-pagination a,
  .fr-pagination > span { min-width: var(--fr-touch-min); min-height: var(--fr-touch-min); }
}

/* ══════════════════════════════════════════════════════════════════════
   5.5 · TARJETAS, KPI Y ESTADOS
   ══════════════════════════════════════════════════════════════════════ */
.fr-card { background: var(--fr-raised); border: 1px solid var(--fr-line-1); border-radius: var(--fr-r-card); box-shadow: var(--fr-sh-card); }
/* El relleno de una tarjeta es COMPOSICION, no detalle optico: crece con
   la pantalla. 16 px en un telefono, 26 en un monitor. */
.fr-card--pad { padding: var(--fr-pad-card); }
.fr-card--nav { cursor: pointer; transition: box-shadow var(--fr-d-micro) var(--fr-ease), border-color var(--fr-d-micro) var(--fr-ease), transform var(--fr-d-micro) var(--fr-ease); }
.fr-card--nav:hover  { box-shadow: var(--fr-sh-float); border-color: var(--fr-line-2); }
.fr-card--nav:active { transform: translateY(0.5px); box-shadow: var(--fr-sh-card); }
.fr-card-kicker { font-size: var(--fr-t-micro); font-weight: var(--fr-fw-bold); letter-spacing: var(--fr-ls-wide); text-transform: uppercase; color: var(--fr-text-3); }
.fr-card-title  { font-size: var(--fr-t-lead); font-weight: var(--fr-fw-bold); color: var(--fr-text); margin: 2px 0 0; }
.fr-card-desc   { font-size: var(--fr-t-mini); color: var(--fr-text-2); margin: 4px 0 0; }

.fr-kpi { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.fr-kpi-label { font-size: var(--fr-t-micro); font-weight: var(--fr-fw-med); letter-spacing: 0.05em; text-transform: uppercase; color: var(--fr-text-3); }
.fr-kpi-value { font-size: var(--fr-t-h2); font-weight: var(--fr-fw-bold); line-height: 1.1; color: var(--fr-text); }
.fr-kpi-value .unit { font-size: var(--fr-t-mini); font-weight: var(--fr-fw-med); color: var(--fr-text-3); margin-left: 4px; }
.fr-kpi-trend { font-size: var(--fr-t-micro); font-weight: var(--fr-fw-med); }
.fr-kpi-trend.up   { color: var(--fr-ok); }
.fr-kpi-trend.down { color: var(--fr-danger); }
.fr-kpi--instrument .fr-kpi-value { font-size: var(--fr-t-h1); }
.fr-kpi--inline { flex-direction: row; align-items: baseline; gap: var(--fr-s4); }
.fr-kpi--inline .fr-kpi-value { font-size: var(--fr-t-lead); }
.fr-kpi-row { display: flex; gap: var(--fr-s8); flex-wrap: wrap; align-items: flex-end; }
.fr-kpi-row .fr-kpi { padding-right: var(--fr-s8); border-right: 1px solid var(--fr-line-2); }
.fr-kpi-row .fr-kpi:last-child { border-right: 0; }

.fr-status { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fr-t-mini); font-weight: var(--fr-fw-med); }
.fr-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.fr-status--ok     { color: var(--fr-ok); }
.fr-status--warn   { color: var(--fr-warn); }
.fr-status--danger { color: var(--fr-danger); }
.fr-status--info   { color: var(--fr-info); }
.fr-status--muted  { color: var(--fr-text-4); }
.fr-status--muted::before { background: var(--fr-n400); }

.fr-chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2px 8px; min-height: 20px;
  border-radius: 999px; font-size: var(--fr-t-micro); font-weight: var(--fr-fw-med);
  background: var(--fr-n100); color: var(--fr-text-2); border: 1px solid var(--fr-line-2);
}
.fr-chip--ok     { background: var(--fr-ok-soft);     color: var(--fr-ok);     border-color: rgba(46, 107, 62, 0.25); }
.fr-chip--warn   { background: var(--fr-warn-soft);   color: var(--fr-warn);   border-color: rgba(138, 88, 0, 0.25); }
.fr-chip--danger { background: var(--fr-danger-soft); color: var(--fr-danger); border-color: rgba(158, 43, 31, 0.25); }
.fr-chip--info   { background: var(--fr-info-soft);   color: var(--fr-info);   border-color: rgba(21, 90, 104, 0.25); }
.fr-chip--teal   { background: rgba(0, 48, 59, 0.08); color: var(--fr-teal);   border-color: rgba(0, 48, 59, 0.22); }
.fr-recent { font-size: 0.625rem; font-weight: var(--fr-fw-med); letter-spacing: 0.04em; text-transform: uppercase; color: var(--fr-text-3); background: var(--fr-n200); border-radius: 2px; padding: 0 4px; }

/* Indicador de espera indeterminada. La animación `fr-spin` ya vivía en
   foundations y la usaba un solo consumidor —el botón cargando—; darle
   forma de componente evita que cada pantalla dibuje su propia rueda. */
.fr-spinner {
  display: inline-block; width: 28px; height: 28px; flex: 0 0 auto;
  border: 2px solid rgba(var(--fr-ink-rgb), 0.12);
  border-top-color: var(--fr-teal);
  border-radius: 50%;
  animation: fr-spin 0.7s linear infinite;
}
@media (prefers-reduced-motion: reduce) { .fr-spinner { animation-duration: 2.4s; } }

.fr-alert {
  display: flex; gap: 10px; align-items: flex-start;
  padding: var(--fr-s4) var(--fr-s5); border-radius: var(--fr-r-card);
  border: 1px solid; font-size: var(--fr-t-small);
}
.fr-alert--ok     { background: #e3ebe1; border-color: #a9c4a6; color: #245430; }
.fr-alert--warn   { background: #f1e6cb; border-color: #d3b878; color: #6f4700; }
.fr-alert--danger { background: #f2dad4; border-color: #d49a90; color: #7f221a; }
.fr-alert--info   { background: #dde7e9; border-color: #9fbcc2; color: #114a56; }
.fr-alert .fr-alert-ico { flex: 0 0 auto; }
/* Resumen de validación con anclas a los bloques afectados. */
.fr-validation { display: flex; gap: 10px; align-items: flex-start; padding: var(--fr-s4) var(--fr-s5); border-radius: var(--fr-r-card); border: 1px solid; font-size: var(--fr-t-small); }
.fr-validation ul { margin: 4px 0 0; padding-left: 16px; }
.fr-validation a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }

/* ══════════════════════════════════════════════════════════════════════
   5.6 · NAVEGACIÓN LOCAL
   ══════════════════════════════════════════════════════════════════════ */
.fr-tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--fr-line-2); overflow-x: auto; }
.fr-tab {
  position: relative; padding: 8px 14px; min-height: 36px;
  border: 0; background: transparent; cursor: pointer; white-space: nowrap;
  font-family: var(--fr-font); font-size: var(--fr-t-small); font-weight: var(--fr-fw-med);
  color: var(--fr-text-3);
  transition: color var(--fr-d-micro) var(--fr-ease);
}
.fr-tab:hover { color: var(--fr-text); }
.fr-tab.is-active { color: var(--fr-text); font-weight: var(--fr-fw-bold); }
/* ACTIVE_INDICATOR (PRODUCT_DERIVED) — NO es la Línea Beyond. */
.fr-tab.is-active::after {
  content: ""; position: absolute; left: 10px; right: 10px; bottom: -1px;
  height: 2px; border-radius: 1px; background: var(--fr-teal);
}
@media (pointer: coarse) { .fr-tab { min-height: var(--fr-touch-min); padding-top: 12px; } }

.fr-crumbs { display: flex; align-items: center; gap: 7px; font-size: var(--fr-t-mini); color: var(--fr-text-3); flex-wrap: wrap; }
.fr-crumbs a { color: var(--fr-text-3); }
.fr-crumbs a:hover { color: var(--fr-link); }
/* La miga de pan es NAVEGACIÓN: en táctil sus enlaces son objetivos como
   cualquier otro. Sin esto miden la altura de su línea de texto —18 px. */
@media (pointer: coarse) {
  /* Tenia el ALTO tactil desde la ola 1 pero no el ancho: una miga corta
     —«Inicio»— mide 30 px y el objetivo real era 30x44. El `min-width`
     solo afecta a las cortas; las largas ya lo superan por su texto. */
  .fr-crumbs a {
    display: inline-flex; align-items: center; justify-content: center;
    min-height: var(--fr-touch-min); min-width: var(--fr-touch-min);
  }
}
.fr-crumbs .sep { color: var(--fr-text-4); }
/* VU-02 · El kicker de la cabecera es la misma navegación que la miga —desde
   esta fase es la vuelta contextual de las dieciséis pantallas de Equipos de
   Producción—, así que en táctil sus enlaces son objetivos como cualquier
   otro: sin esto miden el alto de su línea, 14 px. Misma receta que
   `.fr-crumbs a`, con el relleno horizontal reducido porque el kicker vive
   pegado al título y no puede separarse de él. */
@media (pointer: coarse) {
  .fr-hero-kicker a {
    display: inline-flex; align-items: center;
    min-height: var(--fr-touch-min); min-width: 44px;
    padding-inline: 2px;
  }
}
.fr-crumbs .current { color: var(--fr-text); font-weight: var(--fr-fw-med); }

/* ══════════════════════════════════════════════════════════════════════
   5.7 · VACÍO, CARGA Y ESQUELETO
   ══════════════════════════════════════════════════════════════════════ */
.fr-empty {
  display: flex; flex-direction: column; align-items: center; gap: var(--fr-s4);
  padding: var(--fr-s10) var(--fr-s6); text-align: center;
  background: var(--fr-work); border: 1px dashed var(--fr-line-2); border-radius: var(--fr-r-card);
  color: var(--fr-text-2);
}
.fr-empty h3 { margin: 0; font-size: var(--fr-t-base); font-weight: var(--fr-fw-bold); color: var(--fr-text); }
.fr-empty p  { margin: 0; font-size: var(--fr-t-small); max-width: 44ch; }
.fr-skeleton { position: relative; overflow: hidden; background: var(--fr-n200); border-radius: var(--fr-r-ctl); min-height: 12px; }
.fr-skeleton::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.55), transparent);
  transform: translateX(-100%);
  animation: fr-shimmer 1.4s var(--fr-ease) infinite;
}
@keyframes fr-shimmer { to { transform: translateX(100%); } }
.fr-loadingbar { height: 2px; background: var(--fr-n200); overflow: hidden; border-radius: 1px; }
.fr-loadingbar::after {
  content: ""; display: block; height: 100%; width: 40%;
  background: var(--fr-teal); border-radius: 1px;
  animation: fr-loading 1.1s var(--fr-ease) infinite;
}
@keyframes fr-loading { from { transform: translateX(-110%); } to { transform: translateX(280%); } }
[data-fr-effects="reduced"] .fr-skeleton::after,
[data-fr-effects="reduced"] .fr-loadingbar::after { animation: none; }

/* ===== src/06-patterns.css ===== */
/* ════════════════════════════════════════════════════════════════════════
   FORTE UI · 06 · PATRONES
   ════════════════════════════════════════════════════════════════════════
   Composiciones de varios componentes que resuelven un problema completo:
   capas flotantes, selección, formulario largo y módulo de la home.

   Estas tres piezas están APROBADAS por el propietario y no se reabren
   (OWNER_REWORK_MATRIX_R3.csv): SELECTOR/CHOOSER, CAPAS y FORMULARIOS.
   ════════════════════════════════════════════════════════════════════════ */

/* ══════════════════════════════════════════════════════════════════════
   6.1 · CONTRATO DE CAPAS
   ══════════════════════════════════════════════════════════════════════
   DEFECTO ESTRUCTURAL QUE ESTE CONTRATO ELIMINA: un backdrop compartido
   por tres familias a alturas distintas (modal 2000, drawer 1900) hacía
   que el panel del drawer y el del sheet quedasen POR DEBAJO de su propio
   velo. Verificado con elementsFromPoint: no recibían puntero.

   CORRECCIÓN (no es «un z-index más alto»): cada capa es un elemento raíz
   `.fr-layer` con `isolation: isolate`. Velo y panel viven DENTRO de ese
   contexto de apilado, con z 0 y z 1. Un panel ya no puede caer bajo su
   propio velo: es geométricamente imposible. Entre familias sólo compite
   un número, --fr-layer-z.

   Corolario de gobierno: NINGÚN módulo escribe z-index. Si una pieza flota,
   es una capa; si es una capa, hereda su altura de esta escala. */
.fr-layer {
  position: fixed; inset: 0;
  z-index: var(--fr-layer-z, 1400);
  isolation: isolate;
}
.fr-layer[hidden] { display: none; }
.fr-layer > .fr-veil { position: absolute; inset: 0; z-index: 0; }
.fr-layer > *        { z-index: 1; }
.fr-layer--drawer   { --fr-layer-z: var(--fr-z-drawer); }
.fr-layer--sheet    { --fr-layer-z: var(--fr-z-sheet); }
.fr-layer--modal    { --fr-layer-z: var(--fr-z-modal); }
.fr-layer--chooser  { --fr-layer-z: var(--fr-z-chooser); }
.fr-layer--critical { --fr-layer-z: var(--fr-z-critical); }
.fr-layer--menu     { --fr-layer-z: var(--fr-z-dropdown); pointer-events: none; }
.fr-layer--toast    { --fr-layer-z: var(--fr-z-toast); pointer-events: none; }
.fr-layer--tooltip  { --fr-layer-z: var(--fr-z-tooltip); pointer-events: none; }
.fr-layer--menu > *,
.fr-layer--toast > *,
.fr-layer--tooltip > * { pointer-events: auto; }

/* Los paneles pasan a absolute DENTRO de la capa. */
.fr-layer > .fr-drawer { position: absolute; top: 0; right: 0; bottom: 0; }
.fr-layer > .fr-drawer--left { right: auto; left: 0; border-radius: 0 var(--fr-r-float) var(--fr-r-float) 0; }
.fr-layer > .fr-sheet  { position: absolute; left: 0; right: 0; bottom: 0; top: auto; }
.fr-layer > .fr-modal  { position: absolute; inset: 0; }
.fr-layer > .fr-menu   { position: absolute; }

/* Bloqueo de scroll de fondo mientras hay una capa modal abierta. */
.fr-scroll-locked { overflow: hidden; }

/* ── Modal ──────────────────────────────────────────────────────────── */
/* El marco de la capa respeta el recorte del dispositivo por los CUATRO
   lados. Por arriba y por los costados con `max()` —el relleno base ya
   basta cuando no hay notch—; por abajo tambien, porque la barra de gestos
   se come esa banda y el pie de la capa vive ahi.                        */
.fr-modal {
  display: grid; place-items: center;
  padding: var(--fr-pad-modal);
  padding-top: max(var(--fr-pad-modal), var(--fr-safe-top));
  padding-bottom: max(var(--fr-pad-modal), var(--fr-safe-bottom));
  padding-left: max(var(--fr-pad-modal), var(--fr-safe-left));
  padding-right: max(var(--fr-pad-modal), var(--fr-safe-right));
}
/* ANCHO ADAPTATIVO, no un numero escalado. `min(560px, 100%)` ya evitaba el
   desbordamiento, pero en una tableta de 820 px dejaba una capa de 560 en
   medio de 820: ni ancha ni estrecha. `clamp` le da un suelo util y un
   techo, y el `100%` sigue mandando cuando la ventana es mas pequena que el
   suelo. El alto se mide en `dvh`: con `vh` la barra de Safari deja la capa
   cortada por abajo hasta que el usuario hace scroll.                    */
.fr-modal-panel {
  display: flex; flex-direction: column; min-width: 0;
  width: min(var(--fr-modal-w, 560px), 100%);
  max-height: calc(100vh - 48px);
  /* `100%` DEL MARCO, no una resta del viewport. El marco ya descuenta su
     relleno y el recorte del dispositivo por los cuatro lados (arriba), así
     que restarle otra cantidad a `100dvh` es adivinar: donde el recorte
     inferior es mayor que el relleno —un iPhone: 34 contra 16— la capa salía
     más alta que la caja que la contiene y, al estar CENTRADA, se desbordaba
     por los dos extremos a partes iguales. Medido a 852×393: el pie de la
     capa terminaba 3 px dentro de la banda del indicador de inicio.
     `max-height: 100%` se mide contra el área de la rejilla, que ES el hueco
     real, y se recalcula solo con cualquier inset. */
  max-height: 100%;
  overflow: hidden;
}
/* El SUELO del `clamp` va por debajo del ancho de una tableta a proposito: un
   suelo de 760 en la variante grande haria que a 820 px la capa midiese 760 —o
   sea, lo mismo que antes— y el `clamp` no serviria de nada donde mas falta
   hace. Con 520 y 620 la capa crece con la ventana desde la tableta hasta el
   monitor, y en telefono manda el `min(..., 100%)` y luego el regimen de 576,
   que la lleva a pantalla completa. */
.fr-modal-panel--form  { --fr-modal-w: clamp(460px, 62vw, 780px); }
.fr-modal-panel--large { --fr-modal-w: clamp(620px, 82vw, 1160px); }
.fr-modal-head {
  display: flex; align-items: center; gap: var(--fr-s4);
  padding: var(--fr-s5) var(--fr-s6);
  border-bottom: 1px solid var(--fr-line-1);
  flex: 0 0 auto;
}
.fr-modal-head h2 { margin: 0; font-size: var(--fr-t-lead); font-weight: var(--fr-fw-bold); flex: 1 1 auto; min-width: 0; }
.fr-modal-kicker { display: block; font-size: var(--fr-t-micro); font-weight: var(--fr-fw-med); letter-spacing: var(--fr-ls-wide); text-transform: uppercase; color: var(--fr-text-3); }
.fr-modal-close {
  flex: 0 0 auto; width: 30px; height: 30px; border: 0; border-radius: var(--fr-r-ctl);
  background: transparent; color: var(--fr-text-3); cursor: pointer; line-height: 1;
  display: grid; place-items: center;
  transition: background var(--fr-d-micro) var(--fr-ease), color var(--fr-d-micro) var(--fr-ease);
}
.fr-modal-close:hover { background: var(--fr-hover); color: var(--fr-text); }
.fr-modal-body { padding: var(--fr-s6); overflow-y: auto; overscroll-behavior: contain; flex: 1 1 auto; min-height: 0; }
/* Pie de capa: `div`, nunca `<footer>` — ver nota de marcado en 04-layout. */
.fr-modal-foot {
  display: flex; align-items: center; justify-content: flex-end; gap: var(--fr-s4);
  padding: var(--fr-s5) var(--fr-s6);
  border-top: 1px solid var(--fr-line-1);
  background: rgba(22, 8, 10, 0.025);
  flex: 0 0 auto;
}
.fr-modal-foot .meta { margin-right: auto; font-size: var(--fr-t-micro); color: var(--fr-text-3); }
/* Subtipo destructivo: material M6 + cabecera con acento funcional. */
.fr-modal-panel--destructive .fr-modal-head { border-bottom-color: rgba(158, 43, 31, 0.25); }
.fr-modal-panel--destructive .fr-modal-head h2 { color: var(--fr-danger); }
@media (pointer: coarse) { .fr-modal-close { width: var(--fr-touch-min); height: var(--fr-touch-min); } }

/* ── Drawer y sheet ─────────────────────────────────────────────────── */
/* El ancho es un TOKEN del componente, no una constante escrita en la
   declaración: así una variante lo cambia en un sitio y todo lo derivado
   —el radio, la sombra de contacto, el comportamiento en móvil— sigue
   valiendo sin repetirse. */
.fr-drawer {
  --fr-drawer-w: min(420px, 92vw);
  width: var(--fr-drawer-w);
  display: flex; flex-direction: column;
  border-radius: var(--fr-r-float) 0 0 var(--fr-r-float);
  /* SUPERFICIE (SYS_GAP_04 - MOLDNEW-UX-02).
     `.fr-drawer` no declaraba fondo. Un modal SI lo declara -cada
     consumidor le pone `.fr-m4` a mano- pero al drawer no se lo puso
     nadie, y como `.fr-layer > .fr-drawer` es `absolute` sobre el velo,
     lo que se veia DENTRO del panel era el contenido de la pagina a
     traves de un velo de alfa 0,38. Medido en los tres consumidores del
     producto: `background-color: rgba(0, 0, 0, 0)` y
     `backdrop-filter: none` en Moldes, Maquinas y seg_mj.

     No se resuelve con vidrio: un drawer es una superficie donde se LEE
     y se OPERA, no un popover de paso. El vidrio se queda en el velo,
     que es la transicion; el panel es solido. M2 para el panel de
     contexto, M1 -superficie de trabajo- para la ficha tecnica. Sin
     `backdrop-filter` nuevo y sin fallback nuevo que mantener.

     La superficie va en `:where()`, con especificidad CERO, y no es un
     detalle de estilo: es lo que la convierte en un FALLBACK. `seg_mj`
     SI declara su material -`class="fr-drawer fr-m4 sm-drawer-panel"`- y
     `.fr-m4` vive en 03-materials, que el bundle carga ANTES que este
     fichero: con la misma especificidad ganaba el ultimo declarado y el
     gradiente translucido de ese panel se sustituia por un solido.
     Medido en vivo: `rgb(251, 250, 248)` con el `backdrop-filter` de M4
     todavia puesto y ya sin nada que desenfocar. Con especificidad cero,
     cualquier material declarado -y tambien `--work`- gana, y el
     fallback solo actua donde no hay ninguno. */
  /* Canto y profundidad de contacto: la capa se apoya, no flota sobre un
     vacío. Hairline en el borde que mira al contenido y sombra corta que
     sólo existe en los 6 px de contacto. */
  box-shadow: var(--fr-edge-in), var(--fr-edge-rim),
              -1px 0 0 rgba(22, 8, 10, 0.10),
              -8px 0 18px -10px rgba(22, 8, 10, 0.26),
              -28px 0 64px -28px rgba(22, 8, 10, 0.34);
}
:where(.fr-drawer) { background: var(--fr-raised); }
/* SUPERFICIE DE TRABAJO (SYS_GAP_03 · MAQ-UI-REFINE-01).
   El sistema tenía UN solo ancho de drawer, dimensionado para lo que un
   drawer suele ser: un panel de contexto con una columna de datos. Cuando
   lo que se abre es una FICHA TÉCNICA —pares clave/valor, matrices de
   señales, tableros de zonas—, 420 px la obligan a una sola columna y el
   resto del monitor se queda tras el velo. Medido en `Máquinas · Nueva
   estructura`: 420 px de panel y 362 de contenido útil en 1920, 1600 y
   1366 por igual, con la rejilla de datos colapsada a una columna.

   No es «un drawer más ancho»: es otra clase de superficie. Se dimensiona
   por PROPORCIÓN del viewport, con suelo y techo, para que siga dejando
   contexto visible del listado que hay detrás y no degenere en un modal
   pegado a un borde.
       1920 → 1180   1600 → 992   1366 → 847   1180 → 732   ≤900 → todo
   El techo de 1180 es el mismo umbral con el que el sistema decide que
   caben dos columnas de trabajo (RESPONSIVE.md).

   Genérica a propósito: la misma ficha existe en Moldes. Resolverla en la
   hoja de un módulo sería exactamente la isla que el contrato prohíbe. */
.fr-drawer--work {
  --fr-drawer-w: clamp(560px, 62vw, 1180px);
  background: var(--fr-work);
}

/* BANCO DE TRABAJO (SYS_GAP_05 · MOLDNEW-UX-03R).
   `--work` reparte el monitor entre el panel y el listado que queda detrás,
   y para una ficha de contexto eso es correcto. Pero hay superficies donde
   la EDICIÓN es la tarea principal, no una confirmación al margen: la ficha
   de un molde tiene 100 controles repartidos en once secciones, y en 1920
   `--work` le daba 1.180 px —el 61 % de la ventana— dejando 740 px de velo
   sobre un listado que en ese momento no se está leyendo.

   `--bench` es la misma superficie sólida con otro reparto: el fondo pasa a
   ser un canto de contexto, no media pantalla. El margen exterior se
   ESTRECHA a medida que el monitor se estrecha, porque lo que hay detrás
   vale cada vez menos:
       1920 → 1580 (0,82)   1600 → 1420 (0,89)   1366 → 1270 (0,93)
       1180 → 1124 (0,95)   ≤1023 → ventana menos un canto   ≤576 → todo
   El techo de 1.580 px existe para que una línea de formulario no se
   convierta en un renglón imposible de recorrer con la vista.

   Genérica a propósito: cualquier ficha de edición larga la puede consumir.
   Resolverla en la hoja de un módulo sería la isla que el contrato prohíbe. */
.fr-drawer--bench {
  /* El canto por debajo de 1024 es el MISMO que el de `--work` (16 px). Con
     24 px, una tableta de 820 perdia 8 px de superficie contra el estado
     anterior: una regresion pequena, pero medible, y el gate de espacio
     muerto la cazo. */
  /* `100vw` incluye la barra de scroll: en un escritorio con barra
     clasica de 15 px el cajon desbordaba la pagina por ese margen.
     `100%` mide el contenedor, que es lo que hay de verdad. */
  --fr-drawer-w: calc(100% - 16px);
  background: var(--fr-work);
}
@media (min-width: 1024px) { .fr-drawer--bench { --fr-drawer-w: calc(100% - 56px); } }
@media (min-width: 1280px) { .fr-drawer--bench { --fr-drawer-w: calc(100% - 96px); } }
@media (min-width: 1500px) { .fr-drawer--bench { --fr-drawer-w: calc(100% - 180px); } }
@media (min-width: 1800px) { .fr-drawer--bench { --fr-drawer-w: min(1580px, calc(100% - 300px)); } }

.fr-drawer--left {
  border-radius: 0 var(--fr-r-float) var(--fr-r-float) 0;
  box-shadow: var(--fr-edge-in), var(--fr-edge-rim),
              1px 0 0 rgba(22, 8, 10, 0.10),
              8px 0 18px -10px rgba(22, 8, 10, 0.26),
              28px 0 64px -28px rgba(22, 8, 10, 0.34);
}
.fr-drawer-head { display: flex; align-items: center; gap: var(--fr-s4); padding: var(--fr-s5) var(--fr-s6); border-bottom: 1px solid var(--fr-line-1); flex: 0 0 auto; }
.fr-drawer-head h2 { margin: 0; font-size: var(--fr-t-lead); font-weight: var(--fr-fw-bold); flex: 1; }
.fr-drawer-body { padding: var(--fr-s6); overflow-y: auto; overscroll-behavior: contain; flex: 1 1 auto; min-height: 0; }
.fr-drawer-foot {
  padding: var(--fr-s5) var(--fr-s6);
  padding-bottom: max(var(--fr-s5), var(--fr-safe-bottom));
  border-top: 1px solid var(--fr-line-1);
  display: flex; justify-content: flex-end; gap: var(--fr-s4); flex: 0 0 auto;
}

/* ── EL DRAWER TOCA EL BORDE SIEMPRE, NO SÓLO EN UN TELÉFONO ────────────
   `.fr-layer > .fr-drawer` es `absolute` con `top: 0`, `bottom: 0` y el
   costado anclado a 0, en TODOS los anchos: la lámina llega al borde de la
   ventana siempre. El recorte, en cambio, sólo se consumía por debajo de
   576 px, que es donde el panel pasa a ocupar la pantalla entera.

   Entremedias quedaba todo lo demás. En un iPad instalado como aplicación
   —`display: standalone`, que es como se usa en planta— el inset superior
   vale 24 px y la cabecera del drawer reservaba 12: el botón de CERRAR,
   arriba a la derecha, quedaba medio dentro de la barra de estado. Y en un
   teléfono APAISADO el inset lateral vale 59 px contra los 20 del relleno,
   con el mismo botón bajo la cámara.

   Se resuelve en la BASE y con `max()`: en un monitor los cuatro insets
   valen 0, gana el relleno de diseño y la geometría no se mueve un píxel.
   El costado es el ANCLADO —un drawer a la derecha no necesita apartarse
   del recorte izquierdo, que está a 420 px de él—; el bloque de 576 px se
   encarga de los dos cuando el panel ocupa el ancho entero.

   Se escribe con el ÁMBITO `.fr-layer > .fr-drawer >`, y no por comodidad de
   especificidad: es la condición real. Un `.fr-drawer` sólo toca el borde de
   la ventana cuando cuelga de una capa —ahí es donde §6.1 lo hace `absolute`
   con los costados a 0—. Además hace falta: la ficha de Moldes declara
   `.mne-overlay-root .mne-detail-panel__header { padding-block: 0.45rem }`,
   que con dos clases ganaba a `.fr-drawer-head` y dejaba el ajuste en nada.
   Medido: el botón de cerrar a 7 px del borde con 59 de recorte.         */
.fr-layer > .fr-drawer > .fr-drawer-head {
  padding-top: max(var(--fr-s5), var(--fr-safe-top));
}
.fr-layer > .fr-drawer > .fr-drawer-head,
.fr-layer > .fr-drawer > .fr-drawer-body,
.fr-layer > .fr-drawer > .fr-drawer-foot {
  padding-right: max(var(--fr-s6), var(--fr-safe-right));
}
.fr-layer > .fr-drawer--left > .fr-drawer-head,
.fr-layer > .fr-drawer--left > .fr-drawer-body,
.fr-layer > .fr-drawer--left > .fr-drawer-foot {
  padding-right: var(--fr-s6);
  padding-left: max(var(--fr-s6), var(--fr-safe-left));
}
/* El pie ya reservaba el recorte inferior, pero con su propio `max()`: se
   deja escrito aquí también para que el bloque entero se lea de una vez. */
.fr-layer > .fr-drawer > .fr-drawer-foot {
  padding-bottom: max(var(--fr-s5), var(--fr-safe-bottom));
}

.fr-sheet {
  max-height: 88vh; max-height: 88dvh; display: flex; flex-direction: column;
  border-radius: var(--fr-r-modal) var(--fr-r-modal) 0 0;
  padding-bottom: var(--fr-safe-bottom);
  box-shadow: var(--fr-edge-in), var(--fr-edge-rim),
              0 -1px 0 rgba(22, 8, 10, 0.10),
              0 -8px 18px -10px rgba(22, 8, 10, 0.26),
              0 -28px 64px -28px rgba(22, 8, 10, 0.34);
}
/* Asidero del sheet: barra de agarre, no adorno. Zona táctil de 26 px. */
/* El asidero es un CONTROL de borde a borde, y en un teléfono apaisado sus
   59 px de cada extremo caen bajo la cámara y la esquina redondeada. El
   agarre dibujado va centrado y se ve, pero el rectángulo táctil no puede
   entrar en el recorte (§5 del encargo: el rectángulo COMPLETO). Se aparta
   con margen —no con relleno—, porque lo que hay que mover es la caja, no su
   contenido. `width: auto` deja que el estirado del flex lo dimensione; con
   `100%` el margen lo sacaría de la hoja. */
.fr-sheet-handle {
  position: relative; width: auto; height: 26px;
  margin-block: 0;
  margin-inline: var(--fr-safe-left) var(--fr-safe-right);
  background: none; border: 0; display: block; cursor: grab; flex: 0 0 auto;
}
.fr-sheet-handle::before {
  content: ""; position: absolute; left: 50%; top: 11px;
  width: 44px; height: 4px; margin-left: -22px;
  border-radius: 2px; background: var(--fr-n300);
}
.fr-sheet-handle:active { cursor: grabbing; }
.fr-sheet-handle:active::before { background: var(--fr-n400); }
.fr-sheet-head {
  display: flex; align-items: center; gap: var(--fr-s4);
  padding: 0 var(--fr-s6) var(--fr-s4);
  border-bottom: 1px solid var(--fr-line-1); flex: 0 0 auto;
}
.fr-sheet-head h2 { margin: 0; font-size: var(--fr-t-base); font-weight: var(--fr-fw-bold); flex: 1 1 auto; min-width: 0; }
.fr-sheet-foot {
  flex: 0 0 auto; display: flex; gap: var(--fr-s4); justify-content: flex-end;
  padding: var(--fr-s4) var(--fr-s6);
  /* LA RESERVA INFERIOR LA PONE `.fr-sheet`, UNA VEZ. Aquí volvía a sumarse:
     34 px de la hoja + 8 + 34 del pie = 76 px de aire muerto bajo los
     botones en un iPhone, donde el contrato pide 34. Es exactamente la
     «triple reserva vertical» que esta fase vino a quitar. */
  border-top: 1px solid var(--fr-line-1);
}
/* La hoja ocupa el ancho entero, así que sus dos costados miran al recorte
   —un teléfono apaisado tiene inset a izquierda y derecha—. */
.fr-sheet-head,
.fr-sheet-foot,
.fr-sheet > .fr-drawer-body {
  padding-left: max(var(--fr-s6), var(--fr-safe-left));
  padding-right: max(var(--fr-s6), var(--fr-safe-right));
}
.fr-sheet .fr-drawer-body { padding-top: var(--fr-s4); }

/* ── Menú / popover ─────────────────────────────────────────────────── */
.fr-menu {
  min-width: 220px; max-height: var(--fr-menu-maxh); overflow-y: auto;
  padding: 6px;
}
.fr-menu-item {
  display: flex; align-items: center; gap: 9px;
  min-height: 30px; padding: 5px 9px; border-radius: 7px;
  font-size: var(--fr-t-small); color: var(--fr-text); cursor: pointer;
  transition: background var(--fr-d-micro) var(--fr-ease);
  border: 0; background: transparent; width: 100%; text-align: left; font-family: inherit;
}
.fr-menu-item:hover { background: rgba(0, 48, 59, 0.07); }
.fr-menu-item.is-active { background: var(--fr-teal); color: var(--fr-cream); }
.fr-menu-item .fr-icon { color: var(--fr-text-3); }
.fr-menu-item:hover .fr-icon { color: var(--fr-teal); }
.fr-menu-item.is-active .fr-icon { color: var(--fr-cream); }
.fr-menu-header  { padding: 6px 9px 2px; font-size: var(--fr-t-micro); font-weight: var(--fr-fw-bold); letter-spacing: var(--fr-ls-wide); text-transform: uppercase; color: var(--fr-text-3); }
.fr-menu-divider { height: 1px; background: var(--fr-line-1); margin: 5px 4px; }
/* SYS_GAP_02 — opcion destructiva. `.fr-menu-item` fija `color`, asi que la
   senal de peligro no se podia poner con una utilidad de texto: perdia por
   orden dentro del paquete y habia que colgarla de un <span> interior. */
.fr-menu-item--danger,
.fr-menu-item--danger .fr-icon { color: var(--fr-danger); }
.fr-menu-item--danger:hover { background: var(--fr-danger-soft); }
.fr-menu-item--danger:hover .fr-icon { color: var(--fr-danger); }
@media (pointer: coarse) { .fr-menu-item { min-height: var(--fr-touch-min); } }

.fr-autocomplete { position: relative; }
.fr-autocomplete .fr-menu { position: absolute; top: calc(100% + 4px); left: 0; right: 0; max-height: 260px; z-index: var(--fr-z-dropdown); }
.fr-autocomplete mark { background: rgba(0, 48, 59, 0.14); color: inherit; border-radius: 2px; padding: 0 1px; }

/* ── Tooltip y toast ────────────────────────────────────────────────── */
.fr-tooltip {
  display: inline-block; padding: 4px 9px; border-radius: 6px;
  background: rgba(22, 8, 10, 0.94); color: var(--fr-cream);
  font-size: var(--fr-t-mini); font-weight: var(--fr-fw-med); letter-spacing: 0.01em;
  box-shadow: var(--fr-sh-contact);
}
.fr-toast {
  display: flex; align-items: center; gap: 10px;
  min-width: 260px; max-width: 380px; padding: 11px 14px;
  border-radius: var(--fr-r-float); color: #fff;
  box-shadow: inset 0 1px 0 rgba(240, 238, 233, 0.16), var(--fr-sh-float);
  font-size: var(--fr-t-small);
}
.fr-toast--ok      { background: #2e6b3e; }
.fr-toast--danger  { background: #9e2b1f; }
.fr-toast--info    { background: #155a68; }
.fr-toast--warn    { background: #8a5800; }
.fr-toast--neutral { background: #3e3631; }
.fr-toast .t { font-weight: var(--fr-fw-bold); }
.fr-toast-stack {
  position: absolute; top: calc(var(--fr-topbar-offset) + var(--fr-s4));
  right: var(--fr-s6); display: flex; flex-direction: column; gap: var(--fr-s4);
}

/* ══════════════════════════════════════════════════════════════════════
   6.2 · CHOOSER — la selección como problema, no como widget
   ══════════════════════════════════════════════════════════════════════
   Un solo componente sirve al popover de escritorio, al modal de
   escritorio, al chooser táctil de tablet y al sheet o pantalla completa
   de móvil. La semántica es idéntica; sólo cambia el envoltorio.

   Accesibilidad implementada en forte-ui.js: role/dialog + listbox,
   aria-activedescendant, ↑ ↓ Home End Enter Escape, búsqueda, devolución
   de foco, opción deshabilitada y opción seleccionada.               */
.fr-chooser {
  display: flex; flex-direction: column; min-width: 0;
  width: min(560px, 100%);
  max-height: min(640px, calc(100vh - 96px));
  max-height: min(640px, calc(100dvh - 96px));
  overflow: hidden;
}
.fr-chooser--wide { width: min(760px, 100%); }
.fr-chooser-head {
  flex: 0 0 auto; display: flex; align-items: flex-start; gap: var(--fr-s4);
  padding: var(--fr-s5) var(--fr-s6) var(--fr-s4);
}
.fr-chooser-head .k { display: block; font-size: var(--fr-t-micro); font-weight: var(--fr-fw-bold); letter-spacing: var(--fr-ls-wide); text-transform: uppercase; color: var(--fr-text-3); }
.fr-chooser-head h2 { margin: 1px 0 0; font-size: var(--fr-t-lead); font-weight: var(--fr-fw-bold); }
.fr-chooser-head .ctx { margin: 3px 0 0; font-size: var(--fr-t-mini); color: var(--fr-text-2); }
.fr-chooser-search {
  flex: 0 0 auto; padding: 0 var(--fr-s6) var(--fr-s4);
  display: flex; gap: var(--fr-s4); align-items: center;
}
.fr-chooser-search .fr-search { flex: 1 1 auto; }
.fr-chooser-body {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  padding: 0 var(--fr-s3) var(--fr-s3);
  border-top: 1px solid var(--fr-line-1);
}
.fr-chooser-group {
  position: sticky; top: 0; z-index: 1;
  padding: var(--fr-s5) var(--fr-s4) var(--fr-s2);
  font-size: var(--fr-t-micro); font-weight: var(--fr-fw-bold);
  letter-spacing: var(--fr-ls-wide); text-transform: uppercase; color: var(--fr-text-3);
  background: linear-gradient(180deg, var(--fr-m6-solid) 62%, rgba(253, 252, 250, 0));
}
.fr-chooser-opt {
  position: relative; width: 100%; display: flex; align-items: center; gap: var(--fr-s5);
  min-height: 40px; padding: 7px var(--fr-s4) 7px var(--fr-s5);
  border: 0; border-radius: var(--fr-r-ctl); background: transparent;
  font-family: inherit; text-align: left; color: var(--fr-text); cursor: pointer;
  transition: background var(--fr-d-micro) var(--fr-ease);
}
.fr-chooser-opt:hover { background: var(--fr-hover); }
.fr-chooser-opt:focus-visible { outline: none; background: var(--fr-hover); box-shadow: var(--fr-focus-inset); }
.fr-chooser-opt.is-active   { background: var(--fr-active); }    /* foco de teclado */
.fr-chooser-opt.is-selected { background: var(--fr-selected); }
.fr-chooser-opt.is-selected::before {
  content: ""; position: absolute; left: 0; top: 5px; bottom: 5px;
  width: 2px; background: var(--fr-teal); border-radius: 1px;
}
.fr-chooser-opt[disabled], .fr-chooser-opt[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; }
.fr-chooser-opt .ref  { flex: 0 0 auto; min-width: 62px; font-size: var(--fr-t-small); font-weight: var(--fr-fw-bold); font-variant-numeric: tabular-nums; }
.fr-chooser-opt .txt  { flex: 1 1 auto; min-width: 0; }
.fr-chooser-opt .lbl  { display: block; font-size: var(--fr-t-small); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fr-chooser-opt .dsc  { display: block; font-size: var(--fr-t-micro); color: var(--fr-text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fr-chooser-opt .meta { flex: 0 0 auto; font-size: var(--fr-t-micro); color: var(--fr-text-3); font-variant-numeric: tabular-nums; text-align: right; }
.fr-chooser-opt .mark { flex: 0 0 auto; width: 18px; color: var(--fr-teal); opacity: 0; }
.fr-chooser-opt.is-selected .mark { opacity: 1; }
.fr-chooser-foot {
  flex: 0 0 auto; display: flex; align-items: center; gap: var(--fr-s4);
  padding: var(--fr-s4) var(--fr-s6);
  padding-bottom: calc(var(--fr-s4) + var(--fr-safe-bottom));
  border-top: 1px solid var(--fr-line-1);
  background: rgba(22, 8, 10, 0.025);
}
.fr-chooser-foot .count { margin-right: auto; font-size: var(--fr-t-micro); color: var(--fr-text-3); font-variant-numeric: tabular-nums; }
.fr-chooser-empty { padding: var(--fr-s9) var(--fr-s6); text-align: center; color: var(--fr-text-2); }
.fr-chooser-empty h3 { margin: var(--fr-s4) 0 2px; font-size: var(--fr-t-base); color: var(--fr-text); }
.fr-chooser-empty p  { margin: 0 0 var(--fr-s5); font-size: var(--fr-t-small); }
@media (pointer: coarse) {
  .fr-chooser-opt { min-height: 52px; }
  .fr-chooser-opt .lbl { font-size: var(--fr-t-base); }
}
/* Móvil: la misma semántica, otra presentación. Hasta 12 opciones el
   chooser ES un sheet: sube desde abajo, se alcanza con el pulgar y deja
   ver el contexto. A partir de ahí, pantalla completa: con muchas opciones
   lo que importa es la lista, no el fondo. */
@media (max-width: 576px) {
  .fr-layer--chooser > .fr-modal { align-items: end; padding: 0; }
  .fr-layer--chooser .fr-chooser { width: 100%; max-height: 86dvh; border-radius: var(--fr-r-modal) var(--fr-r-modal) 0 0; }
  .fr-layer--chooser[data-fr-chooser-mobile="full"] > .fr-modal { align-items: stretch; }
  .fr-layer--chooser[data-fr-chooser-mobile="full"] .fr-chooser { max-height: 100dvh; border-radius: 0; }
  .fr-layer--chooser > .fr-sheet .fr-chooser { width: 100%; max-height: none; }
}

/* Disparador: parece un campo, se comporta como un botón. */
.fr-picker {
  display: flex; align-items: center; gap: var(--fr-s4);
  width: 100%; min-height: 32px; padding: 0 var(--fr-s3) 0 var(--fr-s4);
  background: var(--fr-raised); border: 1px solid var(--fr-line-3);
  border-radius: var(--fr-r-ctl); cursor: pointer; text-align: left;
  font-family: inherit; font-size: var(--fr-t-small); color: var(--fr-text);
  transition: border-color var(--fr-d-micro) var(--fr-ease), background var(--fr-d-micro) var(--fr-ease);
}
.fr-picker:hover { border-color: rgba(22, 8, 10, 0.34); background: var(--fr-top); }
.fr-picker .val { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fr-picker .val b { font-weight: var(--fr-fw-bold); font-variant-numeric: tabular-nums; }
.fr-picker .val .sub { color: var(--fr-text-3); }
.fr-picker.is-empty .val { color: var(--fr-text-4); }
.fr-picker .fr-icon { color: var(--fr-text-3); }
.fr-picker:focus-visible { outline: none; border-color: var(--fr-teal); box-shadow: 0 0 0 3px var(--fr-focus-ring); }
@media (pointer: coarse) { .fr-picker { min-height: var(--fr-touch-min); font-size: var(--fr-t-lead); } }

/* Lista de radios con descripción — 3 a 6 opciones que necesitan explicarse. */
.fr-radiolist { display: grid; gap: var(--fr-s2); }
.fr-radiolist label {
  display: flex; gap: var(--fr-s5); align-items: flex-start;
  padding: var(--fr-s4) var(--fr-s5); cursor: pointer;
  border: 1px solid var(--fr-line-2); border-radius: var(--fr-r-ctl);
  background: var(--fr-raised);
  transition: border-color var(--fr-d-micro) var(--fr-ease), background var(--fr-d-micro) var(--fr-ease);
}
.fr-radiolist label:hover { border-color: var(--fr-line-3); }
.fr-radiolist label:has(input:checked) { border-color: var(--fr-teal); background: var(--fr-selected); }
.fr-radiolist input { margin-top: 2px; flex: 0 0 auto; }
.fr-radiolist .t { display: block; font-size: var(--fr-t-small); font-weight: var(--fr-fw-med); }
.fr-radiolist .d { display: block; font-size: var(--fr-t-micro); color: var(--fr-text-3); }

/* Selección por muestra — cuando la opción SE VE mejor de lo que se lee. */
.fr-tiles { display: grid; gap: var(--fr-s4); grid-template-columns: repeat(auto-fit, minmax(104px, 1fr)); }
.fr-tile {
  display: flex; flex-direction: column; gap: var(--fr-s3); align-items: center;
  padding: var(--fr-s4); cursor: pointer; text-align: center;
  border: 1px solid var(--fr-line-2); border-radius: var(--fr-r-card);
  background: var(--fr-raised); font-family: inherit;
  transition: border-color var(--fr-d-micro) var(--fr-ease), box-shadow var(--fr-d-micro) var(--fr-ease);
}
.fr-tile:hover { border-color: var(--fr-line-3); }
.fr-tile[aria-pressed="true"] { border-color: var(--fr-teal); box-shadow: inset 0 0 0 1px var(--fr-teal); background: var(--fr-selected); }
.fr-tile .sample { width: 100%; height: 46px; border: 1px solid var(--fr-line-2); border-radius: 3px; background: var(--fr-top); }
.fr-tile .t { font-size: var(--fr-t-mini); font-weight: var(--fr-fw-med); }

/* ══════════════════════════════════════════════════════════════════════
   6.3 · FORMULARIO LARGO
   ══════════════════════════════════════════════════════════════════════
   Bloques numerados y plegables con estado propio + navegación local que
   sustituye al `<select> Ir a un bloque`. Aprobado como KEEP_REFINED. */
.fr-form-grid { display: grid; gap: var(--fr-gap-form) var(--fr-pad-card); grid-template-columns: repeat(12, minmax(0, 1fr)); }
/* `width: auto` NO es decorativo. Bootstrap 5 declara `.col-1`…`.col-12` con
   `width: 33.333%`, `50%`, `100%`… y se carga globalmente en `base.html`.
   Dentro de una rejilla CSS ese porcentaje se resuelve contra el ÁREA de
   rejilla del elemento, así que un `.fr-field.col-4` ocupaba UN TERCIO de su
   propio hueco: 132 px medidos dentro de un área de 396 en 1366, y 49 px
   dentro de 147 en 393. El hueco sobraba, así que no había desbordamiento y
   ninguna métrica lo delataba; lo que había era un `select` y un campo de
   fecha inoperables en móvil. Afecta a las ONCE plantillas que usan la
   rejilla, no sólo a la familia que lo encontró (05B · ola 9).
   Con la misma especificidad (0,1,0) gana ésta por orden de carga:
   `forte-ui.css` va después de Bootstrap en `base.html`. */
.fr-form-grid > *          { grid-column: span 3; min-width: 0; width: auto; }
.fr-form-grid > .col-2     { grid-column: span 2; }
.fr-form-grid > .col-4     { grid-column: span 4; }
.fr-form-grid > .col-6     { grid-column: span 6; }
.fr-form-grid > .col-12    { grid-column: span 12; }

.fr-form-block { border: 1px solid var(--fr-line-2); border-radius: var(--fr-r-card); background: var(--fr-raised); margin-bottom: var(--fr-s5); }
.fr-form-block > summary,
.fr-form-block > .hd {
  display: flex; align-items: center; gap: var(--fr-s4);
  padding: var(--fr-s4) var(--fr-s5);
  cursor: pointer; list-style: none; border-radius: var(--fr-r-card);
  scroll-margin-block-start: calc(var(--fr-sticky-top) + 8px);
}
.fr-form-block > summary::-webkit-details-marker { display: none; }
.fr-form-block > summary:hover { background: var(--fr-hover); }
/* Un desplegable ES un control: se pulsa para abrirlo y para cerrarlo. Con
   el relleno estándar mide 36 px de alto —8 + 20 de línea + 8—, ocho por
   debajo del objetivo táctil. Medido a 393, 430 y 1366 px con puntero
   grueso en el registro técnico y en el bloque de advertencias de F18 (05B ·
   ola 10). Afecta a TODO formulario largo del producto, no sólo a la
   familia que lo encontró: es la misma corrección de política táctil que el
   sistema ya aplica a `.fr-btn`, `.fr-check`, `.fr-tab`, `.fr-menu-item`,
   `.fr-crumbs a` y `.fr-panel-row a`. Con ratón la geometría no cambia. */
@media (pointer: coarse) {
  .fr-form-block > summary,
  .fr-form-block > .hd { min-height: var(--fr-touch-min); }
}
.fr-form-block .num {
  flex: 0 0 auto; min-width: 22px; height: 20px; padding: 0 5px;
  display: grid; place-items: center; border-radius: 3px;
  background: var(--fr-n200); color: var(--fr-text-2);
  font-size: var(--fr-t-micro); font-weight: var(--fr-fw-bold); font-variant-numeric: tabular-nums;
}
.fr-form-block .ttl  { flex: 1 1 auto; min-width: 0; font-size: var(--fr-t-small); font-weight: var(--fr-fw-bold); letter-spacing: var(--fr-ls-wide); text-transform: uppercase; color: var(--fr-text-2); }
.fr-form-block .st   { flex: 0 0 auto; font-size: var(--fr-t-micro); color: var(--fr-text-3); font-variant-numeric: tabular-nums; }
.fr-form-block .chev { transition: transform var(--fr-d-state) var(--fr-ease); color: var(--fr-text-3); }
.fr-form-block[open] .chev { transform: rotate(180deg); }
.fr-form-block > .bd { padding: 0 var(--fr-s5) var(--fr-s5); border-top: 1px solid var(--fr-line-1); padding-top: var(--fr-s5); }
.fr-form-block.is-target { border-color: var(--fr-teal); box-shadow: inset 0 0 0 1px rgba(0, 48, 59, 0.22); }
.fr-form-block.has-error { border-color: rgba(158, 43, 31, 0.42); }
.fr-form-block.has-error .num { background: var(--fr-danger-soft); color: var(--fr-danger); }
.fr-form-block.is-empty .ttl { color: var(--fr-text-3); }

.fr-form-nav { position: sticky; top: calc(var(--fr-sticky-top) + 8px); align-self: start; }
.fr-form-nav ol { list-style: none; margin: 0; padding: 0; display: grid; gap: 1px; }
.fr-form-nav a {
  display: flex; align-items: center; gap: var(--fr-s4);
  padding: 6px var(--fr-s4); border-radius: var(--fr-r-ctl);
  font-size: var(--fr-t-mini); color: var(--fr-text-2); text-decoration: none;
  border-left: 2px solid transparent;
  transition: background var(--fr-d-micro) var(--fr-ease), color var(--fr-d-micro) var(--fr-ease);
}
.fr-form-nav a:hover { background: var(--fr-hover); color: var(--fr-text); text-decoration: none; }
.fr-form-nav a.is-current { background: var(--fr-selected); border-left-color: var(--fr-teal); color: var(--fr-text); font-weight: var(--fr-fw-med); }
.fr-form-nav .n { flex: 0 0 auto; min-width: 16px; color: var(--fr-text-3); font-variant-numeric: tabular-nums; }
.fr-form-nav .t { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fr-form-nav .d { flex: 0 0 auto; width: 7px; height: 7px; border-radius: 50%; background: var(--fr-n300); }
.fr-form-nav .d.ok   { background: var(--fr-ok); }
.fr-form-nav .d.err  { background: var(--fr-danger); }
.fr-form-nav .d.warn { background: var(--fr-warn); }
.fr-form-nav-head { padding: 0 var(--fr-s4) var(--fr-s3); font-size: var(--fr-t-micro); font-weight: var(--fr-fw-bold); letter-spacing: var(--fr-ls-wide); text-transform: uppercase; color: var(--fr-text-3); }
.fr-form-nav-progress { display: flex; align-items: center; gap: var(--fr-s3); padding: var(--fr-s3) var(--fr-s4) var(--fr-s5); font-size: var(--fr-t-micro); color: var(--fr-text-3); font-variant-numeric: tabular-nums; }
.fr-form-nav-progress .bar { flex: 1 1 auto; height: 3px; border-radius: 2px; background: var(--fr-n200); overflow: hidden; }
.fr-form-nav-progress .bar i { display: block; height: 100%; background: var(--fr-teal); }
.fr-form-layout { display: grid; grid-template-columns: 216px minmax(0, 1fr); gap: var(--fr-s8); align-items: start; }

/* ══════════════════════════════════════════════════════════════════════
   6.4 · MÓDULO DE LA HOME
   ══════════════════════════════════════════════════════════════════════
   El acento sale de `tarjeta.color` en BD, normalizado por el adaptador de
   gobierno de color (OD-3 · A): el módulo recibe `data-fr-accent` con una
   familia curada, nunca un HEX crudo. */
.fr-module {
  position: relative; display: flex; flex-direction: column; gap: var(--fr-s3);
  padding: var(--fr-s5) var(--fr-s5) var(--fr-s4);
  border: 1px solid var(--fr-line-2); border-radius: var(--fr-r-card);
  background: var(--fr-raised); color: inherit; text-decoration: none;
  transition: border-color var(--fr-d-micro) var(--fr-ease),
              box-shadow var(--fr-d-micro) var(--fr-ease),
              transform var(--fr-d-micro) var(--fr-ease);
}
.fr-module:hover  { text-decoration: none; border-color: var(--fr-line-3); box-shadow: var(--fr-sh-card); }
.fr-module:active { transform: translateY(0.5px); box-shadow: var(--fr-sh-contact); }
.fr-module:focus-visible { outline: none; box-shadow: var(--fr-focus); }
.fr-module-n {
  position: absolute; top: var(--fr-s4); right: var(--fr-s5);
  font-size: var(--fr-t-micro); font-weight: var(--fr-fw-bold);
  font-variant-numeric: tabular-nums; color: var(--fr-text-4);
}
.fr-module-ico { color: var(--fr-module-accent, var(--fr-teal)); }
.fr-module-t   { font-size: var(--fr-t-base); font-weight: var(--fr-fw-bold); line-height: 1.25; }
.fr-module-d   { font-size: var(--fr-t-mini); color: var(--fr-text-2); }
.fr-module-fig { display: flex; align-items: baseline; gap: 5px; }
.fr-module-fig b { font-size: var(--fr-t-h1); font-weight: var(--fr-fw-bold); line-height: 1; font-variant-numeric: tabular-nums; }
.fr-module-fig span { font-size: var(--fr-t-mini); color: var(--fr-text-3); }
.fr-module-go { margin-top: auto; display: flex; align-items: center; gap: 5px; padding-top: var(--fr-s3); font-size: var(--fr-t-mini); font-weight: var(--fr-fw-med); color: var(--fr-link); }
.fr-module:hover .fr-module-go { text-decoration: underline; text-underline-offset: 2px; }
.fr-module--inactive { background: var(--fr-work); border-style: dashed; cursor: default; }
.fr-module--inactive .fr-module-t,
.fr-module--inactive .fr-module-ico { color: var(--fr-text-3); }
.fr-module--inactive:hover { box-shadow: none; border-color: var(--fr-line-2); }
@media (pointer: coarse) { .fr-module { min-height: 92px; } }

/* Familias de acento autorizadas. Un valor de BD desconocido cae en
   `neutral` por el adaptador; aquí no hay HEX libre posible. */
.fr-module[data-fr-accent="teal"]    { --fr-module-accent: var(--fr-acc-teal-ink);    --fr-module-accent-soft: var(--fr-acc-teal-soft); }
.fr-module[data-fr-accent="red"]     { --fr-module-accent: var(--fr-acc-red-ink);     --fr-module-accent-soft: var(--fr-acc-red-soft); }
.fr-module[data-fr-accent="olive"]   { --fr-module-accent: var(--fr-acc-olive-ink);   --fr-module-accent-soft: var(--fr-acc-olive-soft); }
.fr-module[data-fr-accent="ink"]     { --fr-module-accent: var(--fr-acc-ink-ink);     --fr-module-accent-soft: var(--fr-acc-ink-soft); }
.fr-module[data-fr-accent="slate"]   { --fr-module-accent: var(--fr-acc-slate-ink);   --fr-module-accent-soft: var(--fr-acc-slate-soft); }
.fr-module[data-fr-accent="neutral"] { --fr-module-accent: var(--fr-acc-neutral-ink); --fr-module-accent-soft: var(--fr-acc-neutral-soft); }

/* Lista de acceso rápido: el mismo módulo cuando el espacio manda. */
.fr-modulerow {
  display: flex; align-items: center; gap: var(--fr-s5);
  padding: var(--fr-s4) var(--fr-s5); min-height: 52px;
  border-bottom: 1px solid var(--fr-line-1); color: inherit; text-decoration: none;
  transition: background var(--fr-d-micro) var(--fr-ease);
}
.fr-modulerow:last-child { border-bottom: 0; }
/* MISMA DELICADEZA QUE LA HOME. La fila de una capa tenía el hover gris
   genérico del sistema mientras la tarjeta de la que sale responde con el
   color de su familia: dos gramáticas para el mismo gesto. Aquí se usa la
   misma —fondo de familia al 6 % y filete que crece— y donde no hay familia
   declarada, el juego neutro de tokens la resuelve sola. */
.fr-modulerow {
  position: relative; border-radius: var(--fr-r-ctl);
  transition: background var(--fr-d-micro) var(--fr-ease),
              color var(--fr-d-micro) var(--fr-ease);
}
.fr-modulerow::before {
  content: ""; position: absolute; left: 2px; top: 6px; bottom: 6px;
  width: 2px; border-radius: 1px; background: var(--fr-mod-accent);
  transform: scaleY(0);
  transition: transform var(--fr-d-micro) var(--fr-ease);
}
.fr-modulerow:hover {
  background: rgba(var(--fr-mod-accent-rgb), 0.06); text-decoration: none;
}
.fr-modulerow:hover::before { transform: scaleY(1); }
.fr-modulerow:hover .fr-modcard-plate {
  background: rgba(var(--fr-mod-accent-rgb), 0.24);
}
@media (prefers-reduced-motion: reduce) {
  .fr-modulerow::before { transition: none; }
}
.fr-modulerow .n  { flex: 0 0 auto; width: 14px; font-size: var(--fr-t-micro); color: var(--fr-text-4); font-variant-numeric: tabular-nums; }
.fr-modulerow .tx { flex: 1 1 auto; min-width: 0; }
.fr-modulerow .tx b    { display: block; font-size: var(--fr-t-base); font-weight: var(--fr-fw-bold); }
.fr-modulerow .tx span {
  display: block; margin-top: 1px;
  font-size: var(--fr-t-mini); line-height: 1.4; color: var(--fr-text-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fr-modulerow .fig { flex: 0 0 auto; font-size: var(--fr-t-h3); font-weight: var(--fr-fw-bold); font-variant-numeric: tabular-nums; }

/* Franja de mando del escáner: la acción nº 1 de la planta. */
.fr-scanbar { display: flex; align-items: center; gap: var(--fr-s5); padding: var(--fr-s5) var(--fr-s6); border-radius: var(--fr-r-card); }
.fr-scanbar-id { display: flex; align-items: center; gap: var(--fr-s4); flex: 0 1 auto; min-width: 0; }
.fr-scanbar-id .k { font-size: var(--fr-t-micro); font-weight: var(--fr-fw-bold); letter-spacing: var(--fr-ls-wide); text-transform: uppercase; color: var(--fr-text-inv-2); }
.fr-scanbar-id .t { font-size: var(--fr-t-base); font-weight: var(--fr-fw-bold); color: var(--fr-cream); }
.fr-scanbar-form { display: flex; align-items: center; gap: var(--fr-s4); flex: 1 1 auto; min-width: 0; }
.fr-scanbar-form .fr-input {
  flex: 1 1 auto; min-width: 0; min-height: 40px;
  background: rgba(240, 238, 233, 0.94); border-color: transparent; color: var(--fr-ink);
  font-size: var(--fr-t-lead);
}
.fr-scanbar-form .fr-input::placeholder { color: var(--fr-text-4); }
.fr-scanbar-note { flex: 1 1 auto; min-width: 0; font-size: var(--fr-t-micro); color: var(--fr-text-inv-2); }
@media (pointer: coarse) { .fr-scanbar-form .fr-input { min-height: var(--fr-touch-min); } }

/* ══════════════════════════════════════════════════════════════════════
   6.5 · TARJETA DE MÓDULO · HOME V2
   ══════════════════════════════════════════════════════════════════════
   Implantación de R6B · V2 EQUILIBRADA, la variante aprobada por el
   propietario. Sustituye a `.fr-module` EN LA HOME; `.fr-module` sigue
   entero y sin tocar porque lo consumen las entradas de la capa de
   submódulos y otras pantallas.

   POR QUÉ ES UN PATRÓN Y NO CSS DE MÓDULO. Resuelve una composición
   completa —identidad, dato, accesos y acción— con seis piezas que
   cualquier pantalla de índice puede reutilizar. Lo que NO es reutilizable
   —la placa del título, la banda de utilidad, el pie de notas— se queda en
   la hoja de dominio de la home. §8.5: NEVER SOLVE A REUSABLE VISUAL
   PROBLEM LOCALLY, pero tampoco al revés.

   ANATOMÍA · dos planos, seis piezas

     ┌────────────────────────────────────────┐
     │ [placa]  MÓDULO · PERSONAL          02 │  fila de identidad
     │ Gestión de Personal                    │  título
     │ Control de recursos humanos            │  subtítulo
     │ ▌ 21  OPERARIOS                        │  dato ANCLADO al filete
     ├────────────────────────────────────────┤  el escalón (lo pone .fr-m7)
     │  Listado Operarios                 2.1 │
     │  Turnos                            2.2 │  accesos reales
     ├────────────────────────────────────────┤
     │  ABRIR →                    7 entradas │  pie
     └────────────────────────────────────────┘

   SEIS DECISIONES, cada una con su motivo:

   1  LA PLACA NO CRUZA LA FRONTERA. En R6 colgaba 24 px sobre el plano de
      contenido con un halo y obligaba a bajar todo lo de abajo para
      esquivarla: una cuarta parte del espacio útil gastada en un adorno.
      Aquí vive en su fila, con el antetítulo y el índice.
   2  LA CIFRA SE ANCLA a un filete de familia de 2 px. Deja de ser un
      número suelto y pasa a ser un dato con procedencia. Sólo aparece
      cuando el runtime tiene una métrica real: no se rellena con ceros.
   3  EL ANTETÍTULO DICE LA FAMILIA. Cuesta una línea y hace que la tarjeta
      se identifique aunque el color falle —daltonismo, impresión, pantalla
      mala—. La identidad cromática nunca es la única señal.
   4  LOS ACCESOS SON ENLACES DE VERDAD, no pastillas decorativas. Por eso
      la tarjeta es un `article` y no un `a`: un enlace dentro de otro
      enlace es marcado inválido, y era lo que impedía que `.fr-module`
      tuviera accesos. La acción principal la llevan dos superficies
      explícitas —la zona de identidad y el CTA—, ambas `button`.
   5  EL PIE SE SEPARA POR UNA LÍNEA, no por un cambio de fondo.
   6  ALTURA POR REJILLA, no fija. En el laboratorio la fila tenía altura
      declarada para que las tres variantes fuesen comparables al píxel; en
      producción el contenido es real y desigual —un módulo con dos accesos
      y otro con ninguno—, así que la iguala la rejilla y el pie se ancla
      con `margin-top: auto`.

   MATERIAL. Un solo translúcido por tarjeta: `.fr-m7` en el plano de
   contenido, que transmite el plano de identidad que tiene justo encima.
   El canto es de TINTA, no de luz: sobre lienzo claro un borde blanco no
   existe —medido en diez de doce materiales de R6—. */

/* CADA ROL NUEVO HAY QUE EMITIRLO AQUI TAMBIEN. Al pasar el plano a RGB
   —para poder servirlo con transmisión— se añadió `--fr-mod-<fam>-plane-rgb`
   a los tokens y se consumió `--fr-mod-plane-rgb` en la tarjeta, pero NO se
   emitió el alias en estos cinco bloques: las cuatro familias cayeron al
   plano NEUTRO de `:root` y la Home se quedó en gris. Sin error de consola,
   sin gate que se quejara, y sólo visible mirando.
   Es la misma trampa que R6B documentó con `--personal-acento`. */
[data-fr-mod="scan"] {
  --fr-mod-plane:      var(--fr-mod-scan-plane);
  --fr-mod-plane-rgb:  var(--fr-mod-scan-plane-rgb);
  --fr-mod-edge-rgb:   var(--fr-mod-scan-edge-rgb);
  --fr-mod-accent:     var(--fr-mod-scan-accent);
  --fr-mod-accent-rgb: var(--fr-mod-scan-accent-rgb);
  --fr-mod-graphite:   var(--fr-mod-scan-graphite);
  --fr-mod-ink:        var(--fr-mod-scan-ink);
  --fr-mod-plate-ink:  var(--fr-mod-scan-plate-ink);
}
[data-fr-mod="personal"] {
  --fr-mod-plane:      var(--fr-mod-personal-plane);
  --fr-mod-plane-rgb:  var(--fr-mod-personal-plane-rgb);
  --fr-mod-edge-rgb:   var(--fr-mod-personal-edge-rgb);
  --fr-mod-accent:     var(--fr-mod-personal-accent);
  --fr-mod-accent-rgb: var(--fr-mod-personal-accent-rgb);
  --fr-mod-graphite:   var(--fr-mod-personal-graphite);
  --fr-mod-ink:        var(--fr-mod-personal-ink);
  --fr-mod-plate-ink:  var(--fr-mod-personal-plate-ink);
}
[data-fr-mod="equipos"] {
  --fr-mod-plane:      var(--fr-mod-equipos-plane);
  --fr-mod-plane-rgb:  var(--fr-mod-equipos-plane-rgb);
  --fr-mod-edge-rgb:   var(--fr-mod-equipos-edge-rgb);
  --fr-mod-accent:     var(--fr-mod-equipos-accent);
  --fr-mod-accent-rgb: var(--fr-mod-equipos-accent-rgb);
  --fr-mod-graphite:   var(--fr-mod-equipos-graphite);
  --fr-mod-ink:        var(--fr-mod-equipos-ink);
  --fr-mod-plate-ink:  var(--fr-mod-equipos-plate-ink);
}
[data-fr-mod="control"] {
  --fr-mod-plane:      var(--fr-mod-control-plane);
  --fr-mod-plane-rgb:  var(--fr-mod-control-plane-rgb);
  --fr-mod-edge-rgb:   var(--fr-mod-control-edge-rgb);
  --fr-mod-accent:     var(--fr-mod-control-accent);
  --fr-mod-accent-rgb: var(--fr-mod-control-accent-rgb);
  --fr-mod-graphite:   var(--fr-mod-control-graphite);
  --fr-mod-ink:        var(--fr-mod-control-ink);
  --fr-mod-plate-ink:  var(--fr-mod-control-plate-ink);
}
[data-fr-mod="dev"] {
  --fr-mod-plane:      var(--fr-mod-dev-plane);
  --fr-mod-plane-rgb:  var(--fr-mod-dev-plane-rgb);
  --fr-mod-edge-rgb:   var(--fr-mod-dev-edge-rgb);
  --fr-mod-accent:     var(--fr-mod-dev-accent);
  --fr-mod-accent-rgb: var(--fr-mod-dev-accent-rgb);
  --fr-mod-graphite:   var(--fr-mod-dev-graphite);
  --fr-mod-ink:        var(--fr-mod-dev-ink);
  --fr-mod-plate-ink:  var(--fr-mod-dev-plate-ink);
}

/* ── la pieza ─────────────────────────────────────────────────────────── */
/* VIDRIO TINTADO PLANO, no «background-color bonito».
   La diferencia entre una lámina de policarbonato teñido y un rectángulo
   pastel no está en el color: está en cinco cosas que cuestan una
   declaración cada una y sólo se ven de cerca.

     TRANSMISIÓN   la cabecera se sirve al 94 % sobre el lienzo, no opaca. El
                   lienzo asoma lo justo para que la superficie tenga fondo.
     MICROCONTRASTE un vertical de amplitud MÍNIMA —10 % de luz arriba, 4,5 %
                   del filete de familia abajo—: entre 1,6 y 2,5 puntos de
                   claridad de arriba abajo. No es un degradado decorativo;
                   es lo que impide que la superficie se lea como pintura.
     CANTO ÓPTICO  1 px de luz al 62 % en el borde superior.
     INNER RIM     una luz de 1 px por DENTRO del marco, al 22 %. Apenas se
                   ve, y es lo que le da espesor al canto.
     CONTACTO      1 px de sombra, y una ambiente corta. Sin bloom.

   Nada de acuarela, radial-gradient, glow ni manchas: eso es exactamente lo
   que R6B retiró y no vuelve. */
.fr-modcard {
  position: relative; display: flex; flex-direction: column; min-width: 0;
  height: 100%; overflow: hidden;
  border-radius: var(--fr-r-float);
  background:
    linear-gradient(180deg,
      rgba(255, 255, 255, 0.10) 0,
      rgba(255, 255, 255, 0.015) 38%,
      rgba(var(--fr-mod-edge-rgb), 0.045) 100%),
    rgba(var(--fr-mod-plane-rgb), var(--fr-mod-tint, 0.94));
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.62),
    inset 0 2px 0 -1px rgba(255, 255, 255, 0.22),
    inset 0 0 0 1px rgba(var(--fr-ink-rgb), 0.085),
    inset 0 -1px 0 rgba(var(--fr-mod-edge-rgb), 0.34),
    0 1px 1px rgba(var(--fr-ink-rgb), 0.03),
    0 6px 14px -8px rgba(var(--fr-ink-rgb), 0.12);
  transition: transform var(--fr-d-state) var(--fr-ease),
              box-shadow var(--fr-d-state) var(--fr-ease);
}
/* Hover: 1 px y el canto de familia sube de 0,34 a 0,52. NO aparece ningún
   brillo — una banda especular que se desplaza es exactamente el reflejo
   llamativo que esta dirección vino a quitar. */
/* En hover cambia el MATERIAL, no la geometría: sube el canto de familia,
   se abre un punto el rim de luz y la sombra gana contacto. Un píxel de
   desplazamiento y nada más. */
.fr-modcard:hover {
  transform: translateY(-1px);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.74),
    inset 0 2px 0 -1px rgba(255, 255, 255, 0.30),
    inset 0 0 0 1px rgba(var(--fr-ink-rgb), 0.085),
    inset 0 -2px 0 -1px rgba(var(--fr-mod-edge-rgb), 0.52),
    0 1px 2px rgba(var(--fr-ink-rgb), 0.05),
    0 10px 22px -10px rgba(var(--fr-ink-rgb), 0.17);
}
.fr-modcard:active { transform: translateY(0); }

/* ── plano de identidad — es la superficie de acción principal ────────── */
/* EL ESCALON ES UNA LINEA HORIZONTAL QUE ALINEA LAS CUATRO TARJETAS, y
   por eso tiene que caer a la MISMA altura en todas. Medido antes de esta
   corrección: 154 px en las que tienen cifra —Personal y Equipos— y 113 en
   las que no. Cuarenta y un píxeles de desalineación en la pieza principal
   de la pantalla, y se ve a simple vista.

   No se arregla dándole una cifra a todas —los módulos 4 y 5 no tienen
   métrica real y no se les inventa una— sino dando al plano de identidad
   ALTURA DEFINIDA. Es la misma doctrina que R6B ya había aprendido con la
   fila: una composición cuyo alto depende de lo que le toque dentro no es
   una composición.

   El reparto interior lo hace `justify-content`: el bloque de título arriba
   y la cifra pegada al escalón, anclada a su filete, que es donde tiene que
   estar. En las tarjetas sin cifra ese espacio queda como AIRE bajo el
   subtítulo, no como hueco muerto: el título respira igual en las cuatro. */
.fr-modcard-id {
  /* ABSORBE EL SOBRANTE. La rejilla estira las cuatro tarjetas a la altura
     de la más alta, y el cuerpo mide siempre lo mismo; si la cabecera no se
     quedara con la diferencia, el hueco caería DESPUÉS del cuerpo y el
     escalón subiría en las tarjetas de subtítulo corto. Medido: 148, 150 y
     166 px en la misma fila. Con el sobrante aquí, la frontera cae a
     `alto − cuerpo` y por tanto a la misma altura en todas. */
  flex: 1 1 auto; width: 100%; text-align: left;
  display: flex; flex-direction: column;
  min-height: var(--fr-modcard-id-h, 148px);
  padding: var(--fr-s5) var(--fr-s6) var(--fr-s6);
  border: 0; background: transparent; color: inherit;
  font-family: inherit; cursor: pointer;
}
.fr-modcard-id:focus-visible { outline: none; box-shadow: var(--fr-focus-inset); }
/* LA CABECERA DE LA TARJETA, REPLANTEADA (V2D).
   Antes eran TRES filas: la placa con el antetítulo y el índice; el título;
   el subtítulo. Y el antetítulo decía «MÓDULO · PERSONAL» al lado de un
   título que decía «Gestión de Personal»: la misma información dos veces,
   ocupando una línea entera.

   Ahora la identidad va junta —placa, familia y título en un solo bloque— y
   el subtítulo debajo, a ancho completo. Una línea menos, la jerarquía más
   clara y la placa puede crecer a 44 px sin robar altura, que es lo que le
   da presencia. */
.fr-modcard-idrow {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: start; gap: var(--fr-s5);
}
.fr-modcard-name { min-width: 0; }
.fr-modcard-name i {
  display: block; font-style: normal;
  font-size: 10px; font-weight: var(--fr-fw-med);
  letter-spacing: 0.13em; text-transform: uppercase;
  color: var(--fr-text-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* EL TITULO DOMINA. Estaba a 14 px, el mismo cuerpo que el texto de un
   acceso: en una tarjeta cuyo trabajo es identificar un módulo, el nombre
   del módulo no puede pesar lo mismo que su tercer enlace. */
.fr-modcard-name b {
  display: block; margin-top: 2px;
  font-size: var(--fr-t-h3); font-weight: var(--fr-fw-bold);
  letter-spacing: -0.012em; line-height: 1.2; color: var(--fr-mod-ink);
}
/* EL SUBTITULO RESPIRA: más aire arriba y una línea más suelta. */
.fr-modcard-d {
  display: block; margin-top: var(--fr-s6);
  font-size: var(--fr-t-small); font-weight: var(--fr-fw-reg);
  line-height: 1.55; color: var(--fr-text-2);
}
/* SIN CIFRA REAL no se simula ninguna: se resuelve por COMPOSICIÓN. El
   subtítulo recibe el aire que la cifra habría ocupado y sube medio cuerpo,
   así que el bloque de texto queda equilibrado en vez de dejar un hueco al
   final. Control Operativo y En Desarrollo no inventan un número. */
.fr-modcard--nofig .fr-modcard-d {
  margin-top: var(--fr-s7); font-size: var(--fr-t-base); line-height: 1.5;
}
/* EL ICONO NO ES UN CUADRADO PUESTO ENCIMA. Era una placa de acento macizo
   con su propio canto de tinta: un objeto ajeno posado sobre la superficie.
   Ahora es la MISMA superficie, teñida un 18 % más con el acento de su
   familia, y el glifo va en grafito.

   El glifo tuvo que dejar de ser el acento: sobre su propia placa medía
   2,41–2,64:1 y WCAG 1.4.11 pide 3,0 para un componente no textual. En
   grafito da 4,20–4,87. Un icono a 2,5 no se ve, por bonito que quede en
   una captura. */
.fr-modcard-plate {
  flex: 0 0 auto; width: 44px; height: 44px;
  transition: box-shadow var(--fr-d-micro) var(--fr-ease),
              background var(--fr-d-micro) var(--fr-ease);
  display: grid; place-items: center;
  border-radius: 10px;
  background: rgba(var(--fr-mod-accent-rgb), 0.18);
  color: var(--fr-mod-graphite);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.34),
              inset 0 0 0 1px rgba(var(--fr-mod-accent-rgb), 0.20);
}
.fr-modcard-plate .fr-icon { --fr-icon-size: 22px; }
/* Al pasar el dedo o el ratón por la tarjeta, la placa recibe un punto de
   luz y apoya. Es un cambio de UNA sombra: ni escala, ni brillo, ni glow. */
.fr-modcard:hover .fr-modcard-plate {
  background: rgba(var(--fr-mod-accent-rgb), 0.24);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.44),
              inset 0 0 0 1px rgba(var(--fr-mod-accent-rgb), 0.30);
}
/* LA ESCALA DE TINTA DEL SISTEMA ESTÁ CALIBRADA SOBRE PAPEL, NO SOBRE UN
   PLANO DE COLOR. `--fr-text-3` y `--fr-text-4` se eligieron contra
   `--fr-raised` (#fbfaf8); sobre el plano de identidad de una familia
   —L 84–91 y croma 8–25— el mismo gris rinde MUCHO menos. Medido:

     sobre el plano de identidad   text-4  1,99–2,40   text-3  3,63–4,39
     sobre el plano de contenido   text-4  2,64–2,81   text-3  4,82–5,13

   Es decir: el antetítulo y el índice, que en el mockup iban al 42 % y al
   30 % de tinta, no llegaban a AA en NINGUNA familia. Y no se ve mirando:
   se ve midiendo contra el fondo COMPUESTO, porque preguntarle su
   `background-color` a un elemento translúcido devuelve el suyo, no lo que
   el ojo tiene delante.

   El metadato pasa a `--fr-text-2` (6,87–9,72 en claro, 6,33–8,10 en
   oscuro). La jerarquía no se pierde: la sostienen el TAMAÑO y el PESO
   —11 px versalita contra 14 px de título—, que es donde debía estar. */
.fr-modcard-fam {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--fr-t-micro); font-weight: var(--fr-fw-bold);
  letter-spacing: var(--fr-ls-wide); text-transform: uppercase;
  color: var(--fr-text-2);
}
/* EL NUMERO DE MODULO ES METADATA, no un titular. Estaba en versalita
   negrita del mismo cuerpo que la familia y competía con ella. Baja a peso
   medio y a la altura de la línea de familia, que es su sitio: dice cuál es
   el módulo, no qué es. Mantiene --fr-text-2 porque bajarlo más lo sacaría
   de AA sobre el plano. */
.fr-modcard-n {
  padding-top: 1px;
  font-size: 10px; font-weight: var(--fr-fw-med); letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums; color: var(--fr-text-2);
}
/* `.fr-modcard-t` se conserva para quien ya lo consuma; la Home usa
   `.fr-modcard-name` desde V2D. */
.fr-modcard-t { display: block; margin-top: var(--fr-s5); }
/* El sobrante empuja la CIFRA hacia el escalón, nunca el título.
   Con `:last-child` incluido —que es como estaba— en las tarjetas sin cifra
   el título ERA el último hijo y se iba al centro: «Control Operativo» y «En
   Desarrollo» flotaban 157 px por debajo de los otros dos. Cuatro tarjetas
   con el titular a dos alturas distintas. */
.fr-modcard-fig { margin-top: auto; }
.fr-modcard-t b {
  display: block; font-size: var(--fr-t-base); font-weight: var(--fr-fw-bold);
  line-height: var(--fr-lh-dense); color: var(--fr-mod-ink);
}
.fr-modcard-t i {
  display: block; font-style: normal; margin-top: var(--fr-s1);
  font-size: var(--fr-t-mini); font-weight: var(--fr-fw-reg);
  line-height: var(--fr-lh-dense); color: var(--fr-text-2);
}
/* El dato, anclado a su filete. */
.fr-modcard-fig {
  position: relative; display: flex; align-items: baseline; gap: var(--fr-s4);
  padding-left: var(--fr-s5); padding-top: var(--fr-s5);
}
/* EL FILETE ES EL ANCLA. A 2 px se leía como un adorno; a 3, con la cifra
   pegada a él, el número deja de ser «otra línea de texto» y pasa a ser un
   dato con procedencia. */
.fr-modcard-fig::before {
  content: ""; position: absolute; left: 0; top: 3px; bottom: 4px;
  width: 3px; border-radius: 1.5px; background: var(--fr-mod-accent);
}
.fr-modcard-fig b {
  font-size: 30px; font-weight: var(--fr-fw-light);
  letter-spacing: -0.035em; line-height: 1;
  font-variant-numeric: tabular-nums; color: var(--fr-mod-ink);
}
.fr-modcard-fig i {
  font-style: normal; font-size: 10px; font-weight: var(--fr-fw-bold);
  letter-spacing: 0.13em; text-transform: uppercase;
  color: var(--fr-text-2);
}

/* ── plano de contenido — lleva .fr-m7 en el marcado ──────────────────── */
/* ALTURA DEFINIDA, y por eso el escalon queda alineado SIEMPRE: la frontera
   cae a `alto de la tarjeta - alto del cuerpo`, y el cuerpo mide lo mismo
   tenga dos accesos, uno o ninguno. Dos accesos de 34, el pie de 44 y su
   relleno: 140. */
.fr-modcard-body {
  position: relative; flex: 0 0 auto;
  display: flex; flex-direction: column;
  padding: var(--fr-s5) var(--fr-s6) var(--fr-s6);
}
/* TRES FILAS DE ALTO, aunque el módulo traiga menos entradas.
   Es lo que mantiene el escalón alineado en las cuatro tarjetas sin
   inventar contenido: el cuerpo mide siempre lo mismo y la frontera cae a
   `alto - cuerpo`. Un módulo con dos submódulos enseña dos y deja el resto
   en blanco; lo que no puede es medir distinto que el de al lado. */
/* LA ALTURA DE FILA ES UN TOKEN, NO UN 36 ESCRITO A MANO. La reserva de la
   lista se calcula como N filas, y con el dedo la fila mide 44, no 36. Estaba
   escrito con el numero: en una tableta las listas de cuatro accesos median
   176 px y la de tres se quedaba en la reserva de 144, asi que la cabecera de
   Control absorbia 32 px de mas y su escalon caia a 180 mientras el de las
   otras tres caia a 150. Se veia. */
.fr-modcard-links {
  --fr-modlink-h: 36px;
  display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none;
  min-height: calc(3 * var(--fr-modlink-h));
}
/* Un acceso es un `a` cuando navega y un `button` cuando abre una capa.
   Las dos formas tienen que verse igual, así que el componente neutraliza
   la caja del botón en vez de obligar a un marcado que mentiría. */
.fr-modcard-link {
  display: flex; align-items: center; gap: var(--fr-s4); width: 100%;
  min-height: var(--fr-modlink-h, 36px); color: var(--fr-text-2); text-decoration: none;
  border: 0; background: transparent; text-align: left; cursor: pointer;
  font-family: inherit;
  font-size: var(--fr-t-small); font-weight: var(--fr-fw-reg);
  box-shadow: inset 0 -1px 0 rgba(var(--fr-ink-rgb), 0.042);
  transition: color var(--fr-d-micro) var(--fr-ease);
}
.fr-modcard-links li:last-child .fr-modcard-link { box-shadow: none; }

/* EL HOVER DE LA FILA. Antes sólo oscurecía el texto y no se notaba: una
   lista de accesos que no responde se lee como una lista de etiquetas.
   Aquí aparecen tres cosas a la vez, y ninguna grita:

     · un fondo del acento de la familia al 6 % — medido, dE2000 1,95–3,30
       contra el reposo: se ve, no salta;
     · un filete de familia de 2 px que CRECE desde el centro, no que
       aparece de golpe;
     · el icono, que pasa de tinta terciaria al acento.

   El fondo sangra hasta el borde del relleno de la tarjeta con márgenes
   negativos, para que la fila se lea como una banda y no como una pastilla
   flotando dentro de otra caja. */
.fr-modcard-link {
  position: relative;
  margin-inline: calc(var(--fr-s5) * -1);
  padding-inline: var(--fr-s5);
  border-radius: var(--fr-r-ctl);
  transition: color var(--fr-d-micro) var(--fr-ease),
              background var(--fr-d-micro) var(--fr-ease);
}
.fr-modcard-link::before {
  content: ""; position: absolute; left: 2px; top: 5px; bottom: 5px;
  width: 2px; border-radius: 1px; background: var(--fr-mod-accent);
  transform: scaleY(0);
  transition: transform var(--fr-d-micro) var(--fr-ease);
}
.fr-modcard-link:hover {
  color: var(--fr-text); text-decoration: none;
  background: rgba(var(--fr-mod-accent-rgb), 0.06);
}
.fr-modcard-link:hover::before { transform: scaleY(1); }
.fr-modcard-link:hover .fr-icon { color: var(--fr-mod-accent); }
.fr-modcard-link:hover > em { color: var(--fr-text-2); }
.fr-modcard-link:focus-visible { outline: none; box-shadow: var(--fr-focus-inset); }
.fr-modcard-link .fr-icon {
  --fr-icon-size: 14px; color: var(--fr-text-3); flex: 0 0 auto;
  transition: color var(--fr-d-micro) var(--fr-ease);
}
@media (prefers-reduced-motion: reduce) {
  .fr-modcard-link::before { transition: none; }
}
.fr-modcard-link > span { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* LA REFERENCIA ES CASI METADATA: visible, nunca compitiendo con el título
   del acceso. Deja el peso negrita —que la ponía al nivel del nombre— y se
   queda en medio, con su cifra tabular. */
.fr-modcard-link > em {
  flex: 0 0 auto; font-style: normal; font-size: 10px;
  font-weight: var(--fr-fw-med); letter-spacing: 0.03em;
  font-variant-numeric: tabular-nums; color: var(--fr-text-2);
  transition: color var(--fr-d-micro) var(--fr-ease);
}
.fr-modcard-foot {
  margin-top: auto; padding-top: var(--fr-s4);
  display: flex; align-items: center; gap: var(--fr-s5);
  border-top: 1px solid var(--fr-line-1);
}
/* «ABRIR» NO ES UN BOTON CONVENCIONAL. Con fondo y borde parecía un
   control primario dentro de una tarjeta que ya tiene cuatro enlaces: dos
   niveles de acción compitiendo en 40 px de alto. Ahora es una palabra y una
   flecha, y lo único que aparece al pasar es un filete del acento que se
   despliega de izquierda a derecha. Cero sombra. */
.fr-modcard-cta {
  position: relative;
  display: inline-flex; align-items: center; gap: var(--fr-s4);
  min-height: 28px; padding: 0 var(--fr-s4) 0 0;
  border: 0; background: transparent; box-shadow: none;
  font-family: inherit; font-size: var(--fr-t-micro); font-weight: var(--fr-fw-bold);
  letter-spacing: var(--fr-ls-wide); text-transform: uppercase; cursor: pointer;
  color: var(--fr-mod-graphite);
  transition: color var(--fr-d-micro) var(--fr-ease);
}
.fr-modcard-cta::after {
  content: ""; position: absolute; left: 0; right: var(--fr-s4); bottom: 3px;
  height: 1px; background: rgba(var(--fr-mod-accent-rgb), 0.42);
  transform: scaleX(0); transform-origin: left;
  transition: transform var(--fr-d-state) var(--fr-ease);
}
.fr-modcard-cta:hover::after { transform: scaleX(1); }
@media (prefers-reduced-motion: reduce) {
  .fr-modcard-cta::after { transition: none; }
}
.fr-modcard-cta:focus-visible { outline: none; box-shadow: var(--fr-focus); }
/* Dos píxeles. La flecha de un «abrir» que no se mueve es un dibujo; la que
   avanza dice hacia dónde va. Es el detalle más barato del conjunto. */
.fr-modcard-cta .fr-icon {
  --fr-icon-size: 12px;
  transition: transform var(--fr-d-micro) var(--fr-ease);
}
.fr-modcard-cta:hover .fr-icon { transform: translateX(2px); }
@media (prefers-reduced-motion: reduce) {
  .fr-modcard-cta:hover .fr-icon { transform: none; }
}
.fr-modcard-meta {
  margin-left: auto; font-size: var(--fr-t-micro); font-weight: var(--fr-fw-med);
  font-variant-numeric: tabular-nums; color: var(--fr-text-2);
}

/* Módulo sin destino operativo: se ve, se lee y no promete un clic. */
.fr-modcard--inactive .fr-modcard-id { cursor: default; }
.fr-modcard--inactive .fr-modcard-cta { display: none; }

/* 44, no 40. El sistema tiene un objetivo táctil y no es negociable:
   `--fr-touch-min`. Los 40 px venían del mockup, que se dibujó a escala
   1:1 sobre un lienzo sin dedos. El arnés lo midió en los seis viewports
   de puntero grueso. */
@media (pointer: coarse) {
  .fr-modcard-links { --fr-modlink-h: var(--fr-touch-min); }
  /* El CTA pierde la caja pero NO el objetivo táctil. */
  .fr-modcard-cta  { min-height: var(--fr-touch-min); }
}
@media (prefers-reduced-motion: reduce) {
  .fr-modcard { transition: none; }
  .fr-modcard:hover { transform: none; }
}

/* ── banda de utilidad: escáner + búsqueda ────────────────────────────── */
/* EL REPARTO SE INVIERTE respecto de R6, y ese es el cambio que de verdad
   mueve la jerarquía: el escáner pasa a ser la pieza ESTRECHA (0,92fr) y la
   búsqueda la ancha (1,3fr). Bajarle la altura no bastaba —R6 ya lo había
   hecho— porque seguía ocupando más ancho que nada. */
.fr-utilbar {
  display: grid; gap: var(--fr-s5);
  grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.3fr);
  align-items: stretch;
}
/* Un usuario sin permiso sobre el escáner ve la banda con una sola pieza:
   ocupa el ancho entero en vez de quedarse a media caja con un hueco. */
.fr-utilbar > :only-child { grid-column: 1 / -1; }

/* Variante clara de la franja de mando: mismo componente, material de
   familia en vez de tinta. La oscura sigue viva en sus consumidores. */
/* LA BANDA DEL ESCÁNER CRECE. Con 52 px era un renglón: la acción nº 1 de
   la planta leyéndose como un pie de página. Sube a ~84 con la placa de 44 y
   una tercera línea que recupera la descripción real de la tarjeta —la que
   V2 había mandado a la capa—. Sigue siendo la pieza BAJA de la pantalla:
   una banda ancha y baja no compite con cuatro tarjetas altas. */
.fr-scanbar--mod {
  gap: var(--fr-s6); padding: var(--fr-s5) var(--fr-s6);
  /* La MISMA receta material que la tarjeta: si la Home tiene una sola
     lámina, la franja es la misma lámina en horizontal. */
  background:
    linear-gradient(180deg,
      rgba(255, 255, 255, 0.10) 0,
      rgba(255, 255, 255, 0.015) 38%,
      rgba(var(--fr-mod-edge-rgb), 0.045) 100%),
    rgba(var(--fr-mod-plane-rgb), var(--fr-mod-tint, 0.94));
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.62),
    inset 0 2px 0 -1px rgba(255, 255, 255, 0.22),
    inset 0 0 0 1px rgba(var(--fr-ink-rgb), 0.085),
    inset 0 -1px 0 rgba(var(--fr-mod-edge-rgb), 0.26),
    0 1px 1px rgba(var(--fr-ink-rgb), 0.03),
    0 6px 14px -8px rgba(var(--fr-ink-rgb), 0.12);
}
.fr-scanbar--mod .fr-scanbar-id { flex: 1 1 auto; }
/* Misma razón que en la tarjeta: `--fr-text-3` sobre el plano de la familia
   scan mide 3,74:1 y no llega a AA. Sobre papel habría bastado. */
.fr-scanbar--mod .fr-scanbar-id .k { color: var(--fr-text-2); }
.fr-scanbar--mod .fr-scanbar-id .t { color: var(--fr-mod-ink); }
.fr-scanbar--mod .fr-scanbar-note  { flex: 0 0 auto; color: var(--fr-text-3); }
.fr-scanbar--mod .fr-scanbar-id { gap: var(--fr-s5); }
.fr-scanbar--mod .fr-modcard-plate { width: 44px; height: 44px; }
.fr-scanbar-tx { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.fr-scanbar--mod .fr-scanbar-id .k { font-size: 10px; font-weight: var(--fr-fw-med); letter-spacing: 0.13em; }
.fr-scanbar--mod .fr-scanbar-id .t { font-size: var(--fr-t-h3); letter-spacing: -0.01em; }
.fr-scanbar-tx .k, .fr-scanbar-tx .t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fr-scanbar-tx .d {
  font-size: var(--fr-t-mini); color: var(--fr-text-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* EL ÚNICO VIDRIO NUEVO, y es del tamaño de un botón. La lámina va sobre el
   plano de familia que tiene detrás —no sobre un fondo lejano—, que es la
   misma transmisión que usa la tarjeta. `.fr-m7` trae el `backdrop-filter`
   desde el material system; aquí sólo se reclama el fondo, porque
   `.fr-btn--secondary` se declara antes y ganaría por orden de carga. */
.fr-scanbar--mod .fr-btn.fr-m7 {
  background: var(--fr-m7-bg);
  border-color: transparent;
  color: var(--fr-mod-graphite);
  /* El canto, del ACENTO de la familia y muy bajo. Con tinta neutra al 10 %
     sobre un plano rosa se leía como un recuadro pegado encima; con el
     acento al 14 % el botón pertenece a la banda en vez de posarse en ella. */
  box-shadow: inset 0 0 0 1px rgba(var(--fr-mod-accent-rgb), 0.14),
              inset 0 1px 0 rgba(255, 255, 255, 0.50),
              0 1px 2px rgba(var(--fr-ink-rgb), 0.04);
}
.fr-scanbar--mod .fr-btn.fr-m7:hover {
  box-shadow: inset 0 0 0 1px rgba(var(--fr-mod-accent-rgb), 0.30),
              inset 0 1px 0 rgba(255, 255, 255, 0.60),
              0 2px 6px rgba(var(--fr-ink-rgb), 0.07);
}
.fr-scanbar--mod .fr-btn.fr-m7 .fr-icon { color: var(--fr-mod-accent); }

/* `.fr-picker--find` —el disparador de la búsqueda de la Home— SE RETIRA.
   El propietario la quitó de la pantalla y su único consumidor era ésta;
   dejarla aquí sería exactamente la deuda que el contrato prohíbe: una
   primitiva sin nadie que la use, esperando a que alguien la reutilice mal.
   Cuando una pieza se rehace, la anterior se retira. */

/* Entrada de una lista de acceso: el indice puede ser `3.17`, no cabe en
   catorce pixeles. Se le da suelo y que crezca lo que necesite. */
.fr-modulerow .n {
  width: auto; min-width: 30px; text-align: right;
  font-size: 10px; font-weight: var(--fr-fw-med); letter-spacing: 0.03em;
  color: var(--fr-text-2);
}

/* ===== src/07-responsive.css ===== */
/* ════════════════════════════════════════════════════════════════════════
   FORTE UI · 07 · RESPONSIVE
   ════════════════════════════════════════════════════════════════════════
   Esto NO es «un breakpoint global». Es el conjunto de reglas de ADAPTACIÓN
   de los patrones del sistema, reunidas en un sitio para que un módulo no
   tenga que reinventar cómo se comporta un modal, una tabla o un dock en
   tablet.

   REGÍMENES DEL SISTEMA (razonados, no heredados de Bootstrap y NO
   «breakpoints de dispositivo»: no hay una regla «iPhone 15» ni una regla
   «iPad». Hay anchos en los que el CONTENIDO cambia de forma.)

     COMPACT      ≤  599   una columna · la tarjeta ES el ancho · el plano
                           de identidad baja de tinte porque deja de ser
                           una tarjeta y pasa a ser una banda
     MOBILE     600– 767   una columna con aire
     TABLET     768–1023   dos columnas · gutter claramente mayor
     TABLET_L  1024–1279   tres columnas
     DESKTOP   1280–1599   cuatro columnas
     WIDE      1600–1919   cuatro · el lienzo empieza a sobrar
     ULTRAWIDE   ≥ 1920    la composición se CENTRA y deja de crecer

   Los cortes intermedios que ya existían y siguen vigentes porque los pide
   una pieza concreta, no un dispositivo:
     ≤ 576px   la capa deja de ser tarjeta y pasa a ser la pantalla
     ≤ 700px   el dock envuelve, la paginación scrollea dentro de su caja
     ≤ 900px   las franjas de mando envuelven
     ≤ 1100px  la rejilla de formulario pasa a tercios
     ≤ 1180px  la navegación de formulario pasa de raíl a fichas
     ≤ 500px de alto  móvil apaisado: el modal recupera altura

   REGLA DE ENTRADA TÁCTIL: el objetivo de 44 px lo fija el DISPOSITIVO
   (`pointer: coarse`), no el ancho de la ventana. Un portátil con pantalla
   táctil merece objetivos grandes; un móvil en apaisado sigue siendo un
   dedo. Por eso el sistema no mezcla ambos ejes.
   ════════════════════════════════════════════════════════════════════════ */

/* ── COMPACT · el mismo color NO pesa lo mismo ──────────────────────────
   Un plano de identidad servido al 94 % es una cabecera de tarjeta cuando
   la tarjeta mide 380 px. En un teléfono la tarjeta ocupa el ancho entero y
   esa misma cabecera pasa a ser una BANDA: la superficie crece y con ella la
   coloridad percibida. El sistema baja la transmisión al 86 %, que devuelve
   el peso óptico al sitio donde se decidió.

   Medido con las cinco familias en los DOS regímenes: tinta ≥ 7,89 · glifo
   de placa ≥ 4,01 · 0 fallos de contraste. No es una concesión: es el mismo
   color servido con la dosis que le corresponde a su área.              */
@media (max-width: 599px) {
  :root { --fr-mod-tint: 0.86; }
}

/* ── Rejilla de formulario ──────────────────────────────────────────── */
@media (max-width: 1100px) {
  .fr-form-grid > *,
  .fr-form-grid > .col-2 { grid-column: span 4; }
  .fr-form-grid > .col-6,
  .fr-form-grid > .col-4 { grid-column: span 6; }
}
@media (max-width: 700px) {
  .fr-form-grid > *,
  .fr-form-grid > .col-2,
  .fr-form-grid > .col-4 { grid-column: span 6; }
  .fr-form-grid > .col-6,
  .fr-form-grid > .col-12 { grid-column: span 12; }
}
@media (max-width: 430px) {
  .fr-form-grid { gap: var(--fr-s4) var(--fr-s5); }
  .fr-form-grid > *,
  .fr-form-grid > .col-2 { grid-column: span 6; }
}

/* ── Navegación de formulario: raíl → fichas ────────────────────────── */
@media (max-width: 1180px) {
  .fr-form-layout { grid-template-columns: minmax(0, 1fr); gap: var(--fr-s5); }
  .fr-form-nav {
    position: sticky; top: var(--fr-sticky-top); z-index: var(--fr-z-toolbar);
    margin: 0 calc(-1 * var(--fr-page-pad));
    padding: var(--fr-s3) var(--fr-page-pad);
    background: var(--fr-canvas); border-bottom: 1px solid var(--fr-line-1);
  }
  .fr-form-nav-head, .fr-form-nav-progress { display: none; }
  .fr-form-nav ol { grid-auto-flow: column; grid-auto-columns: max-content; overflow-x: auto; gap: var(--fr-s2); scrollbar-width: none; }
  .fr-form-nav ol::-webkit-scrollbar { display: none; }
  .fr-form-nav a {
    border-left: 0; border-bottom: 2px solid transparent;
    border-radius: var(--fr-r-ctl) var(--fr-r-ctl) 0 0;
    white-space: nowrap; min-height: 38px;
  }
  .fr-form-nav a.is-current { border-bottom-color: var(--fr-teal); }
  .fr-form-nav .t { max-width: 15ch; }
}
@media (pointer: coarse) and (max-width: 1180px) {
  .fr-form-nav a { min-height: var(--fr-touch-min); }
}

/* ── Acciones de cabecera: escritorio ↔ compacta ─────────────────────────
   El intercambio SÓLO donde hay sustituto. `.fr-hero--swap` lo emite la
   plantilla cuando además de la fila de escritorio publica un disparador
   móvil que abre las MISMAS acciones.

   Sin esa condición, el intercambio escondía la fila de escritorio en TODAS
   las cabeceras, y cuatro no tienen sustituto: medido a 393×852,
   `fsoportebd` y `operariobd` servían CERO controles de cabecera donde en
   escritorio tienen dos y tres, y lo mismo la ficha de Moldes y el panel de
   reparaciones. Eso no es compactar: es que el responsive esconda
   funcionalidad, que es exactamente lo que el contrato prohíbe.

   Y se decide también por ALTURA. Un teléfono APAISADO tiene 852 px de
   ancho —así que el corte de 700 no lo alcanza— y 393 de alto, donde la fila
   de acciones vale una de las tres bandas de la cabecera: medido en el
   listado de materiales, tres botones con texto miden 700 px, no comparten
   renglón con nada y la cabecera crecía a 144 px. Con el disparador, 44.
   §16 del encargo: el apaisado se decide por altura, no sólo por ancho.  */
@media (max-width: 700px), (max-height: 500px) {
  .fr-hero--swap > .fr-hero-actions--desktop { display: none; }
  .fr-hero--swap > .fr-hero-actions--mobile  { display: flex; }
}

/* ── LA CABECERA NO SE COME LA PANTALLA ─────────────────────────────────
   `.fr-hero-actions` envuelve, y con cinco piezas —tres botones y dos
   distintivos— eso son tres o cuatro filas apiladas encima del contenido.
   Medido en Admin BD a 393×852: la cabecera sola ocupaba 224 px, y el
   listado no empezaba hasta el píxel 381 de los 852 que tiene la pantalla.
   El 45 % de un iPhone para leer un título y cinco controles.

   La franja pasa a UNA fila que se recorre en horizontal dentro de su caja.
   No es un patrón nuevo: es exactamente lo que el sistema ya hace con la
   paginación treinta líneas más abajo y con el índice de formulario. La
   página nunca scrollea en horizontal; la franja sí, y sólo si hace falta.

   Ninguna acción se oculta —§4 y §20 del encargo lo prohíben—: siguen las
   mismas, en el mismo orden, alcanzables con el dedo.

   Se aplica por ALTURA además de por anchura: en un móvil apaisado la
   pantalla es ancha y aun así la cabecera envuelta se lleva media
   ventana.                                                              */
@media (max-width: 900px), (max-height: 500px) {
  .fr-hero-actions {
    flex-wrap: nowrap;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    max-width: 100%;
  }
  .fr-hero-actions::-webkit-scrollbar { display: none; }
  .fr-hero-actions > * { flex: 0 0 auto; }
}

/* ── RITMO DEL CROMO EN COMPACTO Y EN APAISADO ──────────────────────────
   Compactar CROMO, no contenido (§27). Lo que baja aquí es el aire entre
   piezas de cabecera; ni el cuerpo, ni el objetivo táctil, ni el tamaño de
   letra del dato.                                                       */
@media (max-width: 700px) {
  body.fr-shell .fr-page { padding-top: var(--fr-s4); }
  .fr-hero { margin-bottom: var(--fr-s5); gap: var(--fr-s4); }
  /* LA PLACA DE ICONO NO NECESITA UNA FILA PARA ELLA SOLA.
     `.fr-hero-block` es `flex: 1 1 auto`, así que su base es el tamaño de su
     CONTENIDO: con un título de tres líneas la base es enorme, la línea no
     da para el bloque y la caja de icono, y la caja se iba sola a un renglón
     de 34 px más su hueco. Medido en el Catálogo Técnico a 393×852: 42 px
     de cromo para enseñar un icono ya visible en la barra.
     Con base 0 el bloque se dimensiona por el hueco que queda y los dos
     comparten renglón. Sólo en compacto: en escritorio la base de contenido
     es la que decide si las acciones de cabecera caben al lado del título, y
     ésa es una composición aprobada que aquí no se toca.

     CON SUELO. `flex-basis: 0` a secas deja el bloque de texto a merced del
     ancho de su hermano: en Admin BD, donde las acciones de cabecera miden
     700 px, el bloque se quedaba con 112 y el título se partía en tres
     líneas y la descripción en DIEZ — 109 px de cabecera pasaban a 246.
     Medido a 852×393 al extender esta regla por altura. El suelo hace que la
     línea se rompa ANTES de estrangular el texto: si las dos piezas caben
     con dignidad comparten renglón, y si no, la cabecera envuelve como
     siempre y el bloque recupera el ancho entero. */
  .fr-hero-block { flex: 1 1 0; min-width: min(100%, 12rem); }
  /* El aire heredado del último párrafo de la cabecera — mismo defecto y
     misma corrección que en el régimen de poca altura, explicados abajo.
     En un teléfono VERTICAL son otros 16 px de los 852. */
  .fr-hero-block > p:last-child,
  .fr-hero-meta:last-child,
  .hm-head-tx > p:last-child { margin-bottom: 0; }
}
/* MÓVIL APAISADO. 393 px de alto es el recurso escaso: con la barra y una
   cabecera normal, el Catálogo Técnico empezaba a servir contenido en el
   píxel 344 de 393. Aquí el cromo se aprieta de verdad. */
@media (max-height: 500px) {
  body.fr-shell .fr-page { padding-top: var(--fr-s3); }
  .fr-hero { margin-bottom: var(--fr-s4); gap: var(--fr-s3); }
  .fr-hero h1 { font-size: var(--fr-t-h3); }
  .fr-section { margin-bottom: var(--fr-gap-stack); }
  .fr-section > .hd { padding-bottom: var(--fr-s2); margin-bottom: var(--fr-s3); }
  /* NO se oculta nada. Ni la explicación de la cabecera, ni un control, ni
     un dato: el contrato responsive lo prohíbe y con razón. Lo que baja es
     el AIRE, que es lo que sobra cuando la ventana mide 393 px de alto. */

  /* LA CABECERA NO SE APILA EN CUATRO FILAS.
     02B lo midió: `.fr-hero` del listado CRUD medía 160 px de los 393 que
     tiene un teléfono apaisado, repartidos en cuatro renglones —título con
     su contador, descripción, tres botones y el buscador—. La causa es la
     misma que en compacto: `.fr-hero-block` es `flex: 1 1 auto`, así que su
     base es el ancho de su CONTENIDO, y con un título y una descripción esa
     base no deja sitio para nadie más en la línea.

     Con base 0 el bloque se dimensiona por el hueco que queda y las piezas
     comparten renglón mientras quepan. Es exactamente la corrección que la
     fase 02 aplicó por ANCHO; aquí se extiende por ALTURA, que es el
     recurso escaso en apaisado. Ninguna acción se oculta ni se encoge.

     El suelo es lo que hace que esto sea una mejora y no un intercambio:
     sin él, una cabecera con acciones anchas estrangula su propio texto.
     Ver la nota del bloque de ≤700 px. */
  .fr-hero-block { flex: 1 1 0; min-width: min(100%, 12rem); }

  /* EL AIRE HEREDADO DEL ÚLTIMO PÁRRAFO DE UNA CABECERA.
     `.fr-hero-meta` y `.hm-sub` son `<p>`, y Bootstrap Reboot les pone
     `margin-bottom: 1rem`. Ese margen no separa de nada: es el ÚLTIMO hijo
     de su bloque, y de la pieza siguiente ya separa el `margin-bottom` de la
     cabecera. Son 16 px de aire duplicado en cada familia que tiene
     descripción — medido en el Catálogo Técnico, en el listado CRUD y en
     Tareas.

     Se corrige sólo en los regímenes compactos: en escritorio la
     composición está aprobada y §23 pide DESKTOP_REGRESSION = 0. */
  .fr-hero-block > p:last-child,
  .fr-hero-meta:last-child,
  .hm-head-tx > p:last-child { margin-bottom: 0; }
}

/* ── LA FRANJA DE MANDO, UNA FILA QUE SE RECORRE ────────────────────────
   Es la otra mitad del cromo. En el Catálogo Técnico a 852×393 el contenido
   no empezaba hasta el píxel 344 de los 393 que tiene la pantalla —el 87 %
   en cromo—, y la cabecera sólo ponía 65 de esos: el resto eran los filtros
   envueltos en varias filas. A 393×852 el mismo listado empezaba en el 588.

   Misma solución que la paginación, que el índice de formulario y que las
   acciones de cabecera: una fila que se recorre dentro de su caja. No es una
   UX nueva —§20 la nombra— y no oculta ningún filtro: siguen todos, en el
   mismo orden, y el primero queda en `scrollLeft = 0`.

   Se aplica por ANCHO y por ALTO: el teléfono vertical sufre por el número
   de filas y el apaisado por el alto que le queda.                       */
@media (max-width: 700px), (max-height: 500px) {
  .fr-toolbar {
    flex-wrap: nowrap;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    padding: var(--fr-s3) var(--fr-s4);
  }
  .fr-toolbar::-webkit-scrollbar { display: none; }
  .fr-toolbar > * { flex: 0 0 auto; }
  /* El separador reparte el hueco SOBRANTE; con `flex-basis: auto` reclamaba
     ancho propio y empujaba el resto fuera de la vista. */
  .fr-toolbar > .fr-toolbar-spacer { flex: 1 1 0; min-width: 0; }
}

/* ── LA ETIQUETA DE UN FILTRO, AL LADO DEL CONTROL ──────────────────────
   `.fr-field` apila etiqueta sobre control, que es lo correcto en un
   formulario: la vista recorre una columna de campos. Pero un FILTRO de
   franja de mando no es una columna de campos, es una fila, y ahí apilar
   cuesta el alto de la etiqueta más su hueco por cada filtro: medido, el
   Catálogo Técnico gastaba 79 px de sus 393 en una franja con UN campo
   —etiqueta 14, hueco 3, control 44 y 12 de relleno—.

   Con la etiqueta en línea el filtro cuesta el alto de su control, que es
   el objetivo táctil y no se toca. La etiqueta NO se oculta: sigue leyéndose
   y sigue siendo el `<label>` que asocia el control (§5 del encargo: quitar
   redundancia, nunca información).

   Sólo dentro de una franja de mando, y en los dos regímenes compactos: en
   un teléfono VERTICAL el mismo campo cuesta 79 px de los 852. Un
   formulario —donde la cabecera ES contenido— no entra aquí.

   El campo conserva `flex: 1 1 auto` para que el control se lleve el hueco
   que deja la etiqueta en vez de encogerse a su ancho intrínseco: la franja
   ya se recorre en horizontal, y un `<select>` de 40 px sería peor que la
   fila que veníamos de quitar.                                          */
@media (max-width: 700px), (max-height: 500px) {
  .fr-toolbar > .fr-field {
    flex-direction: row;
    align-items: center;
    gap: var(--fr-s4);
    flex: 1 1 auto;
    min-width: min(100%, 15rem);
  }
  .fr-toolbar > .fr-field > .fr-label { flex: 0 0 auto; white-space: nowrap; }
  .fr-toolbar > .fr-field > .fr-select,
  .fr-toolbar > .fr-field > .fr-input { flex: 1 1 auto; min-width: 0; }
}

/* ── Página de aplicación en móvil apaisado ─────────────────────────────
   Con 393 px de alto, la cabecera y la paginación no dejan sitio para el
   listado: medido, 20 px de cuerpo en 852×393. Por debajo de este umbral la
   página SUELTA su altura fija y el documento vuelve a scrollear entero,
   que es el modelo normal del sistema. Mismo umbral con el que el modal
   recupera altura (RESPONSIVE.md). */
@media (max-height: 500px) {
  .fr-page--app,
  body.fr-shell .fr-page--app { height: auto; overflow: visible; }
  .fr-page-body { overflow: visible; flex: 0 0 auto; }
}

/* ── Paginación en pantalla estrecha ────────────────────────────────────
   Un listado con muchas páginas emite un control por página. Con el
   objetivo táctil de 44 px eso envuelve en varias filas y se come la
   pantalla: medido, 444 px de paginación en 393×852 dejando 41 px de
   listado. La franja pasa a scrollear en horizontal DENTRO de su caja,
   que es la regla del sistema para cualquier pieza ancha; la página nunca
   scrollea en horizontal. */
@media (max-width: 700px), (pointer: coarse) {
  .fr-pagination {
    flex-wrap: nowrap; overflow-x: auto; overscroll-behavior-x: contain;
    scrollbar-width: none;
  }
  .fr-pagination::-webkit-scrollbar { display: none; }
  .fr-pagination button,
  .fr-pagination a,
  .fr-pagination > span { flex: 0 0 auto; }
}
/* El control de pagina ya crecia a 44 de ALTO en tactil, pero no de ancho: se
   quedaba en 30. Un objetivo de 30x44 no cumple los 44 del contrato y ademas
   es justo el control que mas se falla con el dedo, porque van pegados. */
@media (pointer: coarse) {
  .fr-pagination button,
  .fr-pagination a,
  .fr-pagination > span { min-width: var(--fr-touch-min); }
}

/* ── Franjas de mando ───────────────────────────────────────────────── */
@media (max-width: 900px) {
  .fr-scanbar { flex-wrap: wrap; }
  /* flex-basis 100% con shrink permitido: la franja envuelve sin que el
     texto de la nota imponga su ancho mínimo a toda la página. */
  .fr-scanbar-form { flex: 1 1 100%; min-width: 0; }
  .fr-scanbar-note { flex: 1 1 100%; min-width: 0; }
  .fr-toolbar { gap: var(--fr-s3); }
}

/* ── Capas ──────────────────────────────────────────────────────────── */
/* La superficie de trabajo deja de repartirse con el fondo en cuanto el
   fondo ya no cabe: por debajo de este umbral no queda listado útil detrás
   del velo, así que el panel ocupa la ventana menos un canto. El bloque de
   576 px que viene después lo lleva a 100 % como a cualquier otro drawer. */
@media (max-width: 900px) {
  .fr-drawer--work { --fr-drawer-w: calc(100% - 16px); }
}
@media (max-width: 576px) {
  /* El modal deja de ser una tarjeta flotante y pasa a ser la pantalla. */
  .fr-modal { padding: 0; align-items: stretch; }
  .fr-modal-panel { width: 100%; max-height: 100vh; max-height: 100dvh; border-radius: 0; }
  .fr-drawer { width: 100%; border-radius: 0; }
  .fr-toast-stack { right: var(--fr-s4); left: var(--fr-s4); }
  .fr-toast { min-width: 0; max-width: none; }

  /* ── LA LAMINA LLEGA AL BORDE; EL CONTENIDO, NO ────────────────────────
     Al pasar a pantalla completa, `padding: 0` sobre el marco anulaba el
     margen de seguridad que la capa flotante si respetaba, y el BOTON DE
     CERRAR quedaba debajo del notch. Comprobado en captura con los insets de
     un iPhone 15: la cabecera entera dentro de los 59 px del recorte.

     No se resuelve devolviendo el relleno al marco —eso deja una franja de
     velo sobre una hoja que quiere ser la pantalla— sino sirviendo la
     SUPERFICIE hasta el borde y empujando el CONTENIDO por debajo del
     recorte. Es lo que hace el sistema del telefono con sus propias hojas.  */
  .fr-modal-head,
  .fr-drawer-head { padding-top: calc(var(--fr-s5) + var(--fr-safe-top)); }
  .fr-modal-head, .fr-modal-body, .fr-modal-foot,
  .fr-drawer-head, .fr-drawer-body, .fr-drawer-foot {
    padding-left: max(var(--fr-s6), var(--fr-safe-left));
    padding-right: max(var(--fr-s6), var(--fr-safe-right));
  }
  .fr-modal-foot,
  .fr-drawer-foot { padding-bottom: max(var(--fr-s5), var(--fr-safe-bottom)); }
  /* Solo cuando el cuerpo ES el ultimo: si hay pie, la reserva la pone el
     pie y sumarla dos veces deja un hueco muerto de 34 px. */
  .fr-modal-body:last-child,
  .fr-drawer-body:last-child { padding-bottom: calc(var(--fr-pad-modal) + var(--fr-safe-bottom)); }
}
@media (max-height: 500px) {
  /* Móvil apaisado: la altura es el recurso escaso, no el ancho.

     EL ABREVIADO BORRABA EL RECORTE. `padding: var(--fr-s2)` es una
     declaración abreviada y reponía los CUATRO lados, así que anulaba los
     cuatro `max(..., safe)` que §6.1 declara para el marco de la capa. Y el
     único viewport donde este bloque manda es justamente el que tiene inset
     lateral: un teléfono apaisado, 59 px a cada costado. Medido a 852×393,
     la capa llegaba a 4 px del borde físico con el botón de cerrar dentro
     de la cámara. Se escribe por lados: la altura se recorta —que es a lo
     que venía la regla— y el costado sigue respetando el recorte. */
  .fr-modal {
    padding-top: max(var(--fr-s2), var(--fr-safe-top));
    padding-bottom: max(var(--fr-s2), var(--fr-safe-bottom));
    padding-left: max(var(--fr-s2), var(--fr-safe-left));
    padding-right: max(var(--fr-s2), var(--fr-safe-right));
  }
  /* `100%` del marco, por lo mismo que en 06-patterns §6.1: aquí el relleno
     es de 4 px y el recorte inferior de un teléfono apaisado, 21. Restarle
     8 px a `100dvh` daba una capa 17 px más alta que su hueco, y centrada,
     así que el pie acababa dentro de la banda del indicador de inicio.
     Medido a 852×393 antes de esta corrección: 3 px dentro. */
  .fr-modal-panel { max-height: 100%; }
  .fr-modal-head { padding: var(--fr-s3) var(--fr-s5); }
  .fr-modal-foot { padding: var(--fr-s3) var(--fr-s5); }
  .fr-sheet { max-height: 96vh; max-height: 96dvh; }
}

/* ── Página y rejillas ──────────────────────────────────────────────── */
@media (max-width: 700px) {
  /* La reserva inferior baja, pero el recorte del dispositivo NO: la barra
     de gestos del teléfono se come esa banda igual. */
  .fr-page { padding-bottom: calc(var(--fr-s7) + var(--fr-safe-bottom)); }
  /* La exclusión NO es decorativa: `--wide` y `--tight` son modificadores
     que fijan el mismo token con la MISMA especificidad, y este bloque va
     después, así que los cancelaba. Dos paneles declarados `--wide` se
     quedaban en móvil en dos columnas de 170 px y su contenido —un correo
     largo, un botón de bloque— se recortaba contra el `overflow: hidden`
     del panel: sin desbordar el documento, así que ninguna métrica lo
     veía. Medido en la migración de `auth/profile.html` (05B · ola 6).
     Un modificador de rejilla existe porque su contenido lo pide; en
     pantalla estrecha eso se cumple colapsando a UNA columna, no
     estrechando la columna. */
  /* 8 px entre tarjetas convertía la pantalla en un mosaico. El suelo del
     ritmo responsive (14 px) es lo mínimo para que dos tarjetas contiguas
     se lean como dos piezas y no como una retícula. */
  .fr-grid { gap: var(--fr-gap-grid); }
  .fr-grid:not(.fr-grid--wide):not(.fr-grid--tight) { --fr-grid-min: 152px; }
  .fr-kpi-row { gap: var(--fr-s5); }
  .fr-kpi-row .fr-kpi { padding-right: var(--fr-s5); }
}

/* ── Banda de utilidad y tarjeta de módulo (HOME V2 · PATTERNS §6.5) ─── */
/* El reparto 0,92 / 1,3 sólo tiene sentido mientras las dos piezas caben en
   una línea legible. Por debajo se apilan, y el escáner queda ARRIBA: en
   planta es la acción de partida. */
@media (max-width: 900px) {
  .fr-utilbar { grid-template-columns: minmax(0, 1fr); }
}
/* Con permisos reducidos puede haber UNA sola tarjeta. Sin techo, ocuparia
   los 1.584 px de la fila y se leeria como un cartel, no como un modulo. */
.fr-modcard { max-width: 520px; }
@media (max-width: 700px) { .fr-modcard { max-width: none; } }

/* El titulo domina a 18 px, pero en una columna de 300 px «Equipos de
   Produccion» parte en dos lineas y arrastra la altura de LAS CUATRO
   cabeceras —la rejilla las iguala—, dejando aire de mas en las otras tres.
   Por debajo de 1500 px de ancho baja a 16 y entra en una linea. */
@media (max-width: 1499px) {
  .fr-modcard-name b { font-size: var(--fr-t-lead); }
}

/* ── CUANTOS ACCESOS CABEN ─────────────────────────────────────────────
   La presencia de esta pantalla NO se gana estirando la tarjeta: se probó,
   y una de 410 px con dos accesos es una caja a medio llenar. Se gana
   enseñando lo que ya hay. Pero cuatro accesos simultáneos en las cuatro
   tarjetas es ruido: dieciséis enlaces compitiendo bajo cuatro títulos.

   TRES es la composición preferente. El cuarto sólo aparece cuando dos
   cosas lo justifican a la vez:

       · la pantalla es ALTA (≥ 1000 px), y
       · el módulo es DENSO (≥ 6 entradas), de modo que enseñar una más
         sigue dejando la mayoría en la capa.

   Un módulo de cuatro entradas no gana nada enseñando cuatro: dejaría la
   capa sin razón de ser. Y lo que se oculta NO se pierde: está entero al
   abrir el módulo.

       ≥ 1000 px de alto   cuatro, sólo en módulos densos
       820 – 999           tres
       < 820               dos          (la aritmética que R6B midió)

   La reserva se cuenta en FILAS, no en píxeles: con el dedo la fila mide 44 y
   no 36, y escribir el 36 descuadraba el escalón de la tarjeta que enseña una
   entrada menos. `--fr-modlink-h` lo resuelve en los dos regímenes.

   El `min-height` sube para TODAS las listas a la vez, no sólo para las
   densas: si el cuerpo midiese distinto según el módulo, el escalón dejaría
   de estar alineado, que es justo lo que costó corregir. */
.fr-modcard-links li:nth-child(n+4) { display: none; }

@media (min-height: 1000px) {
  .fr-modcard-links { min-height: calc(4 * var(--fr-modlink-h)); }
  .fr-modcard-links--densa li:nth-child(4) { display: list-item; }
}
@media (max-height: 819px) {
  .fr-modcard-links { min-height: calc(2 * var(--fr-modlink-h)); }
  .fr-modcard-links li:nth-child(n+3) { display: none; }
}
@media (max-width: 700px) {
  .fr-scanbar--mod { flex-wrap: wrap; }
  .fr-scanbar--mod .fr-btn { width: 100%; }
  .fr-modcard-id { padding: var(--fr-s5); }
  .fr-modcard-body { padding: var(--fr-s4) var(--fr-s5) var(--fr-s5); }
}

/* ── TABLET · una composición propia, no un escritorio encogido ─────────
   El gutter y el ritmo ya crecen solos con la ventana (§1.17), que es de
   donde salía la mayor parte de la densidad. Lo que NO se arregla solo es
   lo que el sistema decide por ancho: en 768–1023 caben dos columnas
   cómodas, y forzar tres deja tarjetas de 230 px con el texto partido.  */
@media (min-width: 768px) and (max-width: 1023px) {
  .fr-grid:not(.fr-grid--tight) { --fr-grid-min: 300px; }
  /* La franja de KPI en tableta reparte en dos filas antes que estrechar
     cada indicador hasta que su cifra parta. */
  .fr-kpi-row { gap: var(--fr-gap-grid); }
}

/* ── ULTRAWIDE · composición deliberada, no «que no rompa» ──────────────
   Por encima de 1.920 px el lienzo puede ocupar la ventana entera, pero la
   composición no: `--fr-content-max` la centra. Lo que este bloque impide es
   lo otro que pasa al ensanchar sin criterio — que una tarjeta suelta se
   estire hasta parecer una pancarta y que una línea de texto se vuelva
   ilegible por larga. 75 caracteres es el límite tipográfico clásico y aquí
   se aplica al párrafo de ayuda, no a las tablas de datos.              */
@media (min-width: 1920px) {
  .fr-help,
  .fr-empty p,
  .fr-alert > div > p { max-width: 75ch; }
}

/* ── PUNTERO GRUESO · el hover no se queda pegado ───────────────────────
   En un táctil `:hover` no se «sale»: se queda aplicado hasta que el dedo
   toca otra cosa. Una fila resaltada que ya no es la que se pulsó es peor
   que ningún resalte. El hover es MEJORA, nunca requisito (§21-§22): aquí
   se apaga el estado persistente y se conserva `:active`, que sí es el
   testigo correcto del gesto.                                          */
@media (hover: none) {
  .fr-table tbody tr:hover { background: transparent; }
  .fr-table tbody tr.is-selected:hover { background: var(--fr-selected); }
  .fr-card--nav:hover { box-shadow: var(--fr-sh-card); border-color: var(--fr-line-1); }
  .fr-modulerow:hover { background: transparent; }
  .fr-modcard-link:hover { background: transparent; }
  .fr-modcard-link:active { background: var(--fr-hover); }
}

/* ── Tablas en táctil estrecho ──────────────────────────────────────── */
/* La tabla NO se transforma en tarjetas: en planta la lectura por columnas
   es lo que se busca. Se garantiza que el scroll horizontal sea LOCAL y
   que la columna identificadora permanezca visible. */
@media (max-width: 700px) {
  .fr-table-wrap { border-radius: var(--fr-r-ctl); }
  .fr-table--dense td { font-size: var(--fr-t-small); }
}

/* ===== src/08-accessibility.css ===== */
/* ════════════════════════════════════════════════════════════════════════
   FORTE UI · 08 · ACCESIBILIDAD
   ════════════════════════════════════════════════════════════════════════
   Foco, movimiento reducido, alto contraste y objetivos táctiles reunidos
   en un sitio. Ningún componente inventa su propio foco: el anillo vive en
   02-foundations.css y aquí se refuerza para los casos donde el fondo o el
   material lo comprometen.
   ════════════════════════════════════════════════════════════════════════ */

/* ── Movimiento reducido ────────────────────────────────────────────────
   Acotado a `.fr-app`: la regla global equivalente ya existe en el runtime
   (css/lg-shell.css) y no se duplica su alcance. */
@media (prefers-reduced-motion: reduce) {
  .fr-app *,
  .fr-app *::before,
  .fr-app *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ── Foco sobre material oscuro ─────────────────────────────────────────
   El anillo doble usa cream como halo interior. Sobre una placa M5 ese
   halo desaparece: se invierte a ink + petróleo claro. */
.fr-m5 :focus-visible,
.fr-dock :focus-visible,
.fr-scanbar :focus-visible {
  box-shadow: 0 0 0 2px rgba(16, 7, 9, 0.9), 0 0 0 4px #8ec4d0;
}

/* ── Enlace de salto ────────────────────────────────────────────────────
   Primera parada del tabulador en pantallas con topbar fija. */
.fr-skip {
  position: absolute; left: var(--fr-s4); top: -100px; z-index: var(--fr-z-tooltip);
  padding: var(--fr-s3) var(--fr-s5); border-radius: var(--fr-r-ctl);
  background: var(--fr-ink); color: var(--fr-cream);
  font-size: var(--fr-t-small); font-weight: var(--fr-fw-med);
  transition: top var(--fr-d-micro) var(--fr-ease);
}
.fr-skip:focus { top: calc(var(--fr-topbar-offset) + var(--fr-s3)); text-decoration: none; }

/* ── Alto contraste del sistema operativo ───────────────────────────────
   El vidrio no aporta información: cuando el usuario pide contraste, se
   cambia por sólido y los bordes suben de peso. */
@media (prefers-contrast: more) {
  .fr-app {
    --fr-line-1: rgba(22, 8, 10, 0.16);
    --fr-line-2: rgba(22, 8, 10, 0.28);
    --fr-line-3: rgba(22, 8, 10, 0.46);
    --fr-text-3: #574f4a;
    --fr-text-4: #6f6762;
  }
  .fr-m3 { background: var(--fr-m3-solid); -webkit-backdrop-filter: none; backdrop-filter: none; }
  .fr-m4 { background: var(--fr-m4-solid); -webkit-backdrop-filter: none; backdrop-filter: none; }
  .fr-m6 { background: var(--fr-m6-solid); -webkit-backdrop-filter: none; backdrop-filter: none; }
  .fr-btn { border-color: var(--fr-line-3); }
  .fr-status::before { outline: 1px solid rgba(22, 8, 10, 0.35); }
}

/* ── Objetivo táctil ────────────────────────────────────────────────────
   Utilidad para elementos que no pueden crecer (un icono dentro de una
   celda densa): amplía la zona sensible sin cambiar la geometría visible. */
.fr-hit { position: relative; }
.fr-hit::after {
  content: ""; position: absolute; top: 50%; left: 50%;
  width: var(--fr-touch-min); height: var(--fr-touch-min);
  transform: translate(-50%, -50%);
}

/* ── Estado deshabilitado coherente ─────────────────────────────────── */
.fr-app [aria-disabled="true"],
.fr-app .is-disabled { opacity: 0.45; pointer-events: none; }

/* ===== src/09-print.css ===== */
/* ════════════════════════════════════════════════════════════════════════
   FORTE UI · 09 · IMPRESIÓN DE LA INTERFAZ
   ════════════════════════════════════════════════════════════════════════
   Qué pasa cuando alguien imprime una PANTALLA del producto.

   Esto NO es el sistema documental. Los documentos oficiales de FORTEONE
   (fichas, informes, ITC, registros) tienen su propia hoja —
   `design/forte/forte-doc.css` — que se sirve por separado y NUNCA se
   mezcla con el material de pantalla. Ver GOVERNANCE.md.

   PRINT = SOLID. Sin glass, sin blur, sin sombras simuladas.
   ════════════════════════════════════════════════════════════════════════ */

@media print {
  /* El cromo de aplicación no se imprime: no dice nada en papel y gasta
     tóner de sobra. */
  .fr-topbar,
  .fr-toolbar,
  .fr-dock,
  .fr-footer,
  .fr-layer,
  .fr-toast-stack,
  .fr-pagination,
  .fr-skip,
  .fr-form-nav { display: none !important; }

  /* La página recupera el margen del papel y suelta la reserva de topbar. */
  .fr-page {
    padding: 0 !important;
    max-width: none !important;
  }
  .fr-app {
    background: #fff !important;
    color: var(--fr-ink) !important;
  }

  /* Todo material translúcido pasa a sólido y pierde la profundidad. */
  .fr-m2, .fr-m3, .fr-m4, .fr-m5, .fr-m6, .fr-card, .fr-panel, .fr-table-wrap {
    background: #fff !important;
    -webkit-backdrop-filter: none !important;
    backdrop-filter: none !important;
    box-shadow: none !important;
    border: 0.5pt solid rgba(22, 8, 10, 0.28) !important;
  }

  /* Cabeceras de tabla: sin sticky (no existe el scroll en papel) y con
     repetición de cabecera en cada página. */
  .fr-table thead { display: table-header-group; }
  .fr-table th {
    position: static !important;
    background: #eee9e3 !important;
    color: var(--fr-ink) !important;
    border-bottom: 0.7pt solid var(--fr-ink) !important;
  }
  .fr-table tr, .fr-card, .fr-panel, .fr-form-block { break-inside: avoid; }
  .fr-table td { border-bottom: 0.4pt solid rgba(22, 8, 10, 0.18) !important; }

  /* Los bloques plegables se imprimen abiertos: un papel no se despliega. */
  .fr-form-block > .bd { display: block !important; }

  /* El scroll horizontal local no tiene sentido en papel. */
  .fr-table-scroller { overflow: visible !important; }
  .fr-table-scroller .fr-table { min-width: 0 !important; }

  /* Estados por símbolo, no sólo por color: una impresora en blanco y negro
     no distingue un punto verde de uno rojo. */
  .fr-status--ok::after     { content: " ✓"; }
  .fr-status--danger::after { content: " ✕"; }
  .fr-status--warn::after   { content: " !"; }
}

/* ===== src/10-legacy-bridge.css ===== */
/* ════════════════════════════════════════════════════════════════════════
   FORTE UI · 10 · PUENTE LEGACY  —  TEMPORAL POR DISEÑO
   ════════════════════════════════════════════════════════════════════════
   FORTEONE tiene 105 plantillas vivas y 62 ficheros que definen su propio
   botón. Migrarlas de golpe sería un big-bang. Este puente permite que el
   marcado existente CONVERJA visualmente hacia el sistema sin reescribirlo,
   y desaparece pantalla a pantalla conforme 05B avanza.

   ── REGLAS DE ESTE FICHERO ────────────────────────────────────────────
   1. NADA aquí es global. Toda regla cuelga de un ámbito de activación:
        `.fr-shell`  → shell (topbar y pie), activado en base.html
        `.fr-ui`     → pantalla migrada, activado por plantilla
      Cargar forte-ui.css en una pantalla sin esas clases NO cambia nada.
   2. Cada bloque declara SOURCE, TARGET, SCOPE y REMOVAL_GATE. La misma
      información, en tabla, está en
      docs/audits/FORTE_BRAND_05A/LEGACY_BRIDGE_MANIFEST.csv
   3. Ninguna regla de aquí puede sobrevivir a la migración de su pantalla:
      cuando el último consumidor deja de existir, el bloque SE RETIRA.
   4. Se prefiere heredar tokens a repetir valores: el puente traduce, no
      redefine.
   ════════════════════════════════════════════════════════════════════════ */

/* ══════════════════════════════════════════════════════════════════════
   LOS DOS BLOQUES DE SHELL — RETIRADOS (FORTE_BRAND_05B · ola 1B)
   ══════════════════════════════════════════════════════════════════════
   El primero (topbar) y el segundo (pie global) adaptaban el marcado
   Bootstrap/LG del shell al material del sistema. Su condición de retirada
   se ha cumplido:

     · partials/_topbar.html emite `.fr-topbar` + `.fr-topbar-inner`;
     · partials/_footer.html emite `div.fr-footer.fr-footer--fixed`;
     · base.html emite `body.fr-shell` como única clase de shell.

   Con eso el material, la geometría y los estados dejan de necesitar
   traducción: los sirve 04-layout.css §4.1 y §4.4 directamente. Con los
   bloques se retiran también sus fuentes en css/lg-shell.css y en
   css/lg-forte-tokens.css §R-1: cuando una pieza se rehace, la anterior
   SE RETIRA — no se parchea encima.

   Verificado antes de retirar: cero consumidores de `.forte-shell-topbar`
   y de `footer.lg-footer` en las 105 plantillas vivas.
   ══════════════════════════════════════════════════════════════════════ */

/* ══════════════════════════════════════════════════════════════════════
   BR-03 · BOTONES BOOTSTRAP
   SOURCE  .btn / .btn-primary / .btn-secondary / .btn-danger / .btn-sm …
   TARGET  .fr-btn (05-components.css §5.2)
   SCOPE   .fr-ui — sólo pantallas migradas
   GATE    cuando la plantilla emita .fr-btn en lugar de .btn
   ──────────────────────────────────────────────────────────────────────
   Se traduce la ANATOMÍA (altura, radio, tipografía, transición) y las
   cuatro intenciones que Bootstrap ya expresa. No se tocan `.btn-link`
   ni las variantes `-outline-*` poco usadas: convergen en 05B.         */
.fr-ui .btn {
  min-height: 32px; padding: 0 var(--fr-s5);
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  border: 1px solid transparent; border-radius: var(--fr-r-ctl);
  font-family: var(--fr-font); font-size: var(--fr-t-small); font-weight: var(--fr-fw-med);
  line-height: 1; letter-spacing: 0.01em;
  transition: background var(--fr-d-micro) var(--fr-ease),
              border-color var(--fr-d-micro) var(--fr-ease),
              box-shadow var(--fr-d-micro) var(--fr-ease),
              transform var(--fr-d-micro) var(--fr-ease);
}
.fr-ui .btn:active { transform: translateY(0.5px); box-shadow: var(--fr-sh-press); }
.fr-ui .btn-primary,
.fr-ui .btn-primary:focus { background-color: var(--fr-ink); border-color: var(--fr-ink); color: var(--fr-cream); box-shadow: var(--fr-sh-contact); }
.fr-ui .btn-primary:hover { background-color: #241417; border-color: #241417; color: var(--fr-cream); }
.fr-ui .btn-secondary,
.fr-ui .btn-secondary:focus { background-color: var(--fr-raised); border-color: var(--fr-line-3); color: var(--fr-text); box-shadow: var(--fr-sh-contact); }
.fr-ui .btn-secondary:hover { background-color: var(--fr-top); border-color: rgba(22, 8, 10, 0.34); color: var(--fr-text); }
.fr-ui .btn-danger,
.fr-ui .btn-danger:focus { background-color: var(--fr-danger); border-color: var(--fr-danger); color: #fff; }
.fr-ui .btn-danger:hover { background-color: #86251b; border-color: #86251b; color: #fff; }
.fr-ui .btn-success,
.fr-ui .btn-success:focus { background-color: var(--fr-ok); border-color: var(--fr-ok); color: #fff; }
.fr-ui .btn-info,
.fr-ui .btn-info:focus { background-color: var(--fr-info); border-color: var(--fr-info); color: #fff; }
.fr-ui .btn-warning,
.fr-ui .btn-warning:focus { background-color: var(--fr-warn); border-color: var(--fr-warn); color: #fff; }
.fr-ui .btn-sm { min-height: 26px; padding: 0 var(--fr-s4); font-size: var(--fr-t-mini); }
.fr-ui .btn-lg { min-height: 40px; padding: 0 var(--fr-s7); font-size: var(--fr-t-base); }
@media (pointer: coarse) {
  .fr-ui .btn, .fr-ui .btn-sm { min-height: var(--fr-touch-min); padding: 0 var(--fr-s6); font-size: var(--fr-t-base); }
}

/* ══════════════════════════════════════════════════════════════════════
   BR-04 · CAMPOS BOOTSTRAP
   SOURCE  .form-control / .form-select / .form-label
   TARGET  .fr-input / .fr-select / .fr-label (05-components.css §5.3)
   SCOPE   .fr-ui
   GATE    cuando la plantilla emita .fr-field                          */
.fr-ui .form-control,
.fr-ui .form-select {
  font-family: var(--fr-font); font-size: var(--fr-t-small); color: var(--fr-text);
  min-height: 32px; padding: 0 var(--fr-s4);
  background-color: var(--fr-raised);
  border: 1px solid var(--fr-line-3); border-radius: var(--fr-r-ctl);
  box-shadow: none;
  transition: border-color var(--fr-d-micro) var(--fr-ease),
              background var(--fr-d-micro) var(--fr-ease),
              box-shadow var(--fr-d-micro) var(--fr-ease);
}
.fr-ui textarea.form-control { padding: var(--fr-s3) var(--fr-s4); min-height: 72px; }
.fr-ui .form-control:hover,
.fr-ui .form-select:hover { border-color: rgba(22, 8, 10, 0.34); }
.fr-ui .form-control:focus,
.fr-ui .form-select:focus {
  background-color: var(--fr-top);
  border-color: var(--fr-teal);
  box-shadow: 0 0 0 3px var(--fr-focus-ring);
  color: var(--fr-text);
}
.fr-ui .form-control::placeholder { color: var(--fr-text-4); }
.fr-ui .form-control[readonly] { background-color: var(--fr-n100); color: var(--fr-text-2); border-style: dashed; }
.fr-ui .form-control:disabled,
.fr-ui .form-select:disabled { background-color: var(--fr-n100); color: var(--fr-text-4); }
.fr-ui .form-label {
  font-size: var(--fr-t-mini); font-weight: var(--fr-fw-med);
  color: var(--fr-text-3); letter-spacing: 0.015em; margin-bottom: 3px;
}
/* Mismo hueco de autofill que resuelve 05-components.css §5.3 para
   `.fr-input`: un campo Bootstrap de una pantalla migrada tiene que
   comportarse igual. La receta no se repite — se apunta a los mismos
   tokens. */
.fr-ui .form-control:-webkit-autofill,
.fr-ui .form-select:-webkit-autofill {
  -webkit-text-fill-color: var(--fr-text);
  caret-color: var(--fr-text);
  -webkit-box-shadow: inset 0 0 0 100px var(--fr-top);
  border-color: var(--fr-line-3);
  transition: background-color 100000s ease-in-out 0s;
}
.fr-ui .is-invalid, .fr-ui .form-control.is-invalid { border-color: var(--fr-danger); background-color: #fdf6f5; }
.fr-ui .invalid-feedback { font-size: var(--fr-t-micro); color: var(--fr-danger); font-weight: var(--fr-fw-med); }
@media (pointer: coarse) {
  .fr-ui .form-control, .fr-ui .form-select { min-height: var(--fr-touch-min); font-size: var(--fr-t-lead); }
}

/* ══════════════════════════════════════════════════════════════════════
   BR-05 · MODAL BOOTSTRAP
   SOURCE  .modal-content / -header / -body / -footer / .modal-backdrop
   TARGET  .fr-layer + .fr-modal-* (06-patterns.css §6.1)
   SCOPE   .fr-ui
   GATE    cuando la plantilla use FR.layers en vez de bootstrap.Modal
   ──────────────────────────────────────────────────────────────────────
   AVISO: Bootstrap saca su modal a `body`, FUERA del contenedor `.fr-ui`.
   Por eso este bloque se ancla en `body.fr-ui`, no en un `.fr-ui`
   intermedio. Es la excepción que confirma por qué el sistema propio no
   usa portales implícitos.                                             */
body.fr-ui .modal-content {
  background: var(--fr-m4-bg);
  -webkit-backdrop-filter: blur(var(--fr-m4-blur)) saturate(var(--fr-m4-sat));
  backdrop-filter: blur(var(--fr-m4-blur)) saturate(var(--fr-m4-sat));
  border: 0; border-radius: var(--fr-r-modal);
  box-shadow: var(--fr-edge-out), var(--fr-edge-in), var(--fr-edge-rim), var(--fr-sh-modal);
  color: var(--fr-text);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  body.fr-ui .modal-content { background: var(--fr-m4-solid); }
}
body.fr-ui .modal-header {
  padding: var(--fr-s5) var(--fr-s6);
  border-bottom: 1px solid var(--fr-line-1);
  background: transparent;
}
body.fr-ui .modal-title { font-size: var(--fr-t-lead); font-weight: var(--fr-fw-bold); color: var(--fr-text); }
body.fr-ui .modal-body { padding: var(--fr-s6); }
body.fr-ui .modal-footer {
  padding: var(--fr-s5) var(--fr-s6);
  border-top: 1px solid var(--fr-line-1);
  background: rgba(22, 8, 10, 0.025);
  gap: var(--fr-s4);
}
body.fr-ui .modal-backdrop.show { background: var(--fr-veil-bg); opacity: 1; }
/* El botón de cerrar de Bootstrap mide 32×32 SIEMPRE, y es el único control
   que toda capa tiene garantizado. Con puntero grueso queda por debajo del
   objetivo táctil en cada modal de cada familia migrada — no es deuda de una
   pantalla, es un hueco del puente. `background-size` conserva el aspa a su
   tamaño óptico mientras la caja crece. */
@media (pointer: coarse) {
  body.fr-ui .modal-header .btn-close,
  body.fr-ui .modal-content .btn-close {
    /* `box-sizing` importa: Bootstrap le pone 0.25rem de relleno y sin esto
       la caja acaba en 60 px y engorda la cabecera del modal. */
    box-sizing: border-box;
    width: var(--fr-touch-min); height: var(--fr-touch-min);
    background-size: 14px 14px;
  }
}
@media (max-width: 576px) {
  body.fr-ui .modal-dialog { margin: 0; max-width: none; min-height: 100dvh; }
  body.fr-ui .modal-content { border-radius: 0; min-height: 100dvh; }
  /* LA LÁMINA LLEGA AL BORDE; EL CONTENIDO, NO.
     Esta capa pasa a pantalla completa igual que la del sistema, pero sin la
     contrapartida que aquélla sí tenía: empujar el contenido por debajo del
     recorte. Medido en `Gestión de usuarios` a 393×852 con los insets de un
     iPhone 15: el botón de cerrar en `y = 4` con 59 px de Dynamic Island
     encima. Y no es una pantalla: son las VEINTISIETE plantillas que todavía
     abren un modal de Bootstrap.

     Es la misma corrección, palabra por palabra, que 07-responsive aplica a
     `.fr-modal-head` en este mismo régimen.                              */
  body.fr-ui .modal-header { padding-top: calc(var(--fr-s5) + var(--fr-safe-top)); }
  body.fr-ui .modal-header,
  body.fr-ui .modal-body,
  body.fr-ui .modal-footer {
    padding-left: max(var(--fr-s6), var(--fr-safe-left));
    padding-right: max(var(--fr-s6), var(--fr-safe-right));
  }
  body.fr-ui .modal-footer { padding-bottom: max(var(--fr-s5), var(--fr-safe-bottom)); }
  body.fr-ui .modal-body:last-child {
    padding-bottom: calc(var(--fr-s6) + var(--fr-safe-bottom));
  }
}
/* Por encima de 576 la capa vuelve a ser una tarjeta centrada y Bootstrap la
   separa con `margin: 1.75rem auto`: 28 px, que basta contra la barra de
   estado de un iPad —24— pero no contra el recorte lateral de un teléfono
   APAISADO. Medido a 852×393 con un `modal-lg`: 800 px centrados dejan el
   borde izquierdo en 26 y la cámara ocupa 59.

   El recorte se pone en el MARCO (`.modal`, que es el contenedor fijo con
   scroll), no en el diálogo. Tocarle los márgenes al diálogo destruiría el
   `margin: 1.75rem auto` con el que Bootstrap lo centra, y un modal
   descentrado en escritorio sería exactamente la regresión que esta fase no
   puede permitirse. El relleno base es 0, así que en un monitor —donde los
   cuatro insets valen 0— no cambia un píxel.                            */
body.fr-ui .modal {
  padding-top: var(--fr-safe-top);
  padding-bottom: var(--fr-safe-bottom);
  padding-left: var(--fr-safe-left);
  padding-right: var(--fr-safe-right);
}
/* En pantalla completa el marco no aparta nada: la lámina llega al borde y
   son la cabecera y el pie los que empujan el contenido (bloque de arriba). */
@media (max-width: 576px) {
  body.fr-ui .modal { padding: 0; }
}

/* ══════════════════════════════════════════════════════════════════════
   BR-06 · TABLA BOOTSTRAP
   SOURCE  .table / thead / tbody
   TARGET  .fr-table (05-components.css §5.4)
   SCOPE   .fr-ui
   GATE    cuando la plantilla emita .fr-table                          */
.fr-ui .table {
  --bs-table-bg: transparent;
  font-size: var(--fr-t-small);
  color: var(--fr-text);
  margin-bottom: 0;
}
.fr-ui .table > thead > tr > th {
  background: var(--fr-work);
  color: var(--fr-text-3);
  font-size: var(--fr-t-micro); font-weight: var(--fr-fw-bold);
  letter-spacing: 0.05em; text-transform: uppercase;
  padding: 7px 10px; border-bottom: 1px solid var(--fr-line-3);
  white-space: nowrap;
}
.fr-ui .table > tbody > tr > td { padding: 6px 10px; border-bottom: 1px solid var(--fr-line-1); vertical-align: middle; }
.fr-ui .table > tbody > tr:hover > td { background: var(--fr-hover); }
.fr-ui .table > tbody > tr.is-selected > td,
.fr-ui .table > tbody > tr.table-active > td { background: var(--fr-selected); }
@media (pointer: coarse) { .fr-ui .table > tbody > tr > td { padding-top: 9px; padding-bottom: 9px; } }

/* ══════════════════════════════════════════════════════════════════════
   BR-07 · DESPLEGABLE BOOTSTRAP
   SOURCE  .dropdown-menu / .dropdown-item / .dropdown-header / -divider
   TARGET  .fr-menu (06-patterns.css §6.1)
   SCOPE   .fr-shell (el menú de usuario de la topbar) y .fr-ui
   GATE    cuando el shell use FR.layers para el menú de usuario        */
body.fr-shell .dropdown-menu,
.fr-ui .dropdown-menu {
  background: var(--fr-m3-bg);
  -webkit-backdrop-filter: blur(var(--fr-m3-blur)) saturate(var(--fr-m3-sat));
  backdrop-filter: blur(var(--fr-m3-blur)) saturate(var(--fr-m3-sat));
  border: 0; border-radius: var(--fr-r-float); padding: 6px;
  box-shadow: var(--fr-edge-out), var(--fr-edge-in), var(--fr-sh-menu);
  color: var(--fr-text);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  body.fr-shell .dropdown-menu, .fr-ui .dropdown-menu { background: var(--fr-m3-solid); }
}
/* El puente tenía el fallback de `@supports` pero NO el modo de rendimiento:
   con `[data-fr-effects="reduced"]` el producto entero apagaba su vidrio y
   este menú se quedaba con `blur(14px)` permanente. Medido con el navegador
   al comprobar el coste de composición de la Home: 14 vidrios activos, 1
   superviviente, y era éste. Preexistente y ajeno a la Home; se corrige aquí
   porque el modo existe justo para el equipo de planta antiguo. */
.fr-nobf body.fr-shell .dropdown-menu,
.fr-nobf .fr-ui .dropdown-menu,
[data-fr-effects="reduced"] body.fr-shell .dropdown-menu,
[data-fr-effects="reduced"] .fr-ui .dropdown-menu {
  background: var(--fr-m3-solid);
  -webkit-backdrop-filter: none; backdrop-filter: none;
}
body.fr-shell .dropdown-item,
.fr-ui .dropdown-item {
  border-radius: 7px; min-height: 30px; padding: 5px 9px;
  display: flex; align-items: center; gap: 9px;
  font-size: var(--fr-t-small); color: var(--fr-text);
  transition: background var(--fr-d-micro) var(--fr-ease);
}
body.fr-shell .dropdown-item:hover,
.fr-ui .dropdown-item:hover { background: rgba(0, 48, 59, 0.07); color: var(--fr-text); }
body.fr-shell .dropdown-item:active,
.fr-ui .dropdown-item:active { background: var(--fr-teal); color: var(--fr-cream); }
body.fr-shell .dropdown-header,
.fr-ui .dropdown-header {
  font-size: var(--fr-t-micro); font-weight: var(--fr-fw-bold);
  letter-spacing: var(--fr-ls-wide); text-transform: uppercase; color: var(--fr-text-3);
  padding: 6px 9px 2px;
}
body.fr-shell .dropdown-divider,
.fr-ui .dropdown-divider { border-top: 1px solid var(--fr-line-1); margin: 5px 4px; opacity: 1; }
@media (pointer: coarse) {
  body.fr-shell .dropdown-item, .fr-ui .dropdown-item { min-height: var(--fr-touch-min); }
}

/* Menú de usuario de la topbar: altura segura y anclaje al viewport.
   Estas reglas vivían en un `<style>` dentro de partials/_topbar.html —una
   isla de 30 líneas en una plantilla— y se traen aquí porque describen un
   comportamiento reutilizable, no una excepción de esa pantalla.

   El motivo original se conserva: el toggle vive dentro de un contenedor
   `position:absolute` con `max-width: calc(50% - 8px)` anclado a `right:2%`;
   en viewports estrechos el menú de Bootstrap se desbordaba por la derecha.
   Se fija al viewport en vez de al contenedor restringido. El `top` deriva
   de la altura real de la barra, así que sigue a la barra sin ajuste
   manual, y el notch lateral se suma en apaisado. */
body.fr-shell .fr-topbar-menu {
  max-height: calc(100vh - 60px);           /* respaldo para Safari < 15.4 */
  max-height: var(--fr-menu-maxh);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
}
@media (max-width: 991.98px) {
  body.fr-shell .fr-topbar-menu {
    position: fixed;
    top: calc(var(--fr-topbar-offset) + 4px);
    right: max(2%, 8px, var(--fr-safe-right));
    left: auto;
    width: min(22rem, calc(100% - 16px));
    max-width: calc(100% - 16px);
  }
}
@media (max-width: 575.98px) {
  body.fr-shell .fr-topbar-menu {
    right: max(8px, var(--fr-safe-right));
    left: max(8px, var(--fr-safe-left));
    width: auto;
  }
}

/* ══════════════════════════════════════════════════════════════════════
   BR-08 · TARJETAS, ETIQUETAS Y AVISOS BOOTSTRAP
   SOURCE  .card / .badge / .alert / .pagination
   TARGET  .fr-card / .fr-chip / .fr-alert / .fr-pagination
   SCOPE   .fr-ui
   GATE    migración de la familia de pantalla                          */
.fr-ui .card {
  background: var(--fr-raised);
  border: 1px solid var(--fr-line-1); border-radius: var(--fr-r-card);
  box-shadow: var(--fr-sh-card); color: var(--fr-text);
}
.fr-ui .card-header { background: transparent; border-bottom: 1px solid var(--fr-line-1); padding: var(--fr-s4) var(--fr-s5); }
.fr-ui .card-body { padding: var(--fr-s6); }
.fr-ui .badge {
  border-radius: 999px; padding: 2px 8px; min-height: 20px;
  display: inline-flex; align-items: center; gap: 5px;
  font-size: var(--fr-t-micro); font-weight: var(--fr-fw-med);
}
.fr-ui .alert {
  border-radius: var(--fr-r-card); padding: var(--fr-s4) var(--fr-s5);
  font-size: var(--fr-t-small); border-width: 1px;
}
.fr-ui .alert-success { background: #e3ebe1; border-color: #a9c4a6; color: #245430; }
.fr-ui .alert-warning { background: #f1e6cb; border-color: #d3b878; color: #6f4700; }
.fr-ui .alert-danger  { background: #f2dad4; border-color: #d49a90; color: #7f221a; }
.fr-ui .alert-info    { background: #dde7e9; border-color: #9fbcc2; color: #114a56; }
.fr-ui .page-link {
  border: 1px solid var(--fr-line-2); border-radius: var(--fr-r-ctl);
  background: var(--fr-raised); color: var(--fr-text-2);
  font-size: var(--fr-t-mini); min-height: 28px;
  display: inline-flex; align-items: center;
}
.fr-ui .page-item.active .page-link { background: var(--fr-ink); border-color: var(--fr-ink); color: var(--fr-cream); }
@media (pointer: coarse) { .fr-ui .page-link { min-height: var(--fr-touch-min); min-width: var(--fr-touch-min); justify-content: center; } }

/* ══════════════════════════════════════════════════════════════════════
   BR-09 · ICONOGRAFÍA EN CONVIVENCIA
   SOURCE  .bi (Bootstrap Icons, 1.093 usos) · .fa* (Font Awesome, 420)
   TARGET  .fr-icon + sprite icons/forte-icons.svg (05-components §5.1)
   SCOPE   .fr-ui
   GATE    ICON_MIGRATION_MAP.csv al 100 % en la familia de pantalla
   ──────────────────────────────────────────────────────────────────────
   05A NO recorre los 1.513 usos. Lo que hace es normalizar su MÉTRICA
   para que un icono legacy no desentone junto a uno del sistema: mismo
   tamaño óptico relativo a la línea de texto y mismo color heredado. El
   dibujo sigue siendo el de su librería hasta que 05B lo sustituya.    */
.fr-ui .bi::before,
.fr-ui .fa::before, .fr-ui .fas::before, .fr-ui .far::before,
.fr-ui .fab::before, .fr-ui .fa-solid::before, .fr-ui .fa-regular::before {
  vertical-align: -0.08em;
}
.fr-ui .btn > .bi,
.fr-ui .btn > i[class*="fa-"] { font-size: 1.05em; line-height: 1; }
.fr-ui .dropdown-item > .bi,
.fr-ui .dropdown-item > i[class*="fa-"] { color: var(--fr-text-3); width: 16px; text-align: center; flex: 0 0 auto; }

/* ══════════════════════════════════════════════════════════════════════
   BR-10 · CONTENEDOR CRUD LG — RETIRADO en 05B · ola 12
   ══════════════════════════════════════════════════════════════════════
   SOURCE  .lg-crud-shell (css/lg-shell.css) · .lg-scroll-area (lg-tokens.css)
   TARGET  .fr-page + primitivas de scroll (04-layout.css §4.2/§4.3)
   GATE    «cuando NINGUNA plantilla se quede con el contenedor legacy»
   CIERRE  05B · ola 12 — consumidores vivos: 4 -> 0

   El puente existía para reconciliar la altura del contenedor CRUD legacy
   con las reservas del shell: `.lg-crud-shell` calculaba
   `100dvh - navbar` y el body acumulaba barra y pie por su cuenta, así que
   sin el descuento de 28 px aparecía un scroll exterior en trece CRUD.

   Sus consumidores se fueron ola a ola: 18 plantillas de F03 en la 2, las
   dos de F07 en la 3, y en la 12 las cuatro últimas —`maqbd_inventario`
   y `maqbd_catalogo_tecnico`, `moldebd_nueva_estructura` y la superficie
   muerta `polimaquina_crud`—. `.lg-scroll-area` aguantó un consumidor más,
   la consola de incidencias de F09, que pasó a `.fr-page-body` en la misma
   ola.

   Con cero consumidores el bloque se retira entero, y con él la clase en
   `css/lg-shell.css`. El registro histórico queda en
   `docs/audits/FORTE_BRAND_05A/LEGACY_BRIDGE_MANIFEST.csv` y en
   `docs/audits/FORTE_BRAND_05B_WAVE_12/BR10_CLOSURE.md`.
   ══════════════════════════════════════════════════════════════════════ */

