@layer xone-reset, xone-base, xone-responsive, xone-layout, xone-components, xone-project;
@layer xone-reset {
  xone-island { display: block; }
  xone-combo { display: flex; }
  xone-combo[hidden] { display: none; }
  *, *::before, *::after { box-sizing: border-box; }
}
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/Roboto-Regular.ttf') format('truetype');
}
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/fonts/Roboto-Medium.ttf') format('truetype');
}
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/fonts/Roboto-Bold.ttf') format('truetype');
}
@layer xone-base {
  /* Layout primitives compartidas con el SPA (baselineCss.ts) vía
     @xone-web/css-engine → LAYOUT_PRIMITIVES_CSS (F1 §1.C, fuente única). */
  body { display: flex; flex-direction: column; align-items: stretch; min-height: 100dvh; margin: 0; }

/* El atributo `hidden` debe ganar SIEMPRE: el orchestrator lo togglea para
   `disablevisible` (paridad Android ControlsUtils.isPropertyVisible) y para
   grupos/tabs. Sin `!important`, `.xone-frame { display: flex }` (misma
   especificidad/capa) o `.xone-prop { display: flex }` pisan el `display:none`
   del UA y el elemento "oculto" sigue visible (bug modales flotantes Alivia). */
[hidden] { display: none !important; }

/* Overlay flotante "pass-through": un frame floating con fondo TOTALMENTE
   transparente (bgcolor alpha 00, p.ej. frmChatBot/frmBot cuando su contenido
   esta vacio/comentado o el iframe lo bloquea la CSP) es una caja invisible que,
   por defecto, seguiria interceptando los clicks sobre los controles de debajo
   (tapaba el boton "Volver" de las fichas: el back no funcionaba con el chatbot
   abierto). Lo marcamos data-xone-passthrough (frameRenderer, solo floating +
   alpha 00): su caja y sus contenedores vacios NO capturan puntero; el
   contenido interactivo real (islas/iframe/inputs/botones/enlaces) re-activa
   pointer-events, asi un panel transparente CON contenido sigue siendo usable. */
[data-xone-passthrough] { pointer-events: none; }
[data-xone-passthrough] xone-island,
[data-xone-passthrough] iframe,
[data-xone-passthrough] input,
[data-xone-passthrough] textarea,
[data-xone-passthrough] select,
[data-xone-passthrough] button,
[data-xone-passthrough] a,
[data-xone-passthrough] .xone-widget-host { pointer-events: auto; }

/* ── Canvas — superficie raíz XOne (sistema responsive) ──
   container-type: size (no inline-size) expone cqw (consumido por
   --xone-p-unit en responsive) y cqh. El cqh es la referencia de ALTURA raíz
   (= la pantalla): un frame de nivel superior con height N% emite Ncqh y
   resuelve contra el alto del canvas (paridad ContentFramePage.java:344-345). */
.xone-canvas {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    flex: 1 1 auto;
    /* ALTURA DEFINIDA = la pantalla (paridad Android: el root es el screen).
       Debe ser height (no solo min-height): las cascadas height:N% de los
       frames de nivel superior necesitan un alto de padre DEFINIDO para
       resolver — min-height NO cuenta como definido para % en CSS, así que
       sin esto toda la cadena height:100% colapsaba a la altura del contenido
       (bug paridad login/inicio 2026-05-30). dvh es absoluto (relativo al
       viewport), así que es definido sin depender de <body>. El scroll de
       contenido que desborda va en frames con scroll="true", igual que Android. */
    height: 100dvh;
    min-height: 100dvh;
    width: 100%;
    /* RECORTE A PANTALLA (paridad Android): el View system recorta los hijos a
       los bounds del padre (clipChildren default) y la pantalla NUNCA crece —
       el contenido que excede solo scrollea dentro de frames scroll="true"
       (ContentFramePage.java:344-353 dimensiona el root frame a
       nParentWidth/nParentHeight de la pantalla). Sin esto, un árbol cuyo
       diseño suma >100dvh (p.ej. Inicio mRed: header 8cqh + frmGeneral2 98cqh
       + márgenes) extiende el scrollHeight del body (1027px vs 900 de la
       referencia). */
    overflow: clip;
    container-type: size;
    container-name: xoneCanvas;
}

/* Modo responsive base-FIJA (data-responsive="1" = web-resolution-width > 0).
   Tokens responsive: 1p = (1/base-width)*100cqw; 1 fontsize = font-factor * idem.
   La base la hornea el generador en --xone-base-width.

   Modo responsive base-AUTO (data-responsive="auto" = sin resolución declarada):
   NO se declara --xone-p-unit aquí (la base es desconocida pre-JS). El
   canvasSizer mide el tamaño natural del contenido, fija --xone-base-width y
   sobrescribe --xone-p-unit/--xone-font-unit inline. Sin JS, var(...,1px) cae a
   1px = 1p=1px = tamaño natural del diseño (sin zoom). */
.xone-canvas[data-responsive="1"] {
    min-height: 100dvh;
    --xone-p-unit: calc(100cqw / var(--xone-base-width));
    --xone-font-unit: calc(var(--xone-font-factor) * 100cqw / var(--xone-base-width));
}
/* Modos de canvas y alineación: aplican a ambos modos responsive (la base
   --xone-base-width que necesitan fixed/letterbox la hornea el generador en
   "1" y la fija el canvasSizer tras medir en "auto"). */
.xone-canvas:is([data-responsive="1"],[data-responsive="auto"])[data-canvas-mode="fluid"] {
    width: min(100vw, var(--xone-canvas-max-width, 540px));
    align-self: center;
}
.xone-canvas:is([data-responsive="1"],[data-responsive="auto"])[data-canvas-mode="viewport"] {
    width: 100vw;
    max-width: none;
    align-self: stretch;
}
.xone-canvas:is([data-responsive="1"],[data-responsive="auto"])[data-canvas-mode="fixed"] {
    width: calc(var(--xone-base-width) * 1px);
    height: calc(var(--xone-base-height) * 1px);
    flex: 0 0 auto;
    min-height: 0;
    align-self: center;
}
.xone-canvas:is([data-responsive="1"],[data-responsive="auto"])[data-canvas-mode="letterbox"] {
    aspect-ratio: var(--xone-base-width) / var(--xone-base-height);
    width: min(100vw, calc(100dvh * var(--xone-base-width) / var(--xone-base-height)));
    /* height:auto deja que aspect-ratio gobierne el alto (la base ahora fija
       height:100dvh, que rompería la relación de aspecto si no se neutraliza). */
    height: auto;
    flex: 0 0 auto;
    min-height: 0;
    align-self: center;
}
.xone-canvas:is([data-responsive="1"],[data-responsive="auto"])[data-canvas-align="left"]   { align-self: flex-start; }
.xone-canvas:is([data-responsive="1"],[data-responsive="auto"])[data-canvas-align="right"]  { align-self: flex-end; }
.xone-canvas:is([data-responsive="1"],[data-responsive="auto"])[data-canvas-align="center"] { align-self: center; }

/* ── Coll / Group / Frame: flex column por defecto ── */
.xone-coll, .xone-group, .xone-frame {
    display: flex;
    flex-direction: column;
    align-items: stretch;
}
/* La coll = la pantalla (no puede crecer más allá del canvas): height definido
   en vez de min-height para que la página mida exactamente la pantalla y el
   scroll viva en los frames scroll="true" (overflow:auto emitido por el
   lowering, visualStyle.ts:332-333 / applyVisualAttributes §1b), igual que en
   Android, donde nada se pinta fuera de pantalla. min-height:100% (anterior)
   dejaba crecer la coll y extendía el body (divergencia docHeight 1027 vs 900). */
.xone-coll { flex: 1 1 auto; height: 100%; min-height: 0; }
/* Group raíz = MATCH_PARENT (paridad EditGroupView.java:104). Los groups
   inactivos (tabs) van con [hidden]/display:none y no consumen flex. */
.xone-coll > .xone-group { flex: 1 1 auto; min-height: 0; }
/* position: relative para floating labels / overlays anclados al frame.
   min-width: 0 (espejo de min-height:0): un frame es un flex item cuyo minimum
   automatico (min-width:auto = min-content) le impedia encogerse por debajo del
   ancho intrinseco de su contenido. En una cadena de frames anidados con width
   en porcentaje y texto ENVOLVIBLE largo (WEAL-143: burbujas de chat de
   Mensajeria, MAP_MENSAJE con width 96 -> 90 -> 89 por ciento anidados), ese
   suelo de min-content propagaba el max-content hacia arriba y el arbol entero
   se dimensionaba por el texto SIN envolver (2483px en un panel de 699px ->
   scroll horizontal). Con min-width:0 la cadena puede encogerse a su ancho real
   del panel y el texto envuelve, igual que min-height:0 ya permite en el eje
   vertical. Paridad Android: un LinearLayout con hijos MATCH/porcentuales nunca
   crece mas que su padre (recorta/envuelve). */
.xone-frame { min-height: 0; min-width: 0; position: relative; }
/* WEAL-98 — paridad Android clipChildren=true en contenedores EXACTLY. Un frame
   con altura literal (Np/N% distinta de 100%; el lowering/runtime le añade la
   clase xone-exactly-clip junto al container-type:size) nunca crece con su
   contenido: Android recorta en dispatchDraw lo que exceda la caja, así que el
   default web correcto es clip (no visible, que dejaba al contenido sangrar
   sobre las filas vecinas de un listado — MensajeriaChatListado frmContent 60p).
   :where = especificidad 0: cualquier clase del proyecto (p.ej. .frmCuerpo con
   overflow-y:auto, WEAL-96) o estilo inline (scroll="true", radius) gana a este
   default. clip y no hidden para no crear un scroll container (un focus interno
   no puede desplazar el contenido del frame). */
:where(.xone-frame.xone-exactly-clip) { overflow: clip; }

/* ── Row wrapper: agrupa hijos consecutivos con newline="false" ── */
.xone-row {
    display: flex;
    flex-direction: row;
    align-items: stretch;
    align-self: stretch;
    width: 100%;
    gap: 0;
    flex: 0 0 auto;
}
/* La fila única de un group crece a su contenido (canónico: 1 1 auto). */
.xone-group > .xone-row { flex: 1 1 auto; }
/* La row única de un frame con alto explícito ocupa todo ese alto. */
.xone-frame[style*="height"] > .xone-row:only-child { flex: 1 1 auto; }
/* …salvo que el frame declare un align vertical: entonces el bloque de líneas
   NO debe estirarse a todo el alto, sino conservar su tamaño para que el
   posicionamiento vertical (top/center/bottom) lo coloque dentro del alto
   sobrante. Paridad Android EditFramePage.java:936 (vMainView.setGravity(sAlign)
   sobre LinearLayout VERTICAL: posiciona el bloque de líneas, no lo estira).
   P0-B: se pasa del frágil [style*="justify-content"]:only-child (que fallaba
   con varias filas y dependía del substring inline) a [data-xone-align*=...]
   emitido por frameRenderer, válido con N filas. */
.xone-frame[data-xone-align*="top"]    > .xone-row,
.xone-frame[data-xone-align*="bottom"] > .xone-row,
.xone-frame[data-xone-align*="center"] > .xone-row { flex: 0 0 auto; }
/* …EXCEPCIÓN: si el único contenido de la fila es una imagen full-bleed
   (cover/fill = imagen de fondo, p.ej. el fondo del login con width:100%
   height:100%), esa imagen es MATCH_PARENT y DEBE llenar el alto ignorando la
   gravity (en Android un hijo MATCH_PARENT llena; la gravity solo posiciona
   hijos WRAP_CONTENT). La fila vuelve a 1 1 auto para que height:100% de la
   imagen resuelva al alto del frame. Las imágenes contain (keep-aspect, p.ej.
   un logo) NO entran aquí → siguen centradas por gravity. */
.xone-frame[data-xone-align] > .xone-row:has(> img[style*="object-fit: fill"]:only-child),
.xone-frame[data-xone-align] > .xone-row:has(> img[style*="object-fit: cover"]:only-child) { flex: 1 1 auto; }
/* Frame de alto completo dentro de una fila horizontal: el ALTO lo da la
   resolución height:100% (ahora que el canvas tiene alto definido) o el
   align-items:stretch de la fila; el ANCHO lo da su atributo width (40%/60%…).
   ANTES esto era flex:1 1 0, que en una fila reparte el eje principal
   (ANCHO) a partes iguales con basis 0 → aplastaba el width% (p.ej. 40/60 se
   volvía 50/50). flex:0 0 auto respeta el width inline (bug 2026-05-30). */
.xone-row > .xone-frame[style*="height: 100%"] { flex: 0 0 auto; }

/* ── Prop: bloque vertical (stack), ocupa ancho del padre por defecto ── */
.xone-prop { display: flex; flex-direction: column; width: 100%; min-height: 0; }
.xone-prop-tl { display: block; width: 100%; }

/* ── Pestañas de grupos (F2): modelo "un grupo visible" ──
   El chasis (un <xone-island>) es flex column: [barra 0 0 auto][grupos 1 1 auto].
   La barra se omite en SSR si hay <2 grupos normales o notab (R1). El JS solo
   conmuta hidden/aria-selected; el layout y el primer grupo funcionan sin JS. */
.xone-tabs-chassis { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.xone-tabs {
    display: flex; flex-direction: row; flex: 0 0 auto;
    overflow-x: auto; scrollbar-width: thin;
    background: var(--xone-tab-bg, transparent);
}
.xone-tabs[data-tab-orientation="bottom"] { order: 2; }
.xone-tab {
    /* FIX-6: WRAP_CONTENT empaquetado a la izquierda — un prop/botón sin
       width jamás recibe MATCH_PARENT ni reparto implícito (paridad
       EditFramePage.java:1042: lp WRAP_CONTENT; misma regla que imágenes
       XOneImageView.java:271-281). Antes: flex 1 1 0 (reparto equitativo
       no canónico que estiraba cada tab a 1/N del ancho). */
    flex: 0 0 auto;
    min-height: var(--xone-tab-height, auto);
    font-size: var(--xone-tab-fontsize, inherit);
    background: transparent;
    color: var(--xone-tab-fg, inherit);
    border: 0; border-bottom: 2px solid transparent;
    padding: 0.6em 1em; cursor: pointer; white-space: nowrap;
    font-family: inherit;
}
/* Si el proyecto declara tab-width, las pestañas pasan a ancho fijo + scroll. */
.xone-tabs[style*="--xone-tab-width"] .xone-tab { flex: 0 0 var(--xone-tab-width); }
.xone-tab[aria-selected="true"] {
    color: var(--xone-tab-fg-selected, var(--xone-tab-fg, inherit));
    border-bottom-color: var(--xone-tab-indicator, currentColor);
}
.xone-tab:focus-visible { outline: 2px solid var(--xone-tab-indicator, currentColor); outline-offset: -2px; }
.xone-groups { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.xone-groups > .xone-group { flex: 1 1 auto; min-height: 0; }
.xone-group[hidden] { display: none; }

/* ── Grupos fijos (F3): bandas ancladas que restan espacio al centro ──
   El chasis es flex: las bandas (flex:0 0 auto) miden su contenido (WRAP) y el
   centro (flex:1 1 auto) ocupa el resto. Reproduce "pantalla − fijos − tabs" sin
   position:fixed. Banda oculta (disablevisible) → 0 espacio. */
.xone-coll-mid { display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
/* El centro NO-fijo del chasis de grupos fijos. container-type:size lo vuelve
   la referencia de ALTURA de los frames de pestaña: un frame raíz con height=N%
   emite Ncqh (pctHeightToCqh) y resuelve contra el ALTO RESTANTE del centro
   (pantalla − bandas fixed), no contra el coll completo (que incluye la banda
   fixed). Replica el "área restante" de Android (EditViewHyper.mapFixedGroupViews:
   los fixed miden contra el contenedor completo y el contenido no-fixed recibe
   el sobrante). Sin esto, 90cqh medía contra el coll y desbordaba el viewport
   por exactamente la altura de la banda fixed x N% (WEAL-30). Seguro para el
   escalado responsive: canvasSizer fija --xone-p-unit como px sobre el canvas
   (no cqw), igual que ya hacen .xone-canvas y los frames containerizados. */
.xone-coll-center { display: flex; flex-direction: column; flex: 1 1 auto; min-width: 0; min-height: 0; container-type: size; }
.xone-fixed { display: flex; flex-direction: column; flex: 0 0 auto; }
.xone-fixed-left, .xone-fixed-right { min-width: 0; }
.xone-fixed[hidden] { display: none; }
/* Banda con un grupo fixed de altura de relleno (height=100% / -1). El emisor
   (fixedGroupsRenderer) le pone esta clase para que la banda CREZCA en vez de
   medir contenido: así el height:100% del grupo resuelve contra una caja con
   alto real y no colapsa a 0. Paridad Android "fixed group = alto disponible"
   (EditViewHyper.createFixedGroup mide contra getMaxScreenHeight()). Caso raro:
   comparte el espacio del coll con el centro en vez de taparlo por completo. */
.xone-fixed-fill { flex: 1 1 auto; min-height: 0; }

/* ── Grupos drawer (F4): panel off-canvas + scrim ──
   El contenido principal NO se desplaza (el drawer va position:fixed por encima).
   El JS solo togglea .is-open; el slide es por transform. width por grupo via
   --xone-drawer-width (default 100% = pantalla completa, paridad doc 01 §2.4.2). */
.xone-drawer {
    position: fixed; top: 0; bottom: 0; z-index: 1000;
    width: var(--xone-drawer-width, 100%); max-width: 100%;
    display: flex; flex-direction: column;
    background: #fff; will-change: transform;
    transition: transform 0.25s ease; overscroll-behavior: contain;
}
.xone-drawer-left  { left: 0;  transform: translateX(-100%); }
.xone-drawer-right { right: 0; transform: translateX(100%); }
.xone-drawer.is-open { transform: translateX(0); }
.xone-drawer[hidden] { display: none; }
.xone-drawer > .xone-group { flex: 1 1 auto; min-height: 0; }
.xone-drawer-scrim {
    position: fixed; inset: 0; z-index: 999;
    background: rgb(0 0 0 / 0.32);
    opacity: 0; transition: opacity 0.25s ease;
}
.xone-drawer-scrim.is-open { opacity: 1; }
.xone-drawer-scrim[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) {
    .xone-drawer, .xone-drawer-scrim { transition: none; }
}

  /* MPA: fuente por defecto del canvas (en el SPA la pone <body>). */
  .xone-canvas { font-family: 'Roboto', sans-serif; }

  /* Island = el box visible del control (paridad Android EditText/Button).
     :where() da especificidad (0,0,1) para que las clases del proyecto
     en @layer xone-project ganen sin esfuerzo. overflow:hidden evita que
     las esquinas del input rectangular asomen del border-radius. */
  /* text-border-*: el borde del SUBVIEW de texto (EditText en Android) SOLO
     aplica a props de entrada de texto editables (T, N, N#, X, D, DT). El
     css-engine traduce text-border / text-border-lado / text-border-color /
     text-border-width a custom properties (--xone-text-border*) que SOLO esta
     regla consume; en el resto de props (B, IMG, NC, TL, ...) quedan inertes y
     no pintan borde (paridad XoneCSSTextBox: text-border es del EditText, no del
     frame — el frame usa border-* sin prefijo).
     Default text-border=true → solid (XoneCSSTextBox.java:132). Anchura por
     defecto 1px (no medium/3px del navegador), ampliable con text-border-width o
     border-width. Color por defecto currentColor. :where() da especificidad
     (0,0,1) para que el CSS del proyecto en @layer xone-project gane. */
  :where(xone-island, xone-combo):where(.xone-prop-t, .xone-prop-x,
    .xone-prop-n, .xone-prop-n1, .xone-prop-n2, .xone-prop-n3,
    .xone-prop-n4, .xone-prop-n5, .xone-prop-n6,
    .xone-prop-d, .xone-prop-dt) {
    border-style:
      var(--xone-text-border-top, var(--xone-text-border, solid))
      var(--xone-text-border-right, var(--xone-text-border, solid))
      var(--xone-text-border-bottom, var(--xone-text-border, solid))
      var(--xone-text-border-left, var(--xone-text-border, solid));
    border-width: var(--xone-text-border-width, 1px);
    border-color: var(--xone-text-border-color, currentColor);
    /* Color/bg del subview de texto (EditText): SOLO en inputs editables.
       Fallback inherit/revert-layer: sin text-* el input usa el color del
       control (forecolor) y su bg normal. El input interno hereda. */
    color: var(--xone-text-forecolor, inherit);
    background-color: var(--xone-text-bgcolor, revert-layer);
  }

  /* Combo = control de selección (Spinner en Android), no EditText: por defecto
     NO pinta el underline de texto del EditText. El combo se emite como
     <xone-combo> conservando la clase por-tipo .xone-prop-t (renderCombo añade
     .xone-combo sin quitarla), así que matchea la regla text-border de arriba y
     hereda el default de proyecto  .xone-prop { --xone-text-border-bottom: solid }
     → pintaba una línea de 1px en el borde inferior (bug WEAL-31, visible en los
     combos dentro de un contents/propContent; los propEdicion/propCombo ya lo
     resetean a none). Reseteamos aquí las 4 text-border vars.
     El !important es NECESARIO: esta regla vive en @layer xone-base y el default
     del proyecto (.xone-prop) va en @layer xone-project, que GANA por ORDEN DE CAPA
     a cualquier especificidad; el important invierte esa precedencia (mismo motivo
     que el reset de fondo del NC más abajo). Un proyecto que quiera un combo CON
     underline puede reactivarlo con su propia declaración !important sobre el combo.
     Paridad Android: XoneCSSTextBox (text-border) es el borde del EditText; el
     Spinner no lo dibuja. WEAL-31. */
  .xone-prop.xone-combo {
    --xone-text-border-top: none !important;
    --xone-text-border-right: none !important;
    --xone-text-border-bottom: none !important;
    --xone-text-border-left: none !important;
  }

  /* Inner controls = llenan el island, heredan todo. NO contribuyen
     bordes/padding → una sola fuente visual (el island wrapper). */
  xone-island input, xone-island textarea, xone-island select,
  xone-combo select.xone-combo-native {
    width: 100% !important; height: 100%;
    padding: 0; border: none; border-radius: 0;
    background: transparent; color: inherit; font-family: inherit;
    font-size: inherit; outline: none;
    box-sizing: border-box;
  }
  /* Reset del <button> generado. Incluye TANTO los botones interactivos
     (envueltos en <xone-island class=xone-prop-b>) COMO los estáticos
     (envueltos en <div class=xone-prop-b>, p.ej. los botones de orden del header
     de contents). Sin esto, un <button> sin clase de fondo (.botonOrden solo
     declara width/height/keep-aspect-ratio) muestra el CHROME NATIVO del
     navegador: borde "2px outset", fondo gris "#efefef" y padding "1px 6px" =
     un recuadro alrededor del icono. Paridad XOne: el boton no pinta borde ni
     fondo salvo que su CSS lo declare (y eso vive en el wrapper). */
  .xone-prop-b > button {
    appearance: none; -webkit-appearance: none; width: 100%; height: 100%;
    padding: 0; border: none; border-radius: 0; margin: 0;
    background: transparent; color: inherit; font-family: inherit;
    font-size: inherit; cursor: pointer; outline: none;
    display: flex; align-items: center; justify-content: center;
    /* WEAL-163: min-width:0 permite que el <button> (flex item de su island)
       ENCOJA por debajo del ancho intrínseco de su texto, y overflow:hidden
       recorta el contenido que se salga de la caja del botón (altura fija
       height="20p") en lugar de dejarlo desbordar por arriba/abajo. Paridad
       Android XOneCSS.applyFormatToButtonV3 (recorte en el borde del botón). */
    min-width: 0; overflow: hidden;
  }

  /* WEAL-154: puente de alineacion para props type="B" (espejo del bloque de
     inputs de texto de mas abajo, y de xone-xml-renderer/baselineCss.ts:390-392
     para el pipeline cliente). El boton interno de un .xone-prop-b es display:flex
     con justify-content:center por defecto (regla base .xone-prop-b > button,
     arriba) y text-align:center por el UA. controlRenderer ya calcula y emite
     xone-align-h-(h) en el wrapper cuando el prop declara align (inline o via
     .class de proyecto, p.ej. .botonContent con align:left), pero —a diferencia de
     los inputs— no habia ninguna regla que propagase ese align al boton, asi que el
     texto quedaba SIEMPRE centrado en cuanto el boton era mas ancho que su
     contenido (bug real en Alivia MAP_BT_CLIENTE, Listado de deudas). La clase de
     align y xone-prop-b viven en el MISMO elemento wrapper, por eso el selector es
     COMPUESTO (sin espacio), no descendente. Especificidad (0,2,1) > (0,1,1) de la
     regla base .xone-prop-b > button gana sin !important. */
  .xone-align-h-left.xone-prop-b > button { justify-content: flex-start; text-align: left; }
  .xone-align-h-center.xone-prop-b > button { justify-content: center; text-align: center; }
  .xone-align-h-right.xone-prop-b > button { justify-content: flex-end; text-align: right; }

  /* WEAL-163 — texto del botón (<span class="xone-btn-text">).
     Por DEFECTO una sola línea con truncado por elipsis: paridad Android
     (xonecss_lib XoneCSS.java:386-391 → bLabelWrap=false por defecto →
     setMaxLines(1)+setSingleLine(true); XoneCSSButton.java:99 default false).
     Sin esta regla el navegador aplica white-space:normal y el título envuelve
     a 2+ líneas, generando un <span> más alto que un botón de altura fija que
     desborda/scrollea dentro del pill (bug reportado en MAP_TL_BUSCADOR_AVANZADO,
     height="20p"). Mismo patrón ya usado por .xone-combo-display/.xone-combo-option
     y .xone-chart-legend-label. min-width:0 es necesario para que, como flex item
     del <button>, el span pueda encoger y el text-overflow:ellipsis surta efecto. */
  .xone-btn-text {
    min-width: 0; max-width: 100%;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }

  /* label-wrap="true" EXPLÍCITO en el .xne → permite multilínea (paridad
     bLabelWrap=true: el TextView nativo envuelve). Se limita a --xone-lines
     líneas si el prop declara altura por líneas (mismo mecanismo -webkit-line-clamp
     que .xone-label-text.xone-label-clamp); sin --xone-lines, envuelve libremente
     acotado por el overflow:hidden del botón. */
  .xone-btn-text.xone-btn-text-wrap {
    white-space: normal; overflow: hidden; text-overflow: clip;
    display: -webkit-box; -webkit-box-orient: vertical;
    -webkit-line-clamp: var(--xone-lines, unset);
  }

  /* #312 — label-wrap="false" EXPLICITO en un prop de texto (TL/T) => una sola
     linea con elipsis, que es el comportamiento de Android
     (XoneCSSTextBox.java:115 -> bLabelWrap default false -> setSingleLine).
     Se aplica tanto al wrapper (texto directo, caso sin "lines") como al
     .xone-label-text interior (caso con "lines", donde el texto va en un span).
     El clamp se anula: label-wrap="false" + lines=N es contradictorio y en
     Android manda el single-line.
     OJO: la AUSENCIA del atributo NO cae aqui: el default sigue siendo envolver.
     Ver la nota de controlRenderer.renderLabel y la ficha del catalogo.
     NO USAR BACKTICKS EN ESTE FICHERO: este bloque CSS vive dentro de un
     template literal de TypeScript y un backtick lo cierra a media cadena. */
  .xone-prop-tl.xone-label-nowrap,
  .xone-prop-tl.xone-label-nowrap .xone-label-text {
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    display: block; -webkit-line-clamp: unset;
  }

  /* Checkbox / radio (NC). El reset "xone-island input { width:100% !important }"
     de arriba estira el check a todo el ancho del island (p.ej. 11%) y su label
     cae a la linea siguiente. Paridad Android XOneCheckBox: el control es un
     CheckBox de tamano fijo con el texto AL LADO (a la derecha), centrado en
     vertical, en una sola Line (ControlsUtils/EditFramePage). Reproducimos:
     el <label> es un flex-row (check + texto), y el check recupera su tamano
     intrinseco (18px) con override de mayor especificidad que gana al reset. */
  .xone-prop-nc > label {
    display: flex; align-items: center; gap: 8px;
    width: 100%; cursor: pointer;
  }
  xone-island.xone-prop-nc input[type="checkbox"],
  xone-island.xone-prop-nc input[type="radio"] {
    /* WEAL-133 — el reset de arriba fija la CAJA del control a 18x18px, pero sin
       appearance:none el PINTADO seguía delegado al tema nativo del SO/navegador
       (appearance:auto por defecto del user-agent). En ciertas combinaciones
       SO/navegador/entrada (Chrome sobre Windows/NativeThemeWin del reportante) el
       tema nativo dibuja affordances extra tipo 'stepper'/spin junto a controles
       pequeños que EXCEDEN la caja de 18px y fuerzan scroll. La cura correcta por
       construcción es quitar el tema nativo (appearance:none) y pintar el check con
       CSS propio, así el control es 18x18px exactos en cualquier entorno. Continúa
       la serie de hardening de checkboxes NC WEAL-65/82/105/121. */
    appearance: none; -webkit-appearance: none;
    /* WEAL-133 (reapertura) — cinturon de seguridad: min-height/padding con
       !important (como width) para que ninguna capa de proyecto (@layer
       xone-project) inyecte alto/relleno interno al check que lo desplace o
       desborde su caja de 18px. margin se deja SIN !important a proposito: el
       inset intrinseco de 5px (WEAL-105, regla .xone-prop-nc > input) es un
       margin-top y debe poder aplicarse; la caja lo absorbe porque una altura
       fija de NC ya se emite como min-height (visualStyle.ts), no como height
       tope, asi que el margin nunca recorta ni fuerza scroll. */
    width: 18px !important; height: 18px;
    min-height: 0 !important; padding: 0 !important; margin: 0; flex: 0 0 auto;
    box-sizing: border-box;
    border: 1px solid currentColor;
    background-color: transparent;
    background-position: center; background-repeat: no-repeat;
    cursor: pointer;
  }
  xone-island.xone-prop-nc input[type="checkbox"] { border-radius: 3px; }
  xone-island.xone-prop-nc input[type="radio"] { border-radius: 50%; }
  /* Marca de check custom (appearance:none deja de dibujar la marca nativa aunque
     haya accent-color): fondo con el color de texto + tick SVG blanco encima. */
  xone-island.xone-prop-nc input[type="checkbox"]:checked {
    background-color: currentColor;
    background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2016%2016'%3E%3Cpath%20fill='none'%20stroke='%23fff'%20stroke-width='2.5'%20stroke-linecap='round'%20stroke-linejoin='round'%20d='M3.5%208.5l3%203%206-7'/%3E%3C/svg%3E");
    background-size: 14px 14px;
  }
  /* Radio marcado: punto central con el color de texto. */
  xone-island.xone-prop-nc input[type="radio"]:checked {
    background-image: radial-gradient(currentColor 0 42%, transparent 48%);
  }
  .xone-prop-nc .xone-checkbox-label { flex: 0 1 auto; min-width: 0; }
  /* WEAL-82 — el island NC no debe recortar el check cuando la suma de anchos
     de una fila (etapas con muchos props, p.ej. un NC width="2%") supera el
     100% y el flex reparte el déficit encogiendo cada caja. Una clase de
     edición con border-corner-radius (p.ej. .propEdicion) emite overflow:hidden
     en el island (paridad Android setClipToOutline, transformer.ts). Por spec
     CSS, un flex item con overflow != visible tiene su "automatic minimum size"
     = 0 (no min-content), así que el island podía encogerse por debajo de los
     18px del check y recortarlo. Restauramos el suelo con min-width:min-content:
     el control atómico (check 18px, flex:0 0 auto) siempre se muestra entero,
     mientras el label (flex:0 1 auto; min-width:0) sigue pudiendo encoger. */
  xone-island.xone-prop-nc { min-width: min-content; }
  /* WEAL-105 — inset superior intrínseco del check (paridad Android XOneCheckBox).
     En Android el CompoundButton se añade al XOneCheckBox con
     LayoutParams(WRAP_CONTENT, WRAP_CONTENT) y gravity START|CENTER_VERTICAL
     (XOneCheckBox.java:556-558), y — a diferencia de XOneEditText (:634,721,724)
     y XOneSpinner (:598,684), que hacen setPadding(0,0,0,0) explícito sobre sus
     vistas internas — el AppCompatCheckBox CONSERVA el padding/inset nativo de su
     button-drawable (el único setPadding de XOneCheckBox, :917-922, reescribe solo
     el padding IZQUIERDO al asignar imágenes custom y preserva el paddingTop).
     Resultado en Android: la marca visible del check queda INSET respecto al borde
     superior de la caja del prop. En web el control es un <input type=checkbox> de
     18px pegado al borde superior del island, sin ese inset — así que el tmargin se
     traduce literal y el check aparece MÁS ARRIBA que sus hermanos de fila (combo,
     fecha), que sí anulan su padding y por eso el .xne les da 1% más de tmargin.
     Reproducimos el inset desplazando el CONTENIDO del island (check + su texto,
     que en Android son UN solo view y por tanto se mueven juntos). Es un offset del
     control, NO del layout: no tocamos margin/align-self (visualStyle.ts), que son
     genéricos a todos los tipos de prop. Se aplica a las dos formas del NC en SSR:
     con título (<label>) y sin título (<input> suelto, controlRenderer.ts:623-626).
     VALOR (5px): es una constante de PLATAFORMA (el inset vive en el widget, no en
     el contenedor) → se expresa en px, NUNCA en %/cqh. Modelo: el button-drawable
     del AppCompatCheckBox mide 32dp de caja con la marca visible centrada dentro,
     así que el inset superior es (32 - marca)/2 ≈ 5-7dp según la métrica exacta de
     la marca (18-22dp); el <input type=checkbox> web mide 18px de marca visible.
     Calibrado dentro de ese rango contra las filas reales de ProcesoLSOEdicion en
     el gate de QA (probe weal105), cuyos tmargin fueron a su vez calibrados contra
     Android: las filas con 1% de diferencia (etapa 13, check 3% vs fecha 4%) piden
     ~7px y la de 0.5% (etapa 10, check 3.5%) pide ~3.5px; 5px es el valor que
     minimiza el error de peor caso (≤ ~2px en ambas). Medidas: con 4px el residuo
     era ~3.4px en las filas de 1%; con 7px se invertía a ~3.5px en la de 0.5%. */
  xone-island.xone-prop-nc > label,
  xone-island.xone-prop-nc > input[type="checkbox"],
  xone-island.xone-prop-nc > input[type="radio"] { margin-top: 5px; }
  /* Paridad Android XOneCheckBox: el control NO pinta fondo — el CompoundButton
     hace setBackground(null) (XOneCheckBox.java:449) y el contenedor LinearLayout
     no aplica bgcolor. En web, una clase de edición genérica aplicada a un NC
     (p.ej. .propEdicion{background:#F2F2F2}, .propContent) pintaba una BANDA de
     color al ancho del island (9-11%) detrás del check pequeño (bug T06).
     El important es NECESARIO: esta regla vive en @layer xone-base y el CSS del
     proyecto (con la clase de fondo) va en @layer xone-project, que gana por ORDEN
     DE CAPA a cualquier especificidad; el important invierte esa precedencia (igual
     que el reset de ancho del check de arriba). */
  xone-island.xone-prop-nc { background-color: transparent !important; }

  /* Password toggle — position absolute dentro del field-floating.
     Selector explícito para que no lo machaque ningún reset de button. */
  .xone-password-toggle {
    position: absolute; right: 0.3em; top: 50%; transform: translateY(-50%);
    width: auto; height: auto;
    background: transparent; border: none; cursor: pointer; color: #9AA0A6;
    padding: 0.3em; border-radius: 50%; line-height: 0;
    display: inline-flex; align-items: center; justify-content: center;
  }
  .xone-password-toggle svg { width: 1.4em; height: 1.4em; display: block; }
  .xone-password-toggle:hover { color: #2D3436; }
  /* Foco: SIN halo. El canal clasico XOne no pinta estilo de foco propio
     (referencia mRed: toda la cadena input->container->prop->row computa
     box-shadow:none en focus; el indicador es el underline constante).
     Paridad Android: EditText tampoco dibuja halo (G1 iter3). */

  /* Disabled state — custom properties from XOne CSS classes.
     revert-layer fallback: when no disabled color is declared, the property
     reverts to the previous @layer value instead of going IACVT → unset
     (which would reset background-color to transparent, breaking inputs
     that have text-bgcolor set). */
  .xone-prop input:disabled,
  .xone-prop textarea:disabled,
  .xone-prop select:disabled {
    background-color: var(--xone-text-bgcolor-disabled, var(--xone-bgcolor-disabled, revert-layer));
    color: var(--xone-text-forecolor-disabled, var(--xone-forecolor-disabled, revert-layer));
  }
  /* WEAL-85 — paridad Android setEnabled(false) en la ruta SSR/islands (la que
     SIRVE alivia.xonemanager.com). Un control deshabilitado (button/checkbox
     con el atributo disabled) agrisa su .xone-prop wrapper AUNQUE el marcador de
     clase xone-disabled no esté presente: el SSR emite disabled en el button pero
     NO la clase en el wrapper, y el runtime islands (setEditDisabledState) sólo
     togglea disabled en el control, no la clase. :has() cubre AMBOS caminos (primer
     paint SSR + toggle reactivo por fila/página) sin tocar JS. Los text inputs
     read-only/inert NO se agrisan a propósito (paridad Android: read-only NO-locked
     mantiene la apariencia; sólo disabled = setEnabled(false) agrisa).

     WEAL-121 — el agrisado se ACOTA POR TIPO XOne (clase xone-prop-TYPE del
     wrapper). La versión original de WEAL-85 incluía dos sub-reglas sobre
     .xone-prop (hijo directo SELECT deshabilitado, hijo directo INPUT
     deshabilitado) SIN condición de tipo: como ningún prop B/NC se pinta jamás
     como SELECT (B = BUTTON, NC = INPUT type=checkbox), esa sub-regla
     sólo alcanzaba a los combos type=T/N/D con linkedto/linkedfield — que usan
     "disabled" porque HTML no soporta SELECT readonly, NO porque estén
     "deshabilitados" en el sentido Android. Resultado: los combos T bloqueados por
     disableedit (p.ej. MAP_VIVIENDA_SITUACION en Alivia/ClienteEdicion) se veían
     atenuados mientras sus vecinos de texto plano (readonly+inert, mismo disableedit)
     se veían normales. Sólo B y NC agrisan; el resto de tipos, nunca.

     WEAL-121 (reapertura) — en los props NC el agrisado debe alcanzar SÓLO al
     cuadrado del check, no al caption/texto. El NC con título no vacío se emite
     como <label><input type=checkbox><span class="xone-checkbox-label">{title}</span></label>
     dentro del wrapper .xone-prop-nc; poner opacity:0.6 en el WRAPPER atenúa todo
     el subárbol (opacity es compositing, no herencia de color) incluyendo el
     <span> del texto. Se separa la regla: el WRAPPER conserva pointer-events:none
     (necesario porque el <label> nativo dejaría "clicable" el texto si sólo se
     bloquea el <input>), y opacity:0.6 se acota al control <input:disabled>. Para
     type=B (botón) no hay caption inline separado, así que su regla mantiene
     opacity+pointer-events a nivel de wrapper (WEAL-85 intacto). */
  .xone-prop-b:has(button:disabled) {
    opacity: 0.6;
    pointer-events: none;
  }
  .xone-prop-nc:has(input:disabled) {
    pointer-events: none;
  }
  .xone-prop-nc input:disabled {
    opacity: 0.6;
  }
  /* WEAL-139 — el select de un combo T/N/D SIEMPRE se bloquea con el atributo
     disabled (HTML no soporta select readonly; ver WEAL-121 e isReadonlyCapableControl
     en XmlRenderer.ts). El navegador aplica por su cuenta opacity~0.7 + color grisáceo
     a CUALQUIER select:disabled vía su UA stylesheet, AL MARGEN de las reglas de
     arriba (que solo tocan background-color/color, nunca opacity, y por tanto
     jamás neutralizan ese agrisado nativo). Ningún prop type=B/NC se pinta jamás
     como select (B=BUTTON, NC=INPUT checkbox — ver comentario WEAL-121 arriba),
     así que resetear opacity/color en TODO select:disabled es seguro: nunca
     des-agrisa un control que debiera agrisarse.

     WEAL-139 (reapertura, QA 2026-07-28) — el reset de opacity/color no bastaba:
     la regla compartida input/textarea/select:disabled de arriba pone
     background-color con fallback revert-layer, que para un <select> SIN
     --xone-*-bgcolor-disabled declarado (todos los combos T/N/D de Alivia)
     resuelve al fondo BLANCO nativo del UA. Como la regla WEAL-139 no tocaba
     background-color, no ganaba esa propiedad y el <select> bloqueado se veía con
     recuadro blanco en vez del gris de su .propEdicion. Se reusa la MISMA cadena
     de variables que el <select> HABILITADO (bgcolor NO-disabled → fallback
     transparent, para que el bloqueado conserve el color que tendría habilitado y
     el gris del wrapper .xone-prop se transparente igual). Sigue siendo seguro:
     ningún prop B/NC se pinta jamás como select. */
  .xone-prop select:disabled {
    opacity: 1;
    color: inherit;
    background-color: var(--xone-text-bgcolor, var(--xone-bgcolor, transparent));
  }
  /* WEAL-85 — porta la regla INCONDICIONAL de baselineCss.ts:585-590 (runtime
     clásico) a la ruta SSR: cuando un contenedor prop/frame/group porta la clase
     xone-disabled (disableedit de contenedor), se agrisa igual que Android. Se
     mantiene el override por custom-prop (xone-bgcolor-disabled) de abajo para
     proyectos con color de deshabilitado explícito. */
  .xone-prop.xone-disabled,
  .xone-frame.xone-disabled,
  .xone-group.xone-disabled {
    opacity: 0.6;
    pointer-events: none;
  }
  .xone-prop-b.xone-disabled {
    background-color: var(--xone-bgcolor-disabled, revert-layer);
    color: var(--xone-forecolor-disabled, revert-layer);
  }

  /* Focus background (text input) */
  .xone-prop input:focus, .xone-prop textarea:focus {
    background-color: var(--xone-text-bgcolor-focus, var(--xone-bgcolor-focus, revert-layer));
  }

  /* Pressed/active state */
  .xone-prop-b:active {
    background-color: var(--xone-bgcolor-pressed, revert-layer);
    color: var(--xone-forecolor-pressed, revert-layer);
  }

  /* Lines clamp via custom property */
  .xone-prop-tl .xone-label-text {
    -webkit-line-clamp: var(--xone-lines, unset);
  }

  /* Ripple CSS approximation */
  [style*="--xone-ripple"] { position: relative; overflow: hidden; }
  [style*="--xone-ripple"]::after {
    content: ''; position: absolute; inset: 0;
    border-radius: inherit; background: currentColor;
    opacity: 0; transition: opacity 0.2s ease; pointer-events: none;
  }
  [style*="--xone-ripple"]:active::after { opacity: 0.12; }

  /* Buttons */
  .xone-prop-b {
    display: flex; align-items: center; justify-content: center;
    cursor: pointer; border: none; background: transparent;
    color: inherit; font-family: inherit; font-size: inherit;
    padding: 0; min-width: 0;
  }

  /* WEAL-170 — boton-icono (img sin caption): neutraliza el background-color que
     aporte un Estilo de proyecto pensado para botones CON texto (p.ej. el Estilo
     'boton' con bgcolor #3E2554 en Alivia, reutilizado en botones de adjuntar/
     borrar/ver documento que son icon-only). Sin esto, el island lleva la clase
     del Estilo y pinta un cuadrado de fondo detras del PNG transparente (percibido
     como negro; bug reportado en la ficha de Deuda con MAP_BT_ADJUNTAR_DOC_/
     MAP_BT_BORRAR_DOC_DATOS_CLIENTE). Paridad Android: un XOneButton icon-only no
     pinta el chrome de texto. controlRenderer marca xone-btn-icon-only SOLO
     cuando hay imagen y NO se pinta texto (mismo willRenderText que decide el
     layout del img), asi los botones CON caption del mismo Estilo (Subir
     comprobante/Guardar/Volver) conservan su morado. El !important es NECESARIO:
     esta regla vive en @layer xone-base y el CSS del proyecto (con la clase de
     fondo) va en @layer xone-project, que gana por ORDEN DE CAPA a cualquier
     especificidad — mismo patron que .xone-prop-nc transparente de arriba. */
  /* WEAL-255 — ademas del background-color hay que resetear el PADDING: el Estilo
     'boton' de Alivia trae padding-left/right:10px (aire para el TEXTO de Guardar/
     Cancelar); con box-sizing:border-box esos 20px se comen casi todo el ancho de
     25px del wrapper icon-only, dejando ~5px para el <img> y renderizando el icono
     como un punto (MAP_BT_ADJUNTAR_DOC/MAP_BT_BORRAR_DOCADJUNTO del modal 'Agregar
     nuevo documento', y ~10 pares mas de botones icon-only que reusan 'boton'). El
     reset a padding:0 devuelve los 25x25 completos; los botones CON caption no
     llevan esta clase, asi que conservan su padding de 10px. */
  .xone-prop-b.xone-btn-icon-only { background-color: transparent !important; padding: 0 !important; }

  /* Vertical center */
  .xone-prop.xone-prop-centered-y {
    display: flex; flex-direction: column; justify-content: center;
  }

  /* ── Control locked/display-only con etiqueta + valor (WEAL-21) ──
     Paridad con el xml-renderer (baselineCss.ts:220 .xone-prop-title y :396
     .xone-prop-t.is-display-only span.xone-text-value). El wrapper .xone-prop
     ya es flex-column → etiqueta encima, valor debajo.
     La ETIQUETA (title) hereda el 'forecolor' del control (color) y su fontsize
     — sin color propio para respetar cl_forecolor* del proyecto (p.ej. el
     blanco de MAP_SALDO). El VALOR (importe) es el target del binding de valor
     (domSync/bindItemValues) y usa 'text-forecolor' (subview de texto) con
     fallback al color del control. */
  .xone-prop-t-locked > .xone-prop-title {
    display: block;
    color: inherit;
    font: inherit;
    line-height: 1.2;
    text-align: inherit;
  }
  /* WEAL-273: esta regla NO declara white-space a proposito. Para T locked
     multilinea (lines>1) controlRenderer.renderLockedText emite INLINE
     white-space:pre-line en el span exterior y llega aqui por herencia
     (preserva los saltos de linea reales del valor). No asumir que un T
     display-only es siempre de una sola linea: lines>1 en modo lectura
     nunca se convierte en textarea. */
  .xone-prop-t-locked > .xone-text-value {
    display: block; width: 100%;
    color: var(--xone-text-forecolor, inherit);
    font: inherit;
    line-height: 1.2;
    text-align: inherit;
    background: transparent; border: none; padding: 0;
    /* WEAL-207: paridad SSR/MPA con baselineCss.ts. Compensación óptica del
       centrado vertical en texto sin descendentes: el descenso reservado por
       'line-height:1.2' queda vacío (p.ej. 'Financiación vehículo' en el pill
       .tipoExpediente) y la tinta se ve ~0.3-1px por encima del centro real.
       Un pequeño padding-block-start (em-relativo) la baja lo justo para
       equilibrar, sin altura fija ni romper el wrap (WEAL-119). */
    padding-block: 0.05em 0;
  }

  /* ── WEAL-47: etiqueta + valor EN LÍNEA (no apilados) ──
     Continuación de WEAL-21. El wrapper .xone-prop es flex-column por defecto
     → con etiqueta ("SALDO:") + valor ("1949.60 €") ambos como bloque, la
     etiqueta queda ENCIMA y el valor DEBAJO (el "salto de línea" reportado en
     MAP_SALDO). Cuando el renderer emite de verdad la etiqueta añade la clase
     marcadora 'xone-prop-inline-title' al wrapper; aquí la ponemos en fila para
     que sea "SALDO: 1949.60 €" en una sola línea. justify-content respeta el
     text-align del prop (center en MAP_SALDO). El valor deja de ser width:100%
     (que empujaba a la 2ª línea) y se ajusta a su contenido. Sin :has() para
     no depender del soporte del navegador. */
  .xone-prop-t-locked.xone-prop-inline-title {
    flex-direction: row;
    align-items: center;
    /* WEAL-79: DEFAULT flex-start (IZQUIERDA), paridad Android TextView (START).
       El center histórico centraba props inline-title SIN alineación declarada
       (MAP_TITULAR_DNI/MAP_ESTADO_TITULAR de las tarjetas titular/cotitular).
       El centro solo aplica si el prop lo pide (align/text-align=center →
       .xone-align-h-center, que emite controlRenderer.appendInlineTitleAlignClass). */
    justify-content: flex-start;
    gap: 4px;
    flex-wrap: nowrap;
    /* WEAL-99: el wrapper nunca es mas estrecho que su par etiqueta+valor a
       tamano completo — el width literal del .xne (p.ej. MAP_SALDO width=100)
       pasa a ser un minimo, no un techo que recorte texto. Requiere QUITAR la
       inline-size containment de WEAL-66 (computaba el max-content del wrapper
       como 0 y ya no queda ninguna cota de container query en estas reglas). */
    min-width: max-content;
  }
  .xone-prop-t-locked.xone-prop-inline-title.xone-align-h-left { justify-content: flex-start; }
  .xone-prop-t-locked.xone-prop-inline-title.xone-align-h-center { justify-content: center; }
  .xone-prop-t-locked.xone-prop-inline-title.xone-align-h-right { justify-content: flex-end; }

  /* WEAL-244 — puente de alineacion horizontal para TEXTO (labels TL y texto T
     locked SIN el patron inline-title), espejo de los puentes ya existentes para
     botones (.xone-align-h-*.xone-prop-b > button, L174-176) e inline-title
     (.xone-prop-t-locked.xone-prop-inline-title.xone-align-h-*, arriba).
     controlRenderer ya calcula el align EFECTIVO de la .class de proyecto (la
     ULTIMA clase gana, igual que Android; cssLayoutResolver.resolve) y empuja
     xone-align-h-(h) al wrapper via appendAlignAndVerticalCenterClasses
     (renderLabel L910, renderLockedText L1078). Pero hasta ahora ninguna regla
     traducia esa clase a text-align en estos dos tipos, asi que el resultado
     dependia del ORDEN de las .class en el .css del proyecto.
     Caso real (Alivia PlanesLiquidacionPDF): class "pdfCuerpoTexto textoCentro
     font4" — el intent es textoCentro (align:center, ULTIMA clase), pero como
     .textoCentro{text-align:center} (styles.css:365) aparece ANTES que
     .pdfCuerpoTexto{text-align:left} (:619) y ambas viven en @layer xone-project
     con misma especificidad, ganaba pdfCuerpoTexto → texto a la izquierda en el
     PDF Plan de liquidacion. Idem MAP_PDF9..17 con textoDrcha (align:right).

     El !important ES NECESARIO aqui (a diferencia de los puentes de boton e
     inline-title, que alinean por justify-content y no colisionan con el CSS de
     proyecto): pdfCuerpoTexto/textoCentro declaran text-align LITERAL, que vive
     en @layer xone-project y GANA por ORDEN DE CAPA a esta regla de @layer
     xone-base a cualquier especificidad; el important invierte esa precedencia
     (mismo mecanismo que .xone-prop-nc transparente L309 y el reset text-border
     L123). Solo se emite cuando el prop declaro alineacion (appendAlignClasses
     unicamente anade xone-align-h-* si hay align), asi que no toca props sin
     alineacion declarada. Para inline-title la alineacion sigue siendo
     justify-content (regla mas arriba); text-align sobre un flex-row no mueve sus
     items (flex:0 0 auto), asi que esta regla es inocua ahi. El VALOR del texto
     locked (.xone-text-value, text-align:inherit) hereda el text-align del wrapper. */
  .xone-align-h-left.xone-prop-tl,
  .xone-align-h-left.xone-prop-t-locked { text-align: left !important; }
  .xone-align-h-center.xone-prop-tl,
  .xone-align-h-center.xone-prop-t-locked { text-align: center !important; }
  .xone-align-h-right.xone-prop-tl,
  .xone-align-h-right.xone-prop-t-locked { text-align: right !important; }
  .xone-prop-t-locked.xone-prop-inline-title > .xone-prop-title {
    flex: 0 0 auto; margin: 0; white-space: nowrap;
    font-size: 1em;
  }
  .xone-prop-t-locked.xone-prop-inline-title > .xone-text-value {
    flex: 0 0 auto; width: auto; white-space: nowrap;
    font-size: 1em;
  }
  /* WEAL-99 (font de la ETIQUETA — cierra el residuo de WEAL-69): el reportante
     exige AMBOS textos al tamano de su clase (font8 ≈ 16px), sin excepciones. La
     ETIQUETA (.xone-prop-title) deja de ceder: usa font-size: 1em EXACTAMENTE
     igual que el VALOR (.xone-text-value), sin cota cqi (en el chip frmsaldo1
     ~95px la cota 11cqi ≈ 10.45px SIEMPRE ganaba a 1em=16px) y sin recorte
     (fuera el overflow: hidden + min-width: 0 + flex-shrink que dejaban
     "SALDO:" cortado en "SAL"). El hueco lo pone el CONTENEDOR, no el texto: el
     wrapper crece a min-width: max-content y el frame chip que lo envuelve
     (patron detectado por frameRenderer, clase xone-frame-inline-chip) crece a
     su contenido en vez de recortarlo. NO reintroducir el hack de WEAL-47
     (var(--xone-textfont-size) * 1px): daba font5=5px ilegible; la base debe
     ser 1em (tamano real del wrapper fontN). */

  /* ── Imágenes (prop IMG/PH) ──
     En el MPA el <img> ES el prop (lleva class xone-prop-img y el width/height
     inline de los atributos XOne). object-fit lo emite renderImage inline
     (contain por defecto = ScaleType.FIT_CENTER; cover si keep-aspect-ratio +
     scale-type=center_crop, XOneImageView.java:266-269). Aquí solo se garantiza
     que el <img> no desborde su caja: max-* al 100% y display:block. NO se fuerza
     width/height (los pone el inline) para respetar keep-aspect (eje libre auto). */
  img.xone-prop-img, img.xone-prop-ph {
    max-width: 100%; max-height: 100%;
    object-position: center;
    display: block;
  }

  /* Imagen DENTRO de un botón (<prop type=B img="...">): el <img class=xone-btn-img>
     va directo en el <button> (flex centrado). Sin acotar, el navegador lo pinta
     a su tamaño natural (p.ej. 144×144) y DESBORDA la caja del botón (p.ej. 21×26),
     solapando elementos vecinos. Acotamos al 100% de la caja con object-fit:contain
     = ScaleType.FIT_CENTER por defecto del XOneButton (XOneButton.java:706-749). */
  img.xone-btn-img {
    max-width: 100%; max-height: 100%;
    object-fit: contain; object-position: center;
    display: block;
  }

  /* Placeholder de imagen sin recurso (src vacío o inexistente). Reserva la
     caja (width/height heredados del inline del prop) sin glifo ni fondo.
     Paridad Android XOneImageView.java:479 (path vacío → no se pinta imagen,
     se conserva la LayoutParams). NO debe pintar borde, fondo ni texto.
     WEAL-244: el reset cubre AMBAS formas del placeholder — el span que emite
     renderEmptyImagePlaceholder (src ausente en build) Y el img que arranca
     oculto en el camino de macro runtime (renderImage isRuntimeMacro, L1340:
     img class="... xone-img-empty") y que la isla deja xone-img-empty cuando el
     ##FLD_## resuelve vacio (XoneIsland.hideBrokenImage). El selector previo
     span.xone-img-empty NO matcheaba ese img, asi que la caja del img vacio
     conservaba el chrome por defecto del navegador (recuadro tipo checkbox visible
     en la columna Producto Alivia de SimulacionesDeudaPDF cuando el producto no
     es Reclamacion de intereses abusivos). Al ser reset sobre el elemento img,
     gana al origen UA sin necesidad de !important. */
  span.xone-img-empty,
  img.xone-img-empty {
    display: inline-block;
    background: none; border: none;
    overflow: hidden;
    flex: 0 0 auto;
  }

  /* Line clamp */
  .xone-prop-tl .xone-label-text.xone-label-clamp {
    display: -webkit-box; -webkit-box-orient: vertical;
    overflow: hidden; text-overflow: ellipsis;
  }

  /* Floating labels.
     SIN padding propio: Android pone el EditText a padding 0 por defecto
     (XOneEditText.java:752 vTextView.setPadding(0,0,0,0)); solo lpadding/
     tpadding... explícitos del prop añaden inset (ControlsUtils.
     setInsidePadding, XOneEditText.java:896-908) y esos ya se loweran aparte.
     El padding 0.4em/0.5em anterior era un invent del template (offset 4px/5px
     vs la referencia). */
  .xone-field-floating {
    position: relative; display: flex; flex-direction: column;
    justify-content: center;
    height: 100%; font-size: inherit; padding: 0;
  }
  .xone-field-floating input,
  .xone-field-floating textarea {
    width: 100%; background: transparent; border: none;
    padding: 0; font-family: inherit; font-size: 1em; color: inherit;
    outline: none;
  }
  /* Label en REPOSO = mismo origen tipográfico que el texto del campo (1em):
     en Android el hint expandido se pinta al text size del EditText. */
  .xone-floating-label {
    position: absolute; left: 0; top: 50%; transform: translateY(-50%);
    pointer-events: none; transition: all 0.15s ease;
    font-size: 1em; opacity: 0.5; line-height: 1;
  }
  /* Label COLAPSADO: hintTextAppearance Material por defecto = 12sp, que XOne
     NUNCA sobreescribe (XoneCSS.java:1034-1036 setHintEnabled+setHint sin
     tocar el appearance). 12sp ≈ 12px legacy (1p=1px); en responsive escala
     con --xone-font-unit. El 0.75em anterior daba 7.5px con fontsize 10. */
  .xone-field-floating input:focus + .xone-floating-label,
  .xone-field-floating input:not(:placeholder-shown) + .xone-floating-label,
  .xone-field-floating textarea:focus + .xone-floating-label,
  .xone-field-floating textarea:not(:placeholder-shown) + .xone-floating-label {
    top: 0.15em; transform: translateY(0);
    font-size: calc(12 * var(--xone-font-unit, 1px)); opacity: 1;
    color: var(--xone-floating-label-color, currentColor);
  }
  /* Autocompletado del navegador: cuando Chrome/Safari rellenan usuario/contraseña
     el campo tiene valor pero (sin interacción del usuario) NO siempre dispara
     :not(:placeholder-shown), así que la etiqueta se quedaba en reposo (centrada)
     y el texto autocompletado se le superponía. Subimos la etiqueta también con
     :-webkit-autofill/:autofill (regla APARTE para que, si un motor no conociera
     :autofill, no invalide la regla principal de focus/valor). */
  .xone-field-floating input:-webkit-autofill + .xone-floating-label,
  .xone-field-floating input:autofill + .xone-floating-label {
    top: 0.15em; transform: translateY(0);
    font-size: calc(12 * var(--xone-font-unit, 1px)); opacity: 1;
    color: var(--xone-floating-label-color, currentColor);
  }
  /* Neutraliza el fondo amarillo/azul y el color de texto que el autofill fuerza,
     para conservar el input transparente del diseño XOne (el color lo aporta el
     frame de detrás). El delay enorme de transición evita el repintado del fondo
     nativo (truco estándar para autofill transparente). */
  .xone-field-floating input:-webkit-autofill,
  .xone-field-floating input:autofill {
    -webkit-text-fill-color: inherit;
    caret-color: inherit;
    transition: background-color 9999s ease-in-out 0s;
  }

  /* Password toggle — el padding-right solo cuando el toggle existe (opt-in
     vía show-password-visibility-toggle, Utils.java:558 + XOneEditText.java:684). */
  .xone-field { position: relative; display: flex; align-items: center; height: 100%; }
  .xone-field input { flex: 1 1 auto; }
  .xone-field:has(.xone-password-toggle) input,
  .xone-field-floating:has(.xone-password-toggle) input { padding-right: 2.5em; }
  .xone-password-toggle {
    position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
    background: transparent; border: none; cursor: pointer; color: #9AA0A6;
    padding: 6px; border-radius: 50%; line-height: 0;
    display: inline-flex; align-items: center; justify-content: center;
  }
  .xone-password-toggle svg { width: 20px; height: 20px; display: block; }
  .xone-password-toggle:hover { color: #2D3436; }
  .xone-password-toggle.is-visible { color: #2D3436; }

  /* Contents (prop type="Z") — layout aplicado por islands-runtime */
  .xone-prop-z { position: relative; min-height: 0; }
  /* Paridad Android: un contents NUNCA pinta fuera de su contenedor.
     ViewGroup clipea los hijos a sus bounds (clipChildren=true default) y el
     RecyclerView se mide con AT_MOST(alto disponible del padre) — como máximo
     ocupa el alto del padre y scrollea internamente si el contenido es mayor
     (XOneContentRecyclerView dentro del frame, EditFramePage). En web: el
     island clipea (overflow hidden) y el div interior llena la caja y
     scrollea en vertical. Para height="-2" (wrap content) la cadena
     .xone-row/min-height:0 + max-height:100% reproduce el AT_MOST cuando el
     padre tiene alto definido. La orientación horizontal re-fija
     overflow-y:hidden inline en applyContentsLayout (gana a esta regla).
     El div interior (.xone-prop-z, overflow:auto) scrollea en AMBOS ejes: en
     vertical siempre; en horizontal "si es necesario" cuando alguna fila crece
     más ancha que la caja (ver regla de crecimiento de filas más abajo). */
  xone-island[data-type="Z"] { max-height: 100%; min-height: 0; overflow: hidden; }
  xone-island[data-type="Z"] > .xone-prop-z {
    height: 100%; max-height: 100%; overflow: auto;
    -webkit-overflow-scrolling: touch;
  }
  .xone-row { min-height: 0; }
  .xone-row:has(> xone-island[data-type="Z"]) { max-height: 100%; }
  /* Cabecera de columnas de un contents (header-contents="Coll", extensión
     web-*). El wrapper apila en columna: banda de cabecera (alto natural, sin
     crecer ni scrollear) + island (rellena el resto y scrollea). Las celdas de
     la cabecera heredan anchos/labels de la colección referenciada, alineando
     con las columnas de las filas de datos. */
  .xone-contents-headed { box-sizing: border-box; min-height: 0; }
  .xone-row:has(> .xone-contents-headed) { max-height: 100%; }
  .xone-contents-header { flex: 0 0 auto; overflow: hidden; width: 100%; }
  .xone-contents-header > .xone-frame { width: 100%; }
  /* WEAL-77: la banda de cabecera NO cuelga de un contenedor por-fila
     (article[data-row-index], que aporta su propio container-type:size con la
     altura de la fila), sino directo de .xone-contents-header. Su frame interior
     trae inline height:Ncqh + container-type:size heredados del proyecto: sin
     contenedor por-fila ese Ncqh re-basa contra el PRIMER ancestro con
     container-type:size (el frame de pantalla, p.ej. frmGeneral 471px → 14cqh =
     66px), inflando la banda arbitrariamente. Además, con la lista VACÍA (0 filas)
     el runtime applyHeaderedTableWidths no setea --xone-hc-h, así que el frame
     queda con el Ncqh crudo. Se neutraliza igual que en las filas de datos
     (article[data-row-index] > .xone-frame, abajo): se suelta la size-containment
     y la altura Ncqh, dejando que el frame se ajuste a su .xone-row (≈1lh) →
     banda tight y CONSISTENTE en todas las secciones, sin depender del runtime ni
     de que haya filas. Las celdas de cabecera usan min-height:1lh + width:N% (NO
     height:Ncqh), así que soltar la containment del frame no las colapsa. */
  .xone-contents-header > .xone-frame {
    container-type: normal !important;
    height: auto !important;
    min-height: 0;
  }
  /* Header-contents — alineación EXACTA de la cabecera con cada columna
     (2026-07-07). Las celdas de cabecera y de datos NO encogen (flex-shrink:0):
     cada columna toma su ancho declarado → el título/flecha cae EXACTO sobre su
     columna (probado: 0px de deriva, vs ~7-43px acumulados con el encogido). Si la
     suma de columnas supera el ancho, el listado (.xone-prop-z, overflow:auto)
     scrollea en horizontal; la banda de cabecera va FUERA de esa caja de scroll,
     así que el runtime la traslada en X junto con el scroll del listado
     (wireHeaderScrollSync) para mantener la alineación. La banda recorta su
     desbordamiento (el JS la desplaza dentro). El interior de la cabecera crece a
     su contenido (mismo ancho exacto que las filas). Scoped a contents CON
     header-contents — las listas sin cabecera conservan su fit/scroll previo. */
  .xone-contents-headed .xone-row { flex-shrink: 0; }
  .xone-contents-headed .xone-row > * { flex-shrink: 0; }
  .xone-contents-headed > .xone-contents-header > * { width: max-content; min-width: 100%; }
  /* El frame raíz de la fila (frmContent) y la Line deben CRECER con su contenido
     (todas las columnas), no quedarse al ancho del padre — si no, el fondo/hover
     de fila se corta a media tabla al hacer scroll (2026-07-07). El frame trae
     inline width:N% y container-type:size (size-containment para cqh), ambos
     bloquean el crecimiento en el eje inline → se anulan con !important. El
     desacople real (que las celdas % dejen de re-crecer al crecer el frame) lo
     hace el runtime (applyHeaderedTableWidths): reescribe el width:N% de cada
     celda a calc(var(--xone-hc-base) * N/100) contra una base FIJA (ancho visible
     de la caja), rompiendo la circularidad. Sin ese desacople, este max-content
     colapsaría al ancho de texto. */
  .xone-contents-headed .xone-row { width: max-content; min-width: 100%; }
  .xone-contents-headed article[data-row-index] > .xone-frame {
    container-type: normal !important;
    width: max-content !important;
    min-width: 100%;
  }
  /* FIX-5: SIN gap entre celdas — Android no añade separación entre las
     Lines de un contents (empaquetan contiguas, EditFramePage.java:967-975);
     la separación visual la dan los margins/paddings del propio item. */
  .xone-contents { display: flex; flex-direction: column; gap: 0; width: 100%; }
  .xone-contents.is-gridview { display: grid; gap: 0; }
  .xone-contents.is-recyclerview { display: flex; gap: 0; }
  .xone-contents[data-orientation="horizontal"] {
    flex-direction: row; overflow-x: auto; overflow-y: hidden;
    scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;
  }
  .xone-contents[data-orientation="horizontal"] > .xone-content-item {
    width: auto; flex: 0 0 auto; scroll-snap-align: start;
  }
  /* WEAL-110: el cursor de una fila de contents NO es pointer por defecto. Solo
     lo es cuando la fila es REALMENTE seleccionable, y eso lo decide el runtime
     marcando data-selectable="true" (en el contents o en el propio item):
     hay <selecteditem> con script de VERDAD (no el boilerplate vacío de
     Studio) o la coll tiene ficha de edición generada (fallback WEAL-14).
     Antes era cursor: pointer incondicional → los ~30 contents de DETALLE de
     actividad (selecteditem vacío) pintaban la mano sobre toda su superficie,
     incluidos sus campos editables. */
  .xone-content-item {
    display: flex; flex-direction: column; width: 100%;
    cursor: default;
  }
  .xone-content-item[data-selectable="true"],
  [data-selectable="true"] .xone-content-item {
    cursor: pointer; transition: filter 0.1s ease;
  }
  .xone-content-item[data-selectable="true"]:hover,
  [data-selectable="true"] .xone-content-item:hover { filter: brightness(0.97); }
  /* WEAL-110 (defensa en profundidad): aun en una fila SELECCIONABLE, las zonas
     EDITABLES no muestran la mano — el control maneja su propio click (el gate
     de wireSelectedItem ya excluye button/input/select/textarea del click de
     fila), así que el cursor debe ser el suyo, no el de navegación. Se hace por
     CSS de mayor especificidad (no !important, no inline style). */
  .xone-content-item input:not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type="submit"]):not([disabled]):not([readonly]),
  .xone-content-item textarea:not([disabled]):not([readonly]) { cursor: auto; }
  /* Los <select> (combos) SÍ conservan su mano: son un control desplegable. */
  /* Frame raíz de un item de contents = MATCH_PARENT por defecto (paridad
     Android: la fila de una lista llena el ancho del list). Sin esto, un
     frmContent SIN width explícito (o con un typo tipo xwidth="100%") colapsa
     a 0 por su container-type:size + flex 0 0 auto → los props se solapan. Un
     width inline del proyecto (p.ej. 96%) gana por especificidad y preserva el
     inset. En listas horizontales el item es width:auto → el frame no se fuerza. */
  .xone-contents:not([data-orientation="horizontal"]) > .xone-content-item > .xone-frame { width: 100%; }
  /* Crecimiento horizontal + scroll-x "si es necesario" (2026-07-06): una
     lista vertical puede tener filas cuyo contenido (muchas columnas, tabla
     ancha) supere el ancho del frame. En vez de recortarlo, la fila crece a su
     contenido con suelo al ancho de la caja (min-width:100%); la caja de scroll
     (.xone-prop-z, overflow:auto) muestra entonces barra horizontal. Si el
     contenido cabe, min-width:100% deja la fila al 100% → idéntico al
     comportamiento previo (background/hover llenan el ancho). NO aplica a listas
     horizontales (gestionan su propio ancho de item + scroll-x). TAMPOCO aplica
     a gridview (:not(.is-gridview), WEAL-12): ahí la pista minmax(0,1fr) ya fija
     el ancho y el item debe ESTIRARSE a la columna.

     WEAL-143: se cambia width:max-content -> width:fit-content. max-content
     forzaba SIEMPRE el ancho natural del contenido SIN envolver, asi que una fila
     con TEXTO ENVOLVIBLE proporcional (burbujas de chat de Mensajeria: MAP_MENSAJE
     type T width 89 por ciento dentro de frames 96/90) crecia hasta el texto en
     una sola linea (medido 10293px de item / 7527px de burbuja en un panel de
     786px) y nunca envolvia -> scroll horizontal. fit-content =
     min(max-content, max(min-content, ancho-disponible)): cuando el contenido ES
     envolvible (max-content > disponible pero min-content <= disponible), colapsa
     al ancho disponible del panel y el texto ENVUELVE; cuando el contenido NO
     puede encoger (celdas px de una tabla ancha, min-content > disponible), sigue
     creciendo a su tamano y conserva el scroll-x. Preserva la divergencia web
     "tabla ancha scrollea en X" y a la vez restaura la paridad Android para las
     filas proporcionales (una fila MATCH_PARENT nunca excede el ancho del list). */
  .xone-contents:not([data-orientation="horizontal"]):not(.is-gridview) > .xone-content-item {
    min-width: 100%; width: fit-content;
  }
  /* Gridview vertical: el item llena su pista (paridad XOneGridView.java:1034-1038
     setStretchMode(STRETCH_COLUMN_WIDTH); intención ya documentada en
     XoneIsland.ts:1405-1411). Regresión WEAL-12: la regla de crecimiento de LISTA
     de arriba también casaba aquí, y width:max-content (que no es auto) anula
     el justify-self:stretch por defecto del grid item → se encoge a su contenido.
     El suelo min-width:100% no lo salvaba porque el runtime pone inline
     min-width:0 (XoneIsland.ts:1619, necesario para el reparto exacto de
     minmax(0,1fr)) e inline gana a la hoja. Y como el frame raíz trae inline
     container-type:size (para resolver los cqh de sus hijos), el size
     containment hace que su contenido NO contribuya al tamaño intrínseco →
     max-content = 0 + padding → el item colapsaba a una astilla. */
  .xone-contents.is-gridview:not([data-orientation="horizontal"]) > .xone-content-item {
    width: 100%; min-width: 0;
  }
  /* Paridad referencia Alivia (2026-07-06): en una fila de contents (lista tipo
     tabla, muchas columnas estrechas) cada CELDA recorta su contenido a su ancho
     de columna. Un valor SIN espacios (email, URL) más ancho que su celda no
     puede envolver y, con overflow:visible, se DERRAMABA sobre la columna vecina
     — síntoma "campos desalineados": el correo pisaba la fecha. overflow:hidden
     lo recorta en el borde de la celda, igual que la web XOne de referencia
     (correo cortado). El texto multi-palabra sigue envolviendo (no se fuerza
     nowrap → NO cambia la altura de las filas). Inerte en listas tipo tarjeta
     (celda = ancho del contenido → nada que recortar). */
  .xone-content-item .xone-prop { overflow: hidden; }
  .xone-contents-empty {
    color: #9AA0A6; text-align: center; padding: 16px; font-size: 12px;
  }

  /* WEAL-205 — el recuadro de resaltado del buscador de mensajeria se recortaba en
     la ULTIMA fila. El JS del proyecto Alivia (mensajes_functions.js,
     scrollYResaltarMensaje; fixture read-only) resalta la fila del mensaje
     encontrado con outline 4px solid #E65100 y outline-offset -4px. Cuando la fila
     objetivo es la ULTIMA de la conversacion, el contenedor de scroll start-from-
     bottom (clase xone-reverse-column, overflow-y:auto, padding:0) queda en su
     scroll MAXIMO (scrollTop == scrollHeight - clientHeight): scrollIntoView con
     block center no puede centrarla (no hay contenido debajo) y la fila queda PEGADA
     al limite de recorte del contenedor (rowRect.bottom aprox containerRect.bottom),
     de modo que el overflow:auto del ancestro recorta el trazo INFERIOR del outline
     (solo se veian 3 lados). Simetricamente, la PRIMERA fila (scrollTop 0) podria
     recortar el trazo superior.

     Fix a nivel de renderer (regla de oro: NO se toca el .xne ni el JS de proyecto):
     dar unos px de aire en el eje de bloque al contenedor de scroll start-from-bottom
     para que ninguna fila quede pegada al limite de recorte y el outline del proyecto
     tenga margen suficiente. scroll-padding hace ademas que scrollIntoView respete ese
     margen. Acotado a xone-reverse-column (chat Mensajeria comparte esta utilidad); sin
     efecto en listas normales. La clase la anade el runtime (XoneIsland) a los contents
     start-from-bottom. Ver ticket WEAL-205. */
  .xone-contents.xone-reverse-column {
    padding-top: 6px; padding-bottom: 6px;
    scroll-padding-top: 6px; scroll-padding-bottom: 6px;
  }

  /* WEAL-206 — el fix de WEAL-205 (arriba) solo cubre la fila verdaderamente
     ULTIMA/PRIMERA del DOM (pegada al limite de recorte del CONTENEDOR). Pero el
     recorte del borde INFERIOR del outline ocurre en CUALQUIER mensaje intermedio,
     con el contenedor lejos de su scroll maximo — que es el caso mucho mas comun.

     Causa raiz (verificada en vivo con getBoundingClientRect en xonemanager.com): la
     fila .xone-content-item.xone-contents-row (flex column, sin padding/margin-bottom
     propio) tiene su borde inferior EXACTAMENTE flush — misma coordenada fraccional de
     pixel — tanto con el borde inferior de su propia burbuja de mensaje (.xone-frame,
     0px de aire) como con el borde superior de la fila hermana siguiente (gap 0px). El
     unico espaciado entre burbujas viene del margin-top de la burbuja SIGUIENTE, nunca
     de un margin/padding-bottom en la fila actual. Por eso el trazo SUPERIOR del outline
     (~10px de aire) se pinta bien y el INFERIOR (0px de aire) coincide pixel a pixel con
     contenido pintado encima/debajo y no se ve. Es la MISMA familia de causa que
     WEAL-205 (el outline-offset:-4px del JS de proyecto necesita "aire" en el borde
     donde se aplica), pero a nivel de FILA en vez de a nivel de CONTENEDOR.

     Fix a nivel de renderer (regla de oro: NO se toca el .xne ni el JS de proyecto): dar
     unos px de aire en el borde inferior de cada FILA del contents start-from-bottom, para
     que el borde inferior real de la fila no coincida pixel a pixel con el ultimo pixel de
     contenido pintado. Selector hijo-directo (>) verificado: la fila es hijo directo del
     contenedor .xone-reverse-column. Acotado a chat Mensajeria; sin efecto en listas
     normales. Ver ticket WEAL-206. */
  .xone-contents.xone-reverse-column > .xone-content-item.xone-contents-row {
    padding-bottom: 6px;
  }

  /* ── Gráficos (pie/ring/line/bar) renderizados en SVG por el runtime ──
     El runtime pinta el SVG + leyenda dentro del .xone-contents (que ya lleva
     el alto del prop chart, p.ej. 200p). Pie/ring = SVG + leyenda en fila;
     line/bar = SVG arriba + leyenda debajo. */
  /* El chart llena la CAJA DEL PROP (paridad XOnePieChartView.java:239-256
     refreshSize → LayoutParams nWidth/nHeight del prop): height:100% define la
     cadena island(caja del prop)→container→SVG. El antiguo max-width:52% (que
     capaba el pie a media columna) y la altura indefinida del container
     producían el cuadrado 113×113 en vez de llenar 98%×200p. */
  .xone-contents.xone-chart { gap: 8px; overflow: hidden; padding: 4px; align-items: stretch; height: 100%; }
  .xone-contents.xone-chart-row { flex-direction: row; align-items: center; }
  .xone-contents.xone-chart-col { flex-direction: column; }
  .xone-chart-svg { display: block; }
  .xone-chart-row > .xone-chart-svg { height: 100%; width: auto; flex: 0 0 auto; }
  .xone-chart-col > .xone-chart-svg { width: 100%; flex: 1 1 auto; min-height: 0; }
  .xone-chart-axis { font-size: 9px; fill: #777777; }
  .xone-chart-legend {
    list-style: none; margin: 0; padding: 0; font-size: 11px; overflow: auto;
    display: flex; flex-direction: column; gap: 2px; flex: 1 1 auto; min-width: 0;
  }
  .xone-chart-col > .xone-chart-legend {
    flex-direction: row; flex-wrap: wrap; justify-content: center;
    gap: 2px 12px; flex: 0 0 auto;
  }
  /* Leyenda del pie SUPERPUESTA al chart, esquina superior-izquierda —
     convención del canal clásico web XOne (referencia mRed, G7 iter3). */
  .xone-contents.xone-chart-row { position: relative; }
  .xone-chart-legend.xone-chart-legend-overlay {
    position: absolute; top: 6px; left: 6px;
    flex: none; max-width: 70%; max-height: calc(100% - 12px);
  }
  .xone-chart-legend-item { display: flex; align-items: center; gap: 5px; line-height: 1.4; }
  .xone-chart-swatch { width: 11px; height: 11px; border-radius: 2px; flex: 0 0 auto; }
  .xone-chart-legend-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .xone-chart-row .xone-chart-legend-label { flex: 1 1 auto; min-width: 0; }
  .xone-chart-legend-value { font-weight: 600; margin-left: auto; padding-left: 6px; }

  /* ── Gráficos: interacción y animación (#389) ──
     Duración y easing NO son de diseño propio: son los que se midieron en el
     canal legacy leyendo 'Chart.instances' en vivo (Chart.js 2.9.4 →
     animDuration 1000, animEasing easeOutQuart). 'easeOutQuart' en CSS es
     cubic-bezier(0.165, 0.84, 0.44, 1).

     🔴 El estado ESTÁTICO de cada elemento es el FINAL. Así, si la animación no
     llega a correr —el baseline anula 'animation-duration' bajo
     'prefers-reduced-motion', y también pasa en una captura o en un navegador
     sin soporte— el gráfico queda TERMINADO. Con el patrón contrario (estado
     estático = inicial) una línea se quedaría invisible y un pie encogido.
     Medido en Chromium: con 'prefers-reduced-motion: reduce' el path sale con
     'stroke-dasharray: none' desde el primer frame, es decir, entero.

     Los @keyframes declaran solo el 'from' SALVO 'xone-chart-draw', que necesita
     un 'to' explicito — ver su propio comentario. */
  .xone-contents.xone-chart { position: relative; }
  .xone-chart-svg:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
  /* Deja el scroll vertical al navegador y solo captura el gesto horizontal. */
  .xone-chart-svg { touch-action: pan-y; }
  .xone-chart-dot.is-activo { r: 4; }
  .xone-chart-bar-rect.is-activo, .xone-chart-slice.is-activa { filter: brightness(1.12); }

  .xone-chart-tip {
    position: absolute; left: 0; top: 0; z-index: 2;
    /* Sin esto el propio tooltip recibe el pointermove, dispara pointerleave
       en el SVG y el gráfico parpadea. */
    pointer-events: none;
    background: rgba(255,255,255,0.96); color: #222222;
    border: 1px solid #D8D8D8; border-radius: 4px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.14);
    padding: 5px 8px; font-size: 11px; line-height: 1.45;
    /* El contenedor recorta (overflow:hidden), asi que el tooltip tiene que
       caber: un 'index' de 5 series sobre un chart de 200p se salia por abajo
       pese al volteo. */
    max-width: 86%; min-width: 0; max-height: calc(100% - 12px); overflow: auto;
  }
  .xone-chart-tip[hidden] { display: none; }
  .xone-chart-tip-title { font-weight: 600; margin-bottom: 2px; }
  .xone-chart-tip-row { display: flex; align-items: center; gap: 6px; min-width: 0; }
  .xone-chart-tip-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
  .xone-chart-tip-value { font-weight: 600; margin-left: auto; padding-left: 10px; white-space: nowrap; }

  /* 🔴 El 'to' es OBLIGATORIO aquí, y no por simetría. Sin él, el destino de
     stroke-dasharray es el estilo calculado, que es 'none'; interpolar una
     lista de longitudes contra 'none' es DISCRETO, así que a mitad de animacion
     el patron de guiones desaparecia de golpe y la linea aparecia entera.
     Medido: a los 2 s de 4 s, dasharray pasaba de '1px' a 'none'. */
  @keyframes xone-chart-draw {
    from { stroke-dasharray: 1; stroke-dashoffset: 1; }
    to   { stroke-dasharray: 1; stroke-dashoffset: 0; }
  }
  @keyframes xone-chart-fade { from { opacity: 0; } }
  @keyframes xone-chart-grow { from { transform: scaleY(0); } }
  @keyframes xone-chart-pie-in { from { transform: rotate(-90deg) scale(0.6); opacity: 0; } }

  .xone-chart-anim .xone-chart-line-path {
    animation: xone-chart-draw 1000ms cubic-bezier(0.165, 0.84, 0.44, 1);
  }
  .xone-chart-anim .xone-chart-dot {
    animation: xone-chart-fade 1000ms cubic-bezier(0.165, 0.84, 0.44, 1);
  }
  .xone-chart-anim .xone-chart-bar-rect {
    /* transform-origin llega inline por barra (la base del eje, que depende del
       rango); 'view-box' hace que se interprete en unidades del viewBox. */
    transform-box: view-box;
    animation: xone-chart-grow 1000ms cubic-bezier(0.165, 0.84, 0.44, 1);
  }
  .xone-chart-anim .xone-chart-pie-rot {
    /* El viewBox del pie es constante 0 0 120 120, así que el centro es un
       literal y no hace falta depender del soporte de 'fill-box'. */
    transform-box: view-box; transform-origin: 60px 60px;
    animation: xone-chart-pie-in 1000ms cubic-bezier(0.165, 0.84, 0.44, 1);
  }
}

@layer xone-components {
  xone-combo {
    align-items: stretch; position: relative; box-sizing: border-box;
    cursor: pointer; min-width: 0; overflow: hidden;
    /* Altura intrínseca de UNA línea, como un input vacío de type T (que nunca
       colapsa a 0). Un combo con height explícito en el XML lo sobreescribe; los
       sin height (p.ej. filtros) quedan a la altura de campo en vez de 0. La
       unidad lh = alto de línea; fallback 1.2em para navegadores sin lh. */
    min-height: 1.2em;
    min-height: 1lh;
  }
  /* WEAL-38: combo-display BLOQUEADA (data-combo-locked, renderLockedComboDisplay).
     Debe verse EXACTAMENTE como el <span> locked equivalente (renderLockedText:
     cajetín gris de propEdicionBloqueado), no como combo: sin cursor pointer y
     sin la caja de spinner. El markup no lleva trigger (no hay caret) ni
     .xone-combo-display; estas overrides anulan las reglas de elemento de
     arriba. El layout lo siguen dando sus clases (.xone-prop flex column +
     clases del proyecto), igual que en el span. */
  xone-combo[data-combo-locked] {
    cursor: default;
    position: static;
    overflow: visible;
    min-height: 0;
  }
  /* Select nativo (estático): llena la caja; caret nativo pegado a la derecha. */
  .xone-combo-native {
    cursor: pointer; padding: 0 18px 0 4px;
    -webkit-appearance: none; -moz-appearance: none; appearance: none;
    background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                      linear-gradient(135deg, currentColor 50%, transparent 50%);
    background-position: calc(100% - 10px) center, calc(100% - 5px) center;
    background-size: 5px 5px, 5px 5px; background-repeat: no-repeat;
  }
  .xone-combo-native:focus { outline: none; }
  /* Dinámico: la etiqueta llena TODA la caja (con hueco a la derecha para el
     caret); el trigger es una capa transparente ABSOLUTA que cubre el control
     (click en cualquier punto abre). El caret/lupa va pegado al borde DERECHO
     (flush-right, como un <select> normal), nunca en el centro. */
  .xone-combo-display {
    flex: 1 1 auto; width: 100%; min-width: 0;
    display: flex; align-items: center;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    padding: 0 20px 0 4px; font: inherit; color: inherit;
  }
  .xone-combo-placeholder { color: #9AA0A6; }
  .xone-combo-trigger {
    position: absolute; inset: 0; width: 100%; height: 100%;
    border: none; background: transparent; cursor: pointer; padding: 0; margin: 0;
  }
  /* Caret (inline/spinner): triángulo pegado al borde derecho de la caja. */
  .xone-combo-trigger::after {
    content: ""; position: absolute; right: 8px; top: 50%; pointer-events: none;
    width: 0; height: 0; transform: translateY(-25%);
    border-left: 4px solid transparent; border-right: 4px solid transparent;
    border-top: 5px solid currentColor;
  }
  /* Modo picker: lupa (también pegada a la derecha) en vez del caret. */
  xone-combo[data-combo-mode="picker"] .xone-combo-trigger::after {
    border: none; width: 9px; height: 9px; right: 7px; transform: translateY(-60%);
    border-radius: 50%; box-shadow: 0 0 0 1.5px currentColor;
  }
  xone-combo[data-combo-mode="picker"] .xone-combo-trigger::before {
    content: ""; position: absolute; right: 5px; top: 62%; pointer-events: none;
    width: 5px; height: 1.5px; background: currentColor;
    transform: rotate(45deg); transform-origin: left center;
  }
  /* WEAL-42: combo deshabilitado (disableedit → [disabled]/[inert]): sin caret/lupa y sin cursor pointer.
     WEAL-232: además pointer-events:none en el trigger — el botón absoluto
     (inset:0) cubre el 100% del combo, incluida la etiqueta con el texto visible,
     y sin esto captura todos los clicks/drag e impide seleccionar/copiar el texto
     de debajo. Es seguro: isDisabled() (XoneCombo) ya gatea la apertura del
     picker desde el listener del propio .xone-combo-display, así que dejar pasar
     el gesto al span NO reabre el combo, solo permite seleccionar su texto. */
  xone-combo[disabled] .xone-combo-trigger,
  xone-combo[inert] .xone-combo-trigger { cursor: default; pointer-events: none; }
  xone-combo[disabled] .xone-combo-trigger::after,
  xone-combo[disabled] .xone-combo-trigger::before,
  xone-combo[inert] .xone-combo-trigger::after,
  xone-combo[inert] .xone-combo-trigger::before { display: none; }
  .xone-combo-native:disabled { background-image: none; cursor: default; }
  /* Panel dropdown (inline/spinner) — anclado bajo el control. */
  .xone-combo-panel {
    position: fixed; z-index: 10000; max-height: 260px; overflow: hidden;
    display: flex; flex-direction: column;
    background: var(--xone-combo-dialog-bg, #ffffff);
    color: var(--xone-combo-dialog-fore, #202124);
    border: 1px solid #dadce0; border-radius: 6px;
    box-shadow: 0 4px 16px rgba(0,0,0,0.18);
    font-size: var(--xone-combo-dialog-fontsize, 13px);
  }
  /* Modal (picker) — ventana flotante centrada con overlay. */
  .xone-combo-overlay {
    position: fixed; inset: 0; z-index: 10000; background: rgba(0,0,0,0.35);
    display: flex; align-items: center; justify-content: center; padding: 16px;
  }
  .xone-combo-modal {
    display: flex; flex-direction: column; width: min(420px, 92vw);
    max-height: 80vh; overflow: hidden;
    background: var(--xone-combo-dialog-bg, #ffffff);
    color: var(--xone-combo-dialog-fore, #202124);
    border-radius: 10px; box-shadow: 0 12px 40px rgba(0,0,0,0.35);
    font-size: var(--xone-combo-dialog-fontsize, 14px);
  }
  .xone-combo-search {
    flex: 0 0 auto; margin: 8px; padding: 8px 10px; font: inherit;
    border: 1px solid #dadce0; border-radius: 6px; outline: none;
    background: #ffffff; color: #202124;
  }
  .xone-combo-search:focus { border-color: #1a73e8; }
  .xone-combo-options {
    list-style: none; margin: 0; padding: 4px 0; overflow-y: auto;
    flex: 1 1 auto; min-height: 0;
  }
  .xone-combo-option {
    padding: 9px 14px; cursor: pointer; white-space: nowrap;
    overflow: hidden; text-overflow: ellipsis;
  }
  .xone-combo-option:hover { background: rgba(26,115,232,0.10); }
  .xone-combo-empty { color: #9AA0A6; cursor: default; }
  .xone-combo-empty:hover { background: transparent; }
}

@layer xone-components {
  /* #367 — rastro de navegacion (`viewmode="breadcrumbview"`).
     El generador emite solo el HOST y el islands-runtime le cuelga dentro
     `.xone-breadcrumb > .xone-breadcrumb-item`, que son <div> = BLOQUE. Sin esta
     base el rastro sale en VERTICAL, una miga por linea, desbordando el hueco
     del titulo de seccion. Mismo criterio que `xone-combo { display:flex }`.

     OJO con el selector: `.xone-breadcrumb` a secas NO vale. Esa clase YA la usa
     `renderWebBreadcrumb` (web-breadcrumb, el <ol> con JSON-LD de SEO), que es
     otra cosa y no debe cambiar de aspecto por esto. Por eso todo va acotado
     bajo `[data-xone-breadcrumb]`, que solo lleva el host de un prop. */
  .xone-prop-breadcrumb { display: flex; align-items: center; min-width: 0; }
  [data-xone-breadcrumb] .xone-breadcrumb {
    display: flex; align-items: center; flex-wrap: nowrap;
    width: 100%; min-width: 0; overflow: hidden;
  }
  [data-xone-breadcrumb] .xone-breadcrumb-item {
    display: flex; align-items: center; min-width: 0;
  }
  /* El separador nunca se encoge ni pierde sus espacios; la etiqueta si puede
     recortarse con puntos suspensivos cuando el rastro no cabe. */
  [data-xone-breadcrumb] .xone-breadcrumb-sep { flex: 0 0 auto; white-space: pre; }
  [data-xone-breadcrumb] .xone-breadcrumb-label {
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  /* Foco visible en las migas navegables: llevan role="link" y tabindex. */
  [data-xone-breadcrumb] .xone-breadcrumb-label[role="link"]:focus-visible {
    outline: 2px solid currentColor; outline-offset: 2px;
  }
}

@layer xone-components {
  @keyframes xone-fadeIn { from { opacity: 0; } to { opacity: 1; } }
  @keyframes xone-fadeOut { from { opacity: 1; } to { opacity: 0; } }
  @keyframes xone-slideInRight { from { transform: translateX(100%); } to { transform: translateX(0); } }
  @keyframes xone-slideInLeft { from { transform: translateX(-100%); } to { transform: translateX(0); } }
  @keyframes xone-slideOutRight { from { transform: translateX(0); } to { transform: translateX(100%); } }
  @keyframes xone-slideOutLeft { from { transform: translateX(0); } to { transform: translateX(-100%); } }
  @keyframes xone-slideInUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
  @keyframes xone-slideOutUp { from { transform: translateY(0); } to { transform: translateY(-100%); } }
  @keyframes xone-slideInDown { from { transform: translateY(-100%); } to { transform: translateY(0); } }
  @keyframes xone-slideOutDown { from { transform: translateY(0); } to { transform: translateY(100%); } }
  @keyframes xone-zoomIn { from { transform: scale(0); opacity: 0; } to { transform: scale(1); opacity: 1; } }
  @keyframes xone-zoomOut { from { transform: scale(1); opacity: 1; } to { transform: scale(0); opacity: 0; } }
  @keyframes xone-rotate3dIn { from { transform: perspective(400px) rotateY(90deg); opacity: 0; } to { transform: perspective(400px) rotateY(0); opacity: 1; } }
  @keyframes xone-rotate3dOut { from { transform: perspective(400px) rotateY(0); opacity: 1; } to { transform: perspective(400px) rotateY(90deg); opacity: 0; } }
}
@layer xone-project {
.xone-coll {
    --xone-cell-border: false;
    --xone-cell-border-width: 0;
    --xone-cell-tpadding: 2px;
    --xone-cell-bpadding: 2px;
}

.xone-prop {
    --xone-text-border-top: none;
    --xone-text-border-bottom: solid;
    --xone-text-border-right: none;
    --xone-text-border-left: none;
    --xone-grid-text-border: false;
    font-weight: normal;
    font-size: 15px;
    --xone-labelfont-size: 7;
    margin-left: 0px;
    --xone-img-spinner: bt_Arrow_down.png;
    --xone-img-spinner-sel: bt_Arrow_down_Sel.png;
    --xone-img-search: bt_Lupa.png;
    --xone-img-search-sel: bt_Lupa_sel.png;
    --xone-img-delete: bt_Delete.png;
    --xone-img-delete-sel: bt_Delete_sel.png;
    --xone-img-undo: undo.png;
    --xone-img-undo-sel: undo_click.png;
    --xone-img-phone: bt_Phone.png;
    --xone-img-phone-sel: bt_Phone_sel.png;
    --xone-img-date: bt_Date.png;
    --xone-img-date-sel: bt_Date_sel.png;
    --xone-img-time: bt_Time.png;
    --xone-img-time-sel: bt_Time_sel.png;
    --xone-img-checked: bt_check.png;
    --xone-img-checked-disabled: bt_check_disabled.png;
    --xone-img-unchecked: bt_uncheck.png;
    --xone-img-unchecked-disabled: bt_uncheck_disabled.png;
    --xone-img-att: bt_attach.png;
    --xone-img-att-sel: bt_attach_sel.png;
    --xone-img-camera: bt_camera.png;
    --xone-img-camera-sel: bt_camera_sel.png;
    --xone-img-video: bt_camera.png;
    --xone-img-video-sel: bt_camera_sel.png;
    --xone-img-height: 28;
    --xone-img-width: 28;
}

.darkColorColl {
    --xone-cell-border: false;
    --xone-cell-border-width: 0;
    --xone-cell-tpadding: 2px;
    --xone-cell-bpadding: 2px;
}

.xnHeaderBar {
    width: 100%;
    height: 10%;
    --xone-align: left|center;
}

.xnBottomBar {
    width: 100%;
    height: 10%;
}

.xnDrawer {
    width: 75%;
    height: 75%;
}

.xnBodyOuterFrame {
    width: 100%;
    height: 100%;
    --xone-align: center;
}

.xnBodyInnerFrame {
    width: 98%;
    height: 100%;
}

.xnBodyDarkFrame {
    width: 100%;
    height: 100%;
    --xone-align: center;
}

.xnFloatingFrame {
    position: absolute;
    border: none;
}

.xnFloatingBackFrame {
    position: absolute;
    width: 100%;
    height: 100%;
    left: 0%;
    top: 0%;
    --xone-grid-framebox: false;
}

.xnDarkTitle {
    font-size: 17px;
}

.xnLightTitle {
    font-size: 17px;
}

.xnSubtitle {
    font-size: 16px;
}

.xnTextReadOnly {
    --xone-text-border-bottom: none;
}

.xnBorderTextEditable {
    --xone-text-border-top: solid;
    --xone-text-border-bottom: solid;
    --xone-text-border-right: solid;
    --xone-text-border-left: solid;
    --xone-grid-text-border: false;
    font-weight: normal;
    font-size: 15px;
    --xone-labelfont-size: 7;
    margin-left: 0px;
    --xone-img-spinner: bt_Arrow_down.png;
    --xone-img-spinner-sel: bt_Arrow_down_Sel.png;
    --xone-img-search: bt_Lupa.png;
    --xone-img-search-sel: bt_Lupa_sel.png;
    --xone-img-delete: bt_Delete.png;
    --xone-img-delete-sel: bt_Delete_sel.png;
    --xone-img-undo: undo.png;
    --xone-img-undo-sel: undo_click.png;
    --xone-img-phone: bt_Phone.png;
    --xone-img-phone-sel: bt_Phone_sel.png;
    --xone-img-date: bt_Date.png;
    --xone-img-date-sel: bt_Date_sel.png;
    --xone-img-time: bt_Time.png;
    --xone-img-time-sel: bt_Time_sel.png;
    --xone-img-checked: bt_check.png;
    --xone-img-checked-disabled: bt_check_disabled.png;
    --xone-img-unchecked: bt_uncheck.png;
    --xone-img-unchecked-disabled: bt_uncheck_disabled.png;
    --xone-img-att: bt_attach.png;
    --xone-img-att-sel: bt_attach_sel.png;
    --xone-img-camera: bt_camera.png;
    --xone-img-camera-sel: bt_camera_sel.png;
    --xone-img-video: bt_camera.png;
    --xone-img-video-sel: bt_camera_sel.png;
    --xone-img-height: 28;
    --xone-img-width: 28;
}

.xnLink {
    --xone-text-border-bottom: none;
    font-style: italic;
}

.xnTextWebview {
    --xone-text-border-top: none;
    --xone-text-border-bottom: solid;
    --xone-text-border-right: none;
    --xone-text-border-left: none;
    --xone-grid-text-border: false;
    font-weight: normal;
    font-size: 15px;
    --xone-labelfont-size: 7;
    margin-left: 0px;
    --xone-img-spinner: bt_Arrow_down.png;
    --xone-img-spinner-sel: bt_Arrow_down_Sel.png;
    --xone-img-search: bt_Lupa.png;
    --xone-img-search-sel: bt_Lupa_sel.png;
    --xone-img-delete: bt_Delete.png;
    --xone-img-delete-sel: bt_Delete_sel.png;
    --xone-img-undo: undo.png;
    --xone-img-undo-sel: undo_click.png;
    --xone-img-phone: bt_Phone.png;
    --xone-img-phone-sel: bt_Phone_sel.png;
    --xone-img-date: bt_Date.png;
    --xone-img-date-sel: bt_Date_sel.png;
    --xone-img-time: bt_Time.png;
    --xone-img-time-sel: bt_Time_sel.png;
    --xone-img-checked: bt_check.png;
    --xone-img-checked-disabled: bt_check_disabled.png;
    --xone-img-unchecked: bt_uncheck.png;
    --xone-img-unchecked-disabled: bt_uncheck_disabled.png;
    --xone-img-att: bt_attach.png;
    --xone-img-att-sel: bt_attach_sel.png;
    --xone-img-camera: bt_camera.png;
    --xone-img-camera-sel: bt_camera_sel.png;
    --xone-img-video: bt_camera.png;
    --xone-img-video-sel: bt_camera_sel.png;
    --xone-img-height: 28;
    --xone-img-width: 28;
    --xone-align: justified;
}

.xnButtonAccent1 {
    border-radius: 8px;
    font-size: 15px;
}
.xnButtonAccent1.xone-frame {
    overflow-y: auto;
    overflow-x: hidden;
}
.xnButtonAccent1.xone-prop {
    overflow: hidden;
}

.xnButtonAccent2 {
    border-radius: 8px;
    font-size: 15px;
}
.xnButtonAccent2.xone-frame {
    overflow-y: auto;
    overflow-x: hidden;
}
.xnButtonAccent2.xone-prop {
    overflow: hidden;
}

.xnButtonAccent3 {
    border-radius: 8px;
    font-size: 15px;
}
.xnButtonAccent3.xone-frame {
    overflow-y: auto;
    overflow-x: hidden;
}
.xnButtonAccent3.xone-prop {
    overflow: hidden;
}

.xnButtonImg {
    width: 10%;
    margin-left: 1%;
}

.xnDarkButtonLine {
    width: 100%;
    height: 4px;
}

.xnLightButtonLine {
    width: 100%;
    height: 4px;
}

.xnCalendar {
    width: 100%;
    height: 100%;
    --xone-cell-border-width: 4;
    --xone-cell-align: center;
    --xone-align: center;
    font-size: 22px;
    --xone-weekdays-fontsize: 6;
    --xone-weekdays-longname: false;
    --xone-weekdays-align: top|left;
    border-width: 2px;
    border: none;
}

.xnCalendarTitle {
    font-size: 19px;
    --xone-align: center;
    --xone-text-border-bottom: none;
}

.xnCheckbox {
    --xone-text-border-top: none;
    --xone-text-border-bottom: solid;
    --xone-text-border-right: none;
    --xone-text-border-left: none;
    --xone-grid-text-border: false;
    font-weight: normal;
    font-size: 15px;
    --xone-labelfont-size: 7;
    margin-left: 0px;
    --xone-img-spinner: bt_Arrow_down.png;
    --xone-img-spinner-sel: bt_Arrow_down_Sel.png;
    --xone-img-search: bt_Lupa.png;
    --xone-img-search-sel: bt_Lupa_sel.png;
    --xone-img-delete: bt_Delete.png;
    --xone-img-delete-sel: bt_Delete_sel.png;
    --xone-img-undo: undo.png;
    --xone-img-undo-sel: undo_click.png;
    --xone-img-phone: bt_Phone.png;
    --xone-img-phone-sel: bt_Phone_sel.png;
    --xone-img-date: bt_Date.png;
    --xone-img-date-sel: bt_Date_sel.png;
    --xone-img-time: bt_Time.png;
    --xone-img-time-sel: bt_Time_sel.png;
    --xone-img-checked: bt_check.png;
    --xone-img-checked-disabled: bt_check_disabled.png;
    --xone-img-unchecked: bt_uncheck.png;
    --xone-img-unchecked-disabled: bt_uncheck_disabled.png;
    --xone-img-att: bt_attach.png;
    --xone-img-att-sel: bt_attach_sel.png;
    --xone-img-camera: bt_camera.png;
    --xone-img-camera-sel: bt_camera_sel.png;
    --xone-img-video: bt_camera.png;
    --xone-img-video-sel: bt_camera_sel.png;
    --xone-img-height: 28;
    --xone-img-width: 50p;
    --xone-text-bgcolor: #00000000;
}
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  @view-transition { navigation: none; }
}
/* Fuentes custom del proyecto (carpeta fonts/) — paridad XoneCSS.loadCustomTypeFace */
@font-face {
  font-family: 'SpaceMono-Regular';
  font-style: normal;
  font-weight: normal;
  font-display: swap;
  src: url('/fonts/SpaceMono-Regular.ttf') format('truetype');
}