:root{
  --ink:#1C2230; --gray:#5A6372; --mute:#9AA0AE; --line:#DDE1E8;
  --navy:#3A497D; --accent:#FF8C15; --accent-dn:#E07A00;
  --green:#1F7A4D; --green-bg:#EAF5EE; --green-bd:#BFE3CD;
  --warn:#B3402F; --warn-bg:#FBE9E7; --warn-bd:#EFC4BC;
  --bg:#F4F6F9; --card:#FFFDFD;
  --foot-clear:28px;
  /* Cover + confirmación legal (Claude Design C1/C2/C3) tokens: navy
     gradient stops, amber kicker, cover subtitle gray, trust/accepted
     green, and the C3 card/box borders. */
  --navy-2:#2A3560; --navy-3:#1F2748; --amber:#E8A33D; --sky:#CBD3E1;
  --mint:#4FC79E; --bd-soft:#E2E5EC; --bd-box:#CBD2DE;
  /* Tarjetas agrupadas (V34, variantes): borde con selección vs. sin
     ella — excepción tolerada del brief, dos tokens nuevos. */
  --sel-bd:#D3DAEA; --bd-card:#E6E9EF;
  /* Piso de tamaño de letra — §7 del brief de niveles: «texto nunca por
     debajo de 13,5px». Los usuarios son trabajadores en Android de gama
     media, no personal de oficina. 13,6px = .85rem con la raíz en 16px.
     Va como token para que el piso se pueda subir de un sitio y para que
     el próximo rótulo chico se note al escribirlo. */
  --fs-min:.85rem;
  /* Movimiento. La curva es la MISMA del glide entre pantallas, para que
     abrir un nivel y cambiar de pantalla se sientan del mismo material.
     El enrollado de las tarjetas con el filtro del saldo se deja como
     está (.43s ease, aprobado por Manu): esto se le suma, no lo pisa. */
  --mo:.45s; --mo-ease:cubic-bezier(.32,.72,0,1);
}
*{box-sizing:border-box;margin:0;padding:0}
/* White root canvas: iOS paints the overscroll/rubber-band gap above a sticky
   header with the ROOT background — so the top bleed reads white, not the green
   Disponible bar. overscroll-behavior-y also damps the bounce. */
html{-webkit-text-size-adjust:100%;background:#fff}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif;
  background:var(--bg);color:var(--ink);font-size:17px;line-height:1.45;min-height:100vh;
  overscroll-behavior-y:none;
  display:flex;flex-direction:column;
  /* Kill the browser double-tap-to-zoom gesture app-wide (a mis-tap next to
     the +/- steppers used to zoom in). Pinch-zoom is preserved for a11y. */
  touch-action:manipulation}
/* Variant A (Manu): the mobile brand ribbon is now a SLIM STICKY header so the
   brand + step tracker stay visible, and the top of the screen is always the
   blue ribbon (no green bleed). The green Disponible bar pins just below it. */
.topbar{background:linear-gradient(90deg,#3A497D 0%,#2A3560 55%,#1F2748 100%);color:#fff;padding:8px 0;display:flex;justify-content:center;position:sticky;top:0;z-index:20}
/* Breathing gap below the ribbon: an 8px page-colored strip that rides WITH the
   sticky ribbon, so the green Disponible bar never butts against the blue and no
   scrolling content peeks through when both are pinned. */
.topbar::after{content:"";position:absolute;left:0;right:0;top:100%;height:8px;background:var(--bg);pointer-events:none}
/* tb-inner mirrors .wrap (max-width:440 + 16px side padding). Logo on the
   left (disc + wordmark centered together), 'Combos Escolares' title to the
   right with padding. Card-02 colors: white carved disc + orange wordmark. */
.topbar .tb-inner{width:100%;max-width:440px;padding:0 16px;display:flex;align-items:center;justify-content:space-between;gap:16px}
.topbar .tb-logo{display:flex;align-items:center;gap:12px}
.topbar .tb-mark{flex:none;width:34px;height:34px;display:block;color:#fff}
.topbar .tb-word{height:28px;width:auto;color:#FFFDFD;display:block}
.topbar .tb-sub{font-size:.95rem;color:#9AA3B5;letter-spacing:.2px}
/* Compact step tracker in the sticky ribbon (mobile). Dots on the blue bg. */
.topbar .tb-track{display:flex;align-items:center;gap:5px;flex:none}
.topbar .tb-dot{width:7px;height:7px;border-radius:50%;background:rgba(255,255,255,.32)}
.topbar .tb-dot.on{width:18px;border-radius:4px;background:var(--accent)}
.topbar .tb-lbl{font-size:var(--fs-min);font-weight:600;color:#CBD3E1;margin-left:4px}
/* Logo (rail lockup + mobile ribbon) is a "start over" control (portal.js). */
.bp-lockup,.tb-logo{cursor:pointer}
/* Logo ball fills — SEALED client decision (2026-07-18): white disc, blue G3
   (option B). The temporary orange preview toggle is retired; the logo's click
   is the flow start-over. Rail (.bp-mark) + ribbon (.tb-mark) share these. */
.g3-disc{fill:#FFFDFD}
.g3-let{fill:#3A497D}
.bp-lockup,.tb-logo{cursor:pointer}
/* App-shell (mobile/tablet): .wrap is a flex column that fills the space
   between the sticky ribbon and the bottom, so the orange action footer can be
   pushed to the bottom edge (margin-top:auto) even when a screen's content is
   short — no more "floating button + dead gap". The page (body) scrolls; the
   footer stays pinned via position:sticky. Desktop resets this to a grid item. */
.wrap{flex:1;width:100%;max-width:440px;margin:0 auto;padding:20px 16px 0;
  display:flex;flex-direction:column}
@media(min-width:768px){.wrap{max-width:480px;padding-top:40px}}
/* In-content step counter: hidden on mobile (the sticky ribbon carries the
   tracker now); shown on desktop, where the ribbon is hidden. */
.steps{display:none;align-items:center;justify-content:center;gap:8px;margin:2px 0 18px}
.steps .dot{width:10px;height:10px;border-radius:50%;background:var(--line)}
.steps .dot.on{background:var(--accent);width:26px;border-radius:6px}
.steps .lbl{font-size:var(--fs-min);color:var(--gray);font-weight:600;margin-left:6px}
.card{background:var(--card);border:1px solid var(--line);border-radius:14px;
  padding:24px 20px;box-shadow:0 1px 3px rgba(31,42,68,.06)}
h1{font-size:1.35rem;font-weight:700;margin-bottom:8px}
.lead{color:var(--gray);font-size:.98rem;margin-bottom:20px}
.field{margin-bottom:16px}
.field label{display:block;font-size:var(--fs-min);font-weight:700;color:var(--gray);
  letter-spacing:.4px;text-transform:uppercase;margin-bottom:6px}
.field input,.field select{width:100%;min-height:52px;border:1.5px solid var(--line);
  border-radius:10px;padding:0 14px;font-size:1.15rem;font-weight:600;color:var(--ink);
  background:#fff;font-family:inherit}
.field input:focus,.field select:focus{outline:3px solid #FBD49A;border-color:var(--accent)}
/* Custom chevron on selects — the native <select> arrow sits flush against the
   edge. Drop it (appearance:none) and paint our own inline-SVG chevron with
   padding-right so the text never runs under it; left padding stays 14px (mirror
   of the inputs). Estado / Municipio / cédula prefix (Manu 2026-07-03). The
   chevron is a background image → taps pass straight through to the select.
   iOS Safari has appearance:none quirks → Manu's device is the final check. */
.field select{-webkit-appearance:none;-moz-appearance:none;appearance:none;
  padding-right:40px;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%235A6372' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 14px center}
.ci-row select{padding-right:34px;background-position:right 12px center}
/* Field-highlight validation (Manu): a missing/invalid required field turns its
   border red on a submit-attempt and clears live on input — no separate red note.
   The office list gets a red ring when none is picked. */
.field-err input,.field-err select{border-color:var(--warn);background:var(--warn-bg)}
.field-err input:focus,.field-err select:focus{outline:3px solid var(--warn-bd);border-color:var(--warn)}
#offs.field-err{outline:2px solid var(--warn);outline-offset:4px;border-radius:12px}
.hint{font-size:.85rem;color:var(--mute);margin-top:6px}
.ci-row{display:flex;gap:8px}
.ci-row select{width:84px;flex:none;text-align:center}
.btn{display:flex;align-items:center;justify-content:center;width:100%;min-height:54px;
  border:none;border-radius:12px;font-size:1.1rem;font-weight:700;font-family:inherit;
  cursor:pointer;text-decoration:none;text-align:center}
.btn-primary{background:var(--accent);color:#fff}
.btn-primary:active{background:var(--accent-dn)}
.btn-quiet{background:none;color:var(--gray);font-weight:600;min-height:48px;font-size:1rem}
.btn-navy{background:var(--navy);color:#fff}
.btn+.btn{margin-top:8px}
/* Footer action = the green header's exact rounded rectangle (48px tall, r10)
   so the green bar and the orange action read as the same shape (Manu #3).
   Scoped to the footer so modal buttons keep their own size. */
.ctk-foot .btn{min-height:48px;border-radius:10px}
.note{display:flex;gap:10px;align-items:flex-start;margin-top:18px;
  font-size:.88rem;color:var(--gray)}
.note .ic{flex:none;width:22px;height:22px;border-radius:50%;background:var(--bg);
  color:var(--gray);font-size:var(--fs-min);font-weight:700;display:flex;align-items:center;justify-content:center;
  border:1px solid var(--line);margin-top:1px}
.saldo-bar{background:var(--green-bg);border:1px solid var(--green-bd);border-radius:10px;
  padding:10px 14px;display:flex;justify-content:space-between;align-items:center;
  gap:10px;margin-bottom:16px}
.saldo-bar .l{font-size:1.1rem;font-weight:700;color:var(--green);letter-spacing:.2px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Value uses the SAME size/weight as the label + the buttons (1.1rem/700) so
   every green bar and orange button reads as one consistent font (Manu). */
.saldo-bar .v{font-size:1.1rem;font-weight:700;color:var(--green);
  font-variant-numeric:tabular-nums}
/* Shared header (spine): the back-link + label lead group for the carrito/
   disponible modes; centered label for the success mode (Listo). */
.saldo-bar .sb-lead{display:flex;align-items:center;gap:10px;min-width:0}
.saldo-bar .ctk-back{flex:none;color:var(--navy);font-weight:800;font-size:1.2rem;
  line-height:1;text-decoration:none;padding:2px}
.saldo-bar.suc,.saldo-bar.benefit{justify-content:center}
.money{font-variant-numeric:tabular-nums;font-weight:800;color:var(--green)}
.money-xl{font-size:2.4rem;line-height:1.1}
.alert-card{text-align:center;padding:32px 20px}
.alert-ic{width:64px;height:64px;border-radius:50%;margin:0 auto 18px;
  display:flex;align-items:center;justify-content:center;font-size:1.8rem;font-weight:800}
.alert-ic.warn{background:var(--warn-bg);border:2px solid var(--warn-bd);color:var(--warn)}
.alert-ic.ok{background:var(--green-bg);border:2px solid var(--green-bd);color:var(--green)}
.alert-ic.idle{background:var(--bg);border:2px solid var(--line);color:var(--gray)}
/* 8 digits (Edenred's OTP length). The card's inner width is only ~301px
   at a 375px viewport (wrap 375-32, card -2 border -40 padding), so fixed
   widths overflow. Boxes flex to fill whatever space exists and cap at
   36px so they don't balloon on desktop — fits every width, no magic
   numbers, no breakpoints. */
.otp-row{display:flex;gap:5px;justify-content:center;margin:20px 0 6px}
.otp-row input{flex:1 1 0;min-width:0;max-width:36px;height:50px;padding:0;
  text-align:center;font-size:1.15rem;font-weight:800;
  border:1.5px solid var(--line);border-radius:9px;font-family:inherit;color:var(--ink)}
.otp-row input:focus{outline:3px solid #FBD49A;border-color:var(--accent)}
.timer{text-align:center;font-size:.9rem;color:var(--gray);margin:10px 0 4px}
.timer b{font-variant-numeric:tabular-nums;color:var(--ink)}
/* Screen 09 (autoriza) — order summary the beneficiary reviews before the code:
   Resumen (line items + Subtotal/IVA/Total split) → Recoja en (pickup) → código.
   Reuses the app tokens; scoped to the pg-autoriza body class. */
.pg-autoriza .seclabel{font-size:var(--fs-min);font-weight:800;letter-spacing:.06em;
  text-transform:uppercase;color:var(--mute);margin:0 0 10px}
.pg-autoriza .sumrow{display:flex;gap:12px;align-items:center;padding:6px 0}
.pg-autoriza .sumrow .ph{flex:none;width:52px;height:52px;border-radius:8px;
  background:var(--bg);display:flex;align-items:center;justify-content:center;
  overflow:hidden;color:var(--mute);font-size:var(--fs-min)}
.pg-autoriza .sumrow .ph img{width:100%;height:100%;object-fit:contain;background:#fff}
.pg-autoriza .sumrow .bd{flex:1;min-width:0}
.pg-autoriza .sumrow .bd h2{font-size:1rem;font-weight:700;color:var(--ink);margin:0}
.pg-autoriza .sumrow .bd .q{font-size:.85rem;color:var(--gray)}
.pg-autoriza .sumrow .lt{font-weight:700;color:var(--ink);font-variant-numeric:tabular-nums}
.pg-autoriza .grand{margin-top:12px;padding-top:12px;border-top:2px solid var(--ink);
  display:flex;justify-content:space-between;align-items:baseline}
.pg-autoriza .grand .gl{font-size:1.05rem;font-weight:700;color:var(--ink)}
.pg-autoriza .grand .gv{font-size:1.5rem;font-weight:800;color:var(--green);
  font-variant-numeric:tabular-nums;line-height:1}
.pg-autoriza .sec{height:1px;background:var(--line);margin:18px 0}
.pg-autoriza .pick{display:flex;gap:10px;align-items:flex-start}
.pg-autoriza .pick .pin{flex:none;color:var(--navy);margin-top:1px}
.pg-autoriza .pick .pn{font-size:1rem;font-weight:700;color:var(--ink)}
.pg-autoriza .pick .pa{font-size:.85rem;color:var(--gray)}
.linkline{text-align:center;margin-top:14px;font-size:.92rem}
.linkline a{color:var(--navy);font-weight:600}
/* Gray legal footer: desktop chrome only. On mobile the orange action band owns
   the bottom edge (matches the iOS frame), so hide it there. (Legal line on
   mobile: revisit placement if wanted — flagged.) */
footer{display:none;padding:18px 16px 26px;text-align:center;font-size:var(--fs-min);color:var(--mute)}
footer a{color:var(--mute)}
/* The GRUPO+TRES mark replaces the company name inline (outlined, gray). */
.ft-mark{height:16px;width:auto;vertical-align:middle;display:inline-block}

/* Screen 05 - catalogo (combo cards, sticky cartbar) */
.combo{display:flex;gap:12px;background:var(--card);border:1px solid var(--line);
  border-radius:14px;padding:14px;margin-bottom:12px;box-shadow:0 1px 3px rgba(31,42,68,.05);
  overflow:hidden;max-height:400px}
/* Affordability filter — "Suave" roll-up/down (Arbiter-approved). The filter
   no longer snaps combos with display:none; portal.js toggles .is-rolled, which
   collapses the card's height + margin + fades + nudges up, in a small
   staggered wave (per-combo transition-delay set in JS). Resetting rolls them
   back down. So the eye SEES the filter act. max-height is generous so no
   combo content ever clips; the height+margin+opacity together carry the
   motion for the full ~.43s. (overflow:hidden clips the faint card shadow —
   the border keeps each card defined.) */
.combo{transition:max-height .43s ease,opacity .34s ease,transform .43s ease,
  margin .43s ease,padding .43s ease,border-width .43s ease}
.combo.is-rolled{max-height:0;opacity:0;margin-top:0;margin-bottom:0;
  padding-top:0;padding-bottom:0;border-top-width:0;border-bottom-width:0;
  transform:translateY(-6px)}
.combo .ph{flex:none;width:84px;height:84px;border-radius:10px;background:var(--bg);
  border:1px solid var(--line);display:flex;align-items:center;justify-content:center;
  color:var(--mute);font-size:var(--fs-min)}
.combo .bd{flex:1;min-width:0}
.combo h2{font-size:1.02rem;font-weight:700;margin-bottom:2px}
.combo .ds{font-size:var(--fs-min);color:var(--gray);margin-bottom:8px}
/* El precio manda en su propia línea y NUNCA se parte: junto al stepper,
   en un teléfono se partía en dos renglones y empujaba el «+» fuera de la
   tarjeta. */
/* La etiqueta de saldo cae a su propio renglón cuando no cabe al lado del
   precio. Antes `white-space:nowrap` en la fila entera la empujaba fuera de
   la tarjeta y quedaba recortada (28px a 360 con precios de tres cifras); el
   desbordamiento no se veía en el documento porque lo recorta la tarjeta. El
   precio sigue sin partirse: eso ahora lo garantiza `.pr-v`, no la fila. */
.combo .pr{margin:0 0 6px;font-size:1.1rem;font-weight:800;color:var(--accent);
  font-variant-numeric:tabular-nums;display:flex;flex-wrap:wrap;
  align-items:center;gap:2px 8px}
.combo .pr .pr-v{white-space:nowrap}
.combo .row{display:flex;align-items:center;justify-content:space-between;
  gap:10px;margin-top:0;flex-wrap:wrap}
/* No "Agregar" button — every card is a − n + stepper from the start (aligned
   with the detail; Manu). The + grays when another unit wouldn't fit the saldo
   (hard cap); the − grays at 0. */
.pg-catalogo .qty{display:flex;align-items:center;gap:8px}
.pg-catalogo .qty button{width:48px;height:48px;border:1.5px solid var(--line);background:#fff;border-radius:10px;
  font-size:1.3rem;font-weight:700;color:var(--navy);font-family:inherit;cursor:pointer}
.pg-catalogo .qty button:disabled{opacity:.3;cursor:not-allowed}
.pg-catalogo .qty .n{min-width:34px;text-align:center;font-size:1.15rem;font-weight:800}
.combo.added{border-color:var(--green-bd)}
/* Agotado (stock gate ON, no free qty in the CTK warehouse): the card stays
   visible but disabled — photo/body dim, the stepper hides (markup kept for
   portal.js) and the red «Agotado» tag takes its slot. Server-side the cart
   parser refuses the product regardless. */
.combo.agotado .ph,.combo.agotado .bd{opacity:.55}
.combo.agotado .qty{display:none}
.agotado-tag{font-size:var(--fs-min);font-weight:800;letter-spacing:.04em;
  text-transform:uppercase;color:var(--warn);background:var(--warn-bg);
  border:1px solid var(--warn-bd);border-radius:8px;padding:6px 10px}
.cartbar{position:sticky;bottom:0;background:var(--card);border-top:1px solid var(--line);
  padding:12px 16px calc(12px + env(safe-area-inset-bottom));margin:18px -16px -32px;display:none}
.cartbar.show{display:flex}
.cartbar .btn{margin:0}

/* ===== Tarjetas agrupadas (V34 — variantes) ===========================
   Producto con familia: el stepper se sustituye por un botón «Elegir
   {tipo}» que despliega el picker DENTRO de la tarjeta (nunca modal — a
   @390 un modal taparía la barra de saldo). Cada variante elegida es su
   propia línea con su propio stepper (reutiliza .qty, ya definido
   arriba). Los productos simples no cambian ni un píxel. */
.combo.combo-grp{flex-wrap:wrap;border-color:var(--bd-card)}
.combo.combo-grp.added{border-color:var(--sel-bd)}
.combo-grp .vpanel,.combo-grp .vlines{flex:0 0 100%;width:100%}
/* El roll-up del filtro de saldo (.is-rolled) sigue colapsando a 0; el
   techo de 400px de .combo se queda corto con el picker + varias líneas
   abiertas, así que la agrupada parte de uno más alto. La duración de
   la transición no cambia (fija en .43s), sólo el destino. */
.combo.combo-grp{max-height:1600px}
.combo.combo-grp.is-rolled{max-height:0}
.combo .pr .nv{font-size:var(--fs-min);font-weight:700;color:var(--gray)}
/* Lupa (sólo si la variante elegida trae hq=1 — portal.js la dibuja). */
.combo .ph{position:relative}
.ph .vz{position:absolute;right:4px;bottom:4px;width:24px;height:24px;border:none;
  border-radius:7px;background:rgba(28,34,48,.72);color:#fff;display:flex;
  align-items:center;justify-content:center;cursor:pointer;padding:0}
/* Punto de nivel (Arbiter, 2026-08-16): la única marca de procedencia que
   lleva un combo del pozo de Estilo libre cuando la búsqueda lo trae a la
   pantalla — misma variable `--niv-c` que ya pinta el punto de cada
   cabecera del acordeón (`.ctk-niv--xxx`), aquí puesta en la propia
   tarjeta porque esta pantalla no tiene alrededor un `.nivgrp` que la
   preste. `ctk_card` sólo dibuja el punto cuando `card['niv_codigo']`
   viene puesto (sólo el pozo de combos lo trae) — el resto de las
   tarjetas, cero cambio. */
.pv-dot{position:absolute;top:6px;left:6px;width:9px;height:9px;
  border-radius:50%;background:var(--niv-c,#2B3663);border:2px solid #fff;
  z-index:1}
/* Botón «Elegir/Listo/Cambiar {tipo}» — máquina de tres estados, borde
   siempre navy; el fondo alterna entre lleno (nada elegido, cerrado) y
   contorno (abierto O con líneas). El caret gira con aria-expanded. */
/* nowrap: el rótulo más largo es «Elegir presentación», y partido en dos
   renglones el botón crece de alto y empuja la tarjeta. Un tipo de variante
   con nombre largo se recorta antes que romper el renglón. */
.vpick{display:inline-flex;align-items:center;gap:7px;height:38px;padding:0 14px;
  border-radius:10px;border:1.5px solid var(--navy);background:var(--navy);
  color:var(--card);font:inherit;font-size:.92rem;font-weight:700;cursor:pointer;
  white-space:nowrap;max-width:100%;width:170px;justify-content:space-between}
/* 170px entra «Presentaciones», el rótulo más largo que se espera. Ancho
   FIJO —no mínimo— para que todos los botones del catálogo midan igual y la
   lista se lea como una columna y no como un serrucho. Un tipo con nombre
   más largo se recorta; nunca estira el botón ni parte el renglón. */
.vpick .vp-lbl{overflow:hidden;text-overflow:ellipsis}
.vp-caret{transition:transform .18s ease}
.vpick[aria-expanded="true"] .vp-caret{transform:rotate(180deg)}
/* Panel del picker: pegado a la tarjeta, borde superior punteado, 180ms. */
.vpanel{margin-top:11px;padding-top:11px;border-top:1px dashed var(--line)}
.vpanel[hidden]{display:none}
@media(prefers-reduced-motion:no-preference){
  .vpanel:not([hidden]){animation:vgrow .18s ease-out}
}
@keyframes vgrow{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:translateY(0)}}
/* Chips: rejilla de columnas iguales (V34 ronda 2, §D) — antes cada
   cápsula medía lo que medía su texto («Azul» contra «Vinotinto») y la
   fila quedaba dentada. auto-fit reparte columnas de mínimo 96px y las
   que no caben envuelven solas; a @390 entran 3 cómodas. Sin scroll
   horizontal en esta ronda: con más variantes, la fila envuelve más. */
.vp-chips{display:grid;grid-template-columns:repeat(auto-fit,minmax(96px,1fr));
  gap:7px;padding-bottom:2px}
/* Alto visual 32px/radio 16px (contrato). El área táctil EFECTIVA llega
   a 44px sin agrandar la píldora: un ::after invisible la desborda 6px
   por lado (32 + 6 + 6 = 44) — un clic ahí cuenta como clic en el chip,
   el navegador reporta el elemento anfitrión como target. */
.vchip{position:relative;display:inline-flex;width:100%;box-sizing:border-box;
  align-items:center;justify-content:center;min-width:0;
  gap:6px;height:32px;padding:0 12px;border-radius:16px;
  border:1.5px solid var(--sel-bd);background:var(--card);color:var(--navy);
  font-size:var(--fs-min);font-weight:700;cursor:pointer}
.vchip::after{content:"";position:absolute;inset:-6px}
.vchip[aria-checked="true"]{background:var(--navy);border-color:var(--navy);color:var(--card)}
.vchip .vc-ok{display:none;flex:none}
.vchip[aria-checked="true"] .vc-ok{display:inline-flex}
/* Rótulo que no quepa: puntos suspensivos, nunca desborda ni estira la
   cápsula (min-width:0 en el .vchip flex arriba es lo que permite que
   este ellipsis funcione dentro de un contenedor grid). */
.vchip .vc-l{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Chip atenuado: excede el saldo restante — nombre de clase del contrato.
   Atenúa y nada más: la geometría de la cápsula (alto, centrado, radio,
   padding) es la MISMA encendida que atenuada. Ver el bloque de la
   pantalla de retiro, acotado a `#offs` justo por esto: ahí `.off` es una
   fila de oficina —otra cosa con el mismo nombre—, y sin acotar ganaba
   por venir después en el archivo y le metía al chip su
   `align-items:flex-start` y su `padding:14px`, que era lo que descolocaba
   el rótulo en cuanto el chip se atenuaba. */
.vchip.off{opacity:.45;cursor:not-allowed}
/* El aviso «Las opciones atenuadas exceden tu saldo restante» se retiro el
   2026-08-03 junto con la etiqueta por ficha: mismo reproche, y encima
   tuteaba. Las capsulas atenuadas se explican por el boton apagado. */
/* Líneas: una por variante elegida, cada una con su propio stepper
   (.qty ya definido arriba — mismo tamaño que el de las simples). */
.vlines{margin-top:11px;padding-top:10px;border-top:1px solid var(--line);
  display:flex;flex-direction:column;gap:8px}
.vlines[hidden]{display:none}
.vline{display:flex;align-items:center;gap:9px}
.vl-l{flex:1;min-width:0;font-size:.9rem;font-weight:700;white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}
/* El pie sólo lleva el subtotal: «+ Agregar otro …» salió porque el botón
   del selector, siempre visible arriba, ya hace exactamente eso. */
.vfoot{display:flex;align-items:center;justify-content:flex-end;
  padding-top:8px;border-top:1px dashed var(--line)}
.vsub{font-size:.92rem;font-weight:800;color:var(--accent);font-variant-numeric:tabular-nums}

/* ===== Visor de foto ampliada (V34) ====================================
   Único y global, al final de #catalogo. Se abre sólo desde la lupa de
   una tarjeta agrupada (nunca sin hq=1). Mínimo a propósito: foto,
   nombre, variante, «Cerrar» — sin carrusel ni swipe. */
.vzoom{position:fixed;inset:0;z-index:62;display:flex;align-items:center;
  justify-content:center;background:rgba(20,25,38,.72);padding:20px}
.vzoom[hidden]{display:none}
.vzoom-card{width:100%;max-width:360px;background:var(--card);border-radius:16px;
  overflow:hidden;box-shadow:0 20px 55px rgba(12,16,26,.4)}
.vzoom-ph{height:280px;background:var(--bg);display:flex;align-items:center;
  justify-content:center}
.vzoom-ph img{width:100%;height:100%;object-fit:contain}
.vzoom-info{padding:14px 16px 16px;display:flex;align-items:center;
  justify-content:space-between;gap:14px}
.vzoom-txt{min-width:0}
.vzoom-name{font-size:.92rem;font-weight:700;line-height:1.3}
.vzoom-var{font-size:.86rem;font-weight:700;color:var(--navy);margin-top:2px}
.vzoom-x{flex:none;height:48px;border-radius:10px;border:none;background:var(--navy);
  color:var(--card);font:inherit;font-size:.88rem;font-weight:700;padding:0 16px;
  cursor:pointer}
@media(prefers-reduced-motion:no-preference){.vzoom:not([hidden]){animation:vzoomfade .16s ease-out}}
@keyframes vzoomfade{from{opacity:0}to{opacity:1}}

/* Screen 06 - carrito (lines, totals, insufficient-balance state) */
.line{display:flex;align-items:center;gap:12px;padding:14px 0;border-bottom:1px solid var(--line)}
.line:last-of-type{border-bottom:none}
.line .ph{flex:none;width:56px;height:56px;border-radius:8px;background:var(--bg);
  border:1px solid var(--line);display:flex;align-items:center;justify-content:center;color:var(--mute);font-size:var(--fs-min)}
.line .bd{flex:1;min-width:0}
.line h2{font-size:.95rem;font-weight:700}
.line .pr{font-size:.9rem;color:var(--gray);font-variant-numeric:tabular-nums}
.pg-carrito .qty{display:flex;align-items:center;gap:8px}
.pg-carrito .qty button{width:48px;height:48px;border:1.5px solid var(--line);background:#fff;border-radius:10px;
  font-size:1.3rem;font-weight:700;color:var(--navy);font-family:inherit;cursor:pointer}
.pg-carrito .qty button:disabled{opacity:.3;cursor:not-allowed}
.pg-carrito .qty .n{min-width:30px;text-align:center;font-size:1.05rem;font-weight:800}
.totals{margin-top:14px;padding-top:14px;border-top:2px solid var(--ink)}
.totals .r{display:flex;justify-content:space-between;font-size:1rem;margin-bottom:8px}
.totals .r b{font-variant-numeric:tabular-nums}
.totals .r.total{font-weight:800;padding-top:8px;margin-top:2px;border-top:1px solid var(--line)}
.totals .r.big{font-size:1.15rem;font-weight:800}
/* Carrito total (Manu): only the all-in Total remains (saldo + te-queda live in
   the header). Left-justified label + big green REF. */
/* Indent to line up with the product caption (.line .ph 56px + .line gap 12px),
   so Total/REF/IVA share the products' left edge — the full-width border stays. */
.totals .ctotal{display:flex;flex-direction:column;align-items:flex-start;gap:2px;
  margin-left:68px}
.ctotal .ct-l{font-size:var(--fs-min);font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--gray)}
.ctotal .ct-v{font-size:2rem;font-weight:800;color:var(--green);font-variant-numeric:tabular-nums;line-height:1.05}
.ctotal .iva-incl{margin-left:0}
.insuf{display:none;background:var(--warn-bg);border:1px solid var(--warn-bd);border-radius:10px;
  padding:12px 14px;margin:14px 0 4px;font-size:.92rem;color:var(--warn);font-weight:600}
body.over .insuf{display:block}
/* Over budget: Continuar is fully DISABLED (not just faded orange, which read
   as active) — clear gray, no orange, unmistakably off. */
body.over #cont{background:var(--line);color:var(--mute);box-shadow:none;pointer-events:none}
body.over #rest{color:var(--warn)}
.empty{text-align:center;color:var(--gray);padding:26px 0}

/* Screen 08 - retiro (location-first: estado filter + unified office list,
   courier shown as a badge per office) */
/* ACOTADO A `#offs`: `off` es un nombre que esta hoja usa para DOS cosas
   sin parentesco —la fila de oficina de aquí y el chip de variante
   atenuado del picker (`.vchip.off`, arriba)—. A igual especificidad
   (0,1,0) mandaba el que viene después en el archivo, que es éste, así
   que un chip atenuado se llevaba `align-items:flex-start`, `padding:14px`
   y `border-radius:11px` de una fila de oficina: el rótulo se despegaba
   del centro de la cápsula justo al atenuarse. Las filas las crea
   portal.js y SIEMPRE dentro de `#offs` (no hay ninguna en las
   plantillas), así que el prefijo no deja fuera ninguna. */
#offs .off{display:flex;align-items:flex-start;gap:12px;border:1.5px solid var(--line);border-radius:11px;
  padding:14px;margin-bottom:10px;cursor:pointer;background:#fff}
#offs .off .rad{flex:none;width:22px;height:22px;border-radius:50%;border:2px solid var(--mute);background:#fff;margin-top:2px}
#offs .off .bd{flex:1;min-width:0}
#offs .off .hdr{display:flex;align-items:center;justify-content:space-between;gap:8px}
#offs .off h2{font-size:.98rem;font-weight:700}
#offs .off .ad{font-size:var(--fs-min);color:var(--gray);margin-top:2px}
#offs .off .badge{flex:none;font-size:var(--fs-min);font-weight:700;padding:2px 9px;border-radius:999px;background:var(--line);color:var(--gray)}
#offs .off .badge.badge-tealca{background:#E1F5EE;color:#085041}
#offs .off .badge.badge-zoom{background:#E6F1FB;color:#0C447C}
#offs .off.sel{border-color:var(--accent);background:#FFF6EC}
#offs .off.sel .rad{border-color:var(--accent);background:var(--accent);box-shadow:inset 0 0 0 4px #fff}
.hint{font-size:.9rem;color:var(--gray);padding:10px 2px;line-height:1.5}
.btn.btn-primary:disabled{opacity:.45;cursor:not-allowed}

/* Screen 09 - autoriza (amount panel) */
.monto{background:#FFF3E3;border:1px solid #FBD49A;border-radius:12px;padding:16px;margin-bottom:18px;text-align:center}
.monto .l{font-size:var(--fs-min);font-weight:800;color:#B5670F;letter-spacing:.6px}
.monto .v{font-size:2.4rem;line-height:1.15;font-weight:800;color:#B5670F;font-variant-numeric:tabular-nums}
.monto .iva-incl{display:inline-block;font-size:var(--fs-min);font-weight:700;color:#B5670F;
  opacity:.85;margin:2px 0 0;text-decoration-color:#B5670F}
.monto .d{font-size:.85rem;color:var(--gray);margin-top:4px}

/* Screens 10 / 10b - confirmacion (guia panel, next steps) */
.guia{background:var(--bg);border:1.5px dashed var(--mute);border-radius:12px;padding:14px;margin:18px 0;text-align:center}
.guia .l{font-size:var(--fs-min);font-weight:800;color:var(--gray);letter-spacing:.6px}
.guia .v-row{display:flex;align-items:center;justify-content:center;gap:10px;margin:2px 0}
.guia .v{font-size:1.7rem;font-weight:800;color:var(--navy);font-variant-numeric:tabular-nums;letter-spacing:1px}
.guia-copy{display:inline-flex;align-items:center;gap:5px;flex:none;border:1.5px solid var(--line);
  background:#fff;color:var(--navy);border-radius:8px;padding:6px 9px;font-family:inherit;
  font-size:var(--fs-min);font-weight:700;cursor:pointer}
.guia-copy.done{color:var(--green);border-color:var(--green-bd);background:var(--green-bg)}
.guia .c{font-size:.85rem;color:var(--gray);margin-top:2px}
.guiab{background:#FFF3E3;border:1.5px dashed #FBD49A;border-radius:12px;padding:16px;margin:18px 0;text-align:center}
.guiab .l{font-size:var(--fs-min);font-weight:800;color:#B5670F;letter-spacing:.6px;margin-bottom:4px}
.guiab p{font-size:.92rem;color:var(--ink)}
.next{text-align:left;margin:18px 0 6px}
.next .st{display:flex;gap:12px;margin-bottom:12px;align-items:flex-start}
.next .n{flex:none;width:26px;height:26px;border-radius:50%;background:var(--navy);color:#fff;
  font-size:.85rem;font-weight:800;display:flex;align-items:center;justify-content:center}
.next p{font-size:.95rem;color:var(--ink);padding-top:2px}
.next p span{color:var(--gray)}

/* Product photos inside the placeholder boxes (catalogo + carrito) */
.combo .ph img,.line .ph img{width:100%;height:100%;object-fit:contain;background:#fff;border-radius:inherit}

/* Full-bleed action footer (mobile/tablet). Spans the whole viewport width
   (not just the centered content column, which looked like a detached floating
   rectangle on laptop widths). The action inside is constrained + centered to
   line up with the content above it. Declared BEFORE the >=992px media query
   so the desktop "go static" rule below correctly overrides it (mobile-first;
   matches how .cartbar is ordered). */
/* --- Tokenized footer clearance (DESIGN SYSTEM — shared with future portals) -
   The sticky action footer (.ctk-foot / detalle's .dfoot / the store's .cartbar,
   which also carries .ctk-foot) can overlay the bottom of a scrolling screen.
   The STORE set this standard with a per-screen desktop-only margin; here it's
   tokenized so the content block immediately before ANY sticky footer clears it
   automatically — one value (--foot-clear), both widths, every screen. Any new
   screen (incl. the wholesale portal) that puts a .card or .combo before a
   .ctk-foot/.dfoot inherits the clearance with zero extra markup. */
/* AMPLIADO: valía sólo si lo que precedía al pie era una .card o un .combo.
   En el catálogo lo que va antes es el último .nivgrp —y el pie ni siquiera
   cuelga de .wrap, cuelga de #catalogo—, así que no se aplicaba NADA y el
   último control quedaba debajo de la barra del carrito. Ahora vale para
   cualquier DESCENDIENTE de .wrap que preceda al pie; descendiente y no
   hijo directo, y con `.wrap` delante para ganarle en especificidad a
   `.nivgrp{margin-bottom:14px}`, que si no se lo comía.
   El valor lo mide portal.js: 28px fijos se quedaban cortos frente a un pie
   real de 48px más relleno más el área segura del teléfono. */
.wrap *:has(+ .ctk-foot),
.wrap *:has(+ .dfoot),
:where(.card,.combo):has(+ .ctk-foot),
:where(.card,.combo):has(+ .dfoot){margin-bottom:var(--foot-clear)}
.ctk-foot{position:sticky;bottom:0;z-index:5;background:var(--bg);
  border-top:1px solid var(--line);box-shadow:0 -6px 16px rgba(28,34,48,.08);
  width:100vw;margin:auto 0 0 calc(50% - 50vw);
  padding:8px 16px calc(8px + env(safe-area-inset-bottom));
  display:flex;flex-direction:column;align-items:center}
.ctk-foot > *{width:100%;max-width:440px;margin:0}
@media(min-width:768px){.ctk-foot > *{max-width:480px}}
/* Form screens: the <form> wraps both the scrolling .card and the pinned
   .ctk-foot so the submit button lives in the form (native submit) while the
   band still pins to the bottom. display:contents removes the form's own box so
   .card + .ctk-foot become direct flex children of .wrap. Degrades gracefully
   (older engines: form is a normal block, footer just sits after content). */
.ctk-form{display:contents}
/* Screens whose content lives in a data-carrying wrapper (#catalogo/#carrito/
   #detalle) must let that wrapper fill .wrap and become a flex column, so the
   footer inside can pin to the bottom (margin-top:auto) even on short content —
   otherwise the wrapper is content-height and the footer floats mid-screen. */
#catalogo,#carrito,#detalle{flex:1;display:flex;flex-direction:column;min-height:0}

/* ---------- Desktop staging: split panel (>=992px) — GOV-approved spec,
   copied verbatim from prototypes/portal-v1 styles.css ---------- */
.brand-panel{display:none}
@media(min-width:992px){
  body{display:grid;grid-template-columns:minmax(380px,42%) 1fr;grid-template-rows:1fr auto}
  .brand-panel{display:flex;flex-direction:column;justify-content:space-between;
    grid-row:1/3;grid-column:1;padding:48px 52px 24px;color:#FFFDFD;position:relative;overflow:hidden;
    background:linear-gradient(165deg,#3A497D 0%,#283260 45%,#161D38 100%)}
  /* On-brand watermark: the real +3 glyph (watermark-plus3-fill, vectors —
     the "3" is the actual G3 glyph), faint white, rising from the bottom-
     right corner. Replaces the CSS system-font "+3" placeholder. */
  .bp-watermark{position:absolute;right:-150px;bottom:-118px;width:560px;height:auto;
    color:#fff;opacity:.05;pointer-events:none}
  /* Rail logo: the real lockup (orange disc + white G3 + white outlined
     GRUPO/+TRES + gray subtitle) — same vectors as the mobile header, no
     font. Replaces the "Grupo +3" text placeholder. */
  /* Rail logo (card 02): white disc + navy-cutout G3 + ORANGE wordmark,
     as one unit. 'Combos Escolares' is NOT part of the logo — it sits
     below as a separate gray title (.bp-title). */
  .bp-lockup{display:flex;align-items:center;gap:18px;position:relative;z-index:1}
  .bp-mark{flex:none;width:62px;height:62px;display:block;color:#fff}
  .bp-word{height:46px;width:auto;color:#FFFDFD;display:block}
  .bp-title{margin-top:14px;font-size:.9rem;font-weight:400;color:#8E99B0;
    line-height:1.55;letter-spacing:.2px}
  .bp-steps{list-style:none;position:relative}
  .bp-steps li{position:relative;padding:0 0 8px}
  .bp-steps li:not(:last-child){padding-bottom:30px}
  .bp-steps li:not(:last-child)::after{content:"";position:absolute;left:14px;top:34px;bottom:4px;
    width:2px;background:rgba(255,255,255,.12)}
  .bp-steps a{display:flex;gap:14px;align-items:center;text-decoration:none;pointer-events:none}
  .bp-steps .c{flex:none;width:30px;height:30px;border-radius:50%;display:flex;align-items:center;
    justify-content:center;font-size:.85rem;font-weight:800;
    border:2px solid rgba(255,255,255,.25);color:rgba(255,255,255,.45);background:transparent;
    transition:none}
  .bp-steps .t{font-size:1rem;font-weight:600;color:rgba(255,255,255,.45)}
  .bp-steps li.done a{pointer-events:auto;cursor:pointer}
  .bp-steps li.done .c{background:rgba(46,158,91,.3);border-color:rgba(122,210,158,.6);color:#7AD29E}
  .bp-steps li.done .c::before{content:"\2713"}
  .bp-steps li.done .c span{display:none}
  .bp-steps li.done .t{color:#CBD3E1}
  .bp-steps li.done a:hover .t{color:#fff;text-decoration:underline}
  /* Future/locked steps: clickable (so the warning can fire) but the cursor says
     no; portal.js railNav intercepts the click and shows the inline warning. */
  .bp-steps li:not(.done):not(.cur) a{pointer-events:auto;cursor:not-allowed}
  .bp-steps .rail-warn{position:absolute;left:44px;top:0;z-index:3;white-space:nowrap;
    background:var(--warn-bg);color:var(--warn);border:1px solid var(--warn-bd);
    border-radius:8px;padding:4px 10px;font-size:var(--fs-min);font-weight:700;
    box-shadow:0 6px 16px rgba(0,0,0,.28);pointer-events:none;
    opacity:0;transform:translateY(-4px);transition:opacity .18s ease,transform .18s ease}
  .bp-steps .rail-warn.show{opacity:1;transform:translateY(0)}
  .bp-steps li.cur .c{background:var(--accent);border-color:var(--accent);color:#fff;
    box-shadow:0 0 0 5px rgba(255,140,21,.22)}
  .bp-steps li.cur .t{color:#fff;font-weight:800}
  body[data-step="6"] .bp-steps li.cur .c{background:var(--green);border-color:var(--green);
    box-shadow:0 0 0 5px rgba(46,158,91,.25)}
  /* Bottom caption (what the portal does). Bigger + sits crisply above the
     watermark, lowered so it clears the G3 cutout. */
  .bp-foot{font-size:.92rem;color:#8E99B0;position:relative;z-index:1;line-height:1.55}
  .bp-foot a{color:#B9C2D4;text-decoration:underline}
  .bp-foot a:visited{color:#B9C2D4}
  .topbar{display:none}
  /* Desktop tracker = the blue brand rail (.bp-steps) ONLY. The in-content
     .steps dots stay hidden here — they'd duplicate the rail (Manu, 2026-07-03).
     The green Disponible bar pins to the very top of the content column. The
     .saldo-bar specificity is needed to beat the base `.ctk-head{top:58px}`
     (declared LATER in the file); without it the header stuck 58px down and
     (a) pulled content up behind it at rest, (b) mis-seated the funnel. */
  .saldo-bar.ctk-head{top:0}
  /* Content column is a full-height flex COLUMN, not a centered block: green
     header pins at the TOP, the body fills the middle, the orange action pins
     at the BOTTOM — the same end-to-end frame as @390, uniform on every screen.
     (Was align-self:center → the green header floated low and the button
     floated mid-page, differently on each screen. Manu, 2026-07-03.) */
  .wrap{grid-row:1;grid-column:2;justify-self:center;width:100%;
    max-width:560px;padding:40px 48px 0;display:flex;flex-direction:column}
  /* Gray legal footer is GONE from the content column (moved into the blue rail's
     .bp-foot, Manu) → the content column fills the full height, so the orange
     action pins to the very bottom of the page on every screen, like the store. */
  footer{display:none}
  .cartbar{width:auto;margin:0;border:none;padding:0;background:none}
  /* Action footer: margin-top:auto pins it to the column bottom on short
     screens; position:sticky keeps it visible while a long screen (catalogo)
     scrolls. Bare orange button, no white band — matches the @390 frame; the
     detalle .dfoot joins the same frame (drops its band — Manu's Issue A/(a)). */
  /* Footer = a page-colored OPAQUE band (like @390) so scrolling cards clip at
     the button instead of passing behind the now-solid orange (Manu #5). The
     band keeps the "white area" look; bg var(--bg) matches the column so it
     reads as a subtle floating shelf. margin-top:auto pins it bottom; sticky
     bottom:0 keeps it visible + masking while a long screen scrolls. */
  .ctk-foot{position:sticky;bottom:0;width:auto;margin:auto 0 0;
    border:none;box-shadow:none;padding:12px 0 20px;background:var(--bg)}
  .ctk-foot > *{max-width:100%}
  /* Detalle footer joins the same masked frame. Scope to #detalle so it BEATS
     the base .dfoot rule (declared later in the file → same specificity would
     otherwise win on source order and keep the old band). */
  #detalle .dfoot{position:sticky;bottom:0;margin:auto 0 0;
    border:none;box-shadow:none;padding:12px 0 20px;background:var(--bg)}
  #detalle .dfoot .dact{margin-bottom:12px}
  /* (Store last-combo clearance is now the tokenized --foot-clear rule near
     .ctk-foot — applied at BOTH widths, not just here.) */
}

/* Arbiter-requested addition (2026-06-12): on long pages (catalogo,
   carrito) the document scrolls — pin the brand panel to the viewport
   so the process rail stays visible while the right column scrolls. */
@media(min-width:992px){
  .brand-panel{position:sticky;top:0;height:100vh}
}

/* Screen 05b - combo detail (spec 05b-combo-detalle, verbatim) */
/* Pastel en la TARJETA del detalle, no en el marco de la foto (Arbiter,
   16-ago, segunda pasada: con foto real el marco teñido quedaba como dos
   franjas tras el letterbox). La .card del detalle carga ctk-niv--X
   (t-attf, respaldo 'lib') y toma la MISMA --niv-bg del acordeón
   (.ctk-niv--pre/pri/bac/uni/lib, más abajo) — el color no se inventa
   aquí. El marco vuelve a su gris neutro y la foto a su blanco. */
.card.ctk-niv--pre,.card.ctk-niv--pri,.card.ctk-niv--bac,
.card.ctk-niv--uni,.card.ctk-niv--lib{background:var(--niv-bg)}
.dphoto{width:100%;aspect-ratio:1/1;max-height:300px;border-radius:12px;background:var(--bg);
  border:1px solid var(--line);display:flex;align-items:center;justify-content:center;
  color:var(--mute);font-size:.9rem;margin-bottom:16px}
.dphoto img{width:100%;height:100%;object-fit:contain;background:#fff;border-radius:inherit}
.dprice{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:4px}
.dprice .p{font-size:1.5rem;font-weight:800;color:var(--accent);font-variant-numeric:tabular-nums}
.contents{margin:14px 0 6px}
.contents h2{font-size:var(--fs-min);font-weight:800;color:var(--gray);letter-spacing:.5px;
  text-transform:uppercase;margin-bottom:10px}
.contents ul{list-style:none}
.contents li{display:flex;gap:10px;align-items:baseline;padding:9px 0;border-bottom:1px solid var(--line);
  font-size:.97rem}
.contents li:last-child{border-bottom:none}
.contents .q{flex:none;min-width:30px;font-weight:800;color:var(--navy);font-variant-numeric:tabular-nums}
/* Detail: qty stepper (mirrors the carrito stepper). The + disables once another
   unit would exceed the saldo — a hard cap, so you can't over-add (JS-driven). */
.pg-detalle .qty{display:flex;align-items:center;gap:8px}
.pg-detalle .qty button{width:48px;height:48px;border:1.5px solid var(--line);background:#fff;
  border-radius:10px;font-size:1.3rem;font-weight:700;color:var(--ink);line-height:1}
.pg-detalle .qty button:disabled{opacity:.3;cursor:not-allowed}
.pg-detalle .qty .n{min-width:34px;text-align:center;font-size:1.15rem;font-weight:800;font-variant-numeric:tabular-nums}
/* Detail sticky footer banner: the stepper + Regresar row over the live cart
   bar, pinned while the photo/contents scroll above it. */
.dfoot{position:sticky;bottom:0;z-index:5;background:var(--bg);border-top:1px solid var(--line);
  box-shadow:0 -6px 16px rgba(28,34,48,.08);padding:10px 12px 12px;margin-top:auto}
.dfoot .dact{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
.dfoot .dback{color:var(--navy);font-weight:600;font-size:1rem;text-decoration:none;padding:4px 6px}
/* iOS page-push glide (Manu) — direction-aware cross-document View Transitions,
   scoped to the store <-> product-detail navigation by portal.js (other navs
   skip). Product slides in from the right (fwd) / out to the right (back);
   older browsers just swap instantly (progressive enhancement). */
@view-transition{navigation:auto}
@media(prefers-reduced-motion:no-preference){
  html[data-vt="fwd"]::view-transition-new(root){animation:vt-in-right .55s cubic-bezier(.32,.72,0,1) both;z-index:2}
  html[data-vt="fwd"]::view-transition-old(root){animation:vt-out-left .55s cubic-bezier(.32,.72,0,1) both;z-index:1}
  html[data-vt="back"]::view-transition-old(root){animation:vt-out-right .55s cubic-bezier(.32,.72,0,1) both;z-index:2}
  html[data-vt="back"]::view-transition-new(root){animation:vt-in-left .55s cubic-bezier(.32,.72,0,1) both;z-index:1}
  /* Full-viewport hops (cover / restart confirms — chrome hidden, the whole
     screen slides) read faster than the body-only glide, so they run longer.
     Same curve, same distance; only the duration stretches. */
  html[data-vt-slow]::view-transition-old(root),
  html[data-vt-slow]::view-transition-new(root){animation-duration:.95s}
  /* Estilo libre, un punto más lento que el resto (Manu, 3-ago). No es un
     salto de pantalla sino un cambio de nivel de detalle DENTRO de la misma
     tienda, y a .55 se leía como un parpadeo. Su propio escalón para no
     tocar el .55 sellado del resto del flujo. */
  html[data-vt-easy]::view-transition-old(root),
  html[data-vt-easy]::view-transition-new(root){animation-duration:.75s}
}
@keyframes vt-in-right{from{transform:translateX(100%)}to{transform:translateX(0)}}
@keyframes vt-out-left{from{transform:translateX(0)}to{transform:translateX(-22%)}}
@keyframes vt-out-right{from{transform:translateX(0)}to{transform:translateX(100%)}}
@keyframes vt-in-left{from{transform:translateX(-22%)}to{transform:translateX(0)}}
/* Persistent chrome — the spine's "always on top" feel. The ribbon, the
   desktop brand rail, and the green header each get their OWN view-transition
   snapshot, so they're lifted out of the sliding `root`: they stay pinned
   while only the body slides beneath them, and the header content crossfades
   IN PLACE across the Disponible→Carrito shift (both screens name it ctk-head,
   so the browser morphs one into the other). Degrades to instant where View
   Transitions are unsupported. Each name is unique per page (one element each). */
.topbar{view-transition-name:ctk-topbar}
.brand-panel{view-transition-name:ctk-rail}
.ctk-head{view-transition-name:ctk-head}
/* The orange action footer is persistent chrome too: lift it out of the sliding
   root so it STAYS PUT (content crossfades in place) while only the body glides
   beneath it — was gliding with the page (Manu). One footer per screen
   (.ctk-foot OR the detalle .dfoot), so the name is unique per page. */
.ctk-foot,.dfoot{view-transition-name:ctk-foot}

/* ---------- T1 — Per-action loading feedback (in-button spinner) -------
   On the tap of a primary action — a form submit, or a .btn navigation
   link (Continuar, Ver mi carrito, Autorizar…) — the button hides its
   label, locks, and spins in place (portal.js). On a Caracas Slow-3G
   connection that gives instant "working…" feedback during the server
   round-trip and doubles as the double-tap guard. No full-screen overlay
   (Arbiter call 2026-06-29): the in-button spinner alone carries it. */
.btn.is-loading{position:relative;color:transparent !important;
  pointer-events:none;opacity:1}
.btn.is-loading::after{content:"";position:absolute;top:50%;left:50%;
  width:20px;height:20px;margin:-10px 0 0 -10px;border-radius:50%;
  border:3px solid rgba(255,255,255,.55);border-top-color:#fff;
  animation:ctk-spin .7s linear infinite}
@keyframes ctk-spin{to{transform:rotate(360deg)}}
/* Global navigation bar (Manu): a thin top line that appears on ANY navigation —
   link, primary CTA, or JS-driven nav — so the app always signals "working"
   during a server round-trip, even when the action isn't a spinnable button. It
   creeps toward 92% and never completes (the next page replaces it). */
.ctk-navbar{position:fixed;top:0;left:0;height:3px;width:0;z-index:100;
  background:var(--accent);box-shadow:0 0 8px rgba(224,122,0,.6);opacity:0}
.ctk-navbar.go{width:92%;opacity:1;
  transition:width 7s cubic-bezier(.1,.75,.2,1),opacity .12s}
@media(prefers-reduced-motion:reduce){.ctk-navbar.go{transition:opacity .12s;width:40%}}
/* quiet/navy buttons have a transparent/dark face — give the spinner ink. */
.btn-quiet.is-loading::after,.btn-navy.is-loading::after{
  border-color:rgba(58,73,125,.35);border-top-color:var(--navy)}
@media(prefers-reduced-motion:reduce){
  .btn.is-loading::after{animation-duration:1.5s}
}

/* ---------- T2 — persistent purchase chrome ----------
   Sticky "Disponible" header (top) + a sticky action footer (bottom) on the
   shopping screens. The footer is sticky only where the screen scrolls and
   has no text input (catalogo / carrito / retiro); the form screens keep
   their in-flow bottom CTA so the mobile keyboard never fights a pinned
   button. The Disponible header is top-anchored, safe on every screen. */
/* Disponible bar pins under the sticky ribbon + an 8px breathing gap (50+8) on
   mobile; on desktop the ribbon is hidden so it pins to the top. */
.ctk-head{position:sticky;top:58px;z-index:5}
/* Disponible header turns red when the cart exceeds the balance. */
.saldo-bar.over{background:var(--warn-bg);border-color:var(--warn-bd)}
.saldo-bar.over .l,.saldo-bar.over .v{color:var(--warn)}
/* Cart button (catalogo) — D-23 minimalist: the whole pill links to the cart.
   Left (.cb-l) shows the count; right (.cb-r) shows the all-in Total + a static
   "IVA incluido" caption. No breakdown/expander — the split lives on the factura. */
.ctk-cartbtn{display:flex;align-items:stretch;width:100%;min-height:48px;
  background:var(--accent);color:#fff;border-radius:10px;overflow:hidden;text-decoration:none}
.ctk-cartbtn .cb-l{display:flex;align-items:center;gap:8px;flex:1 1 auto;
  min-width:0;padding:0 18px;color:#fff;font-size:1.1rem;font-weight:700;
  white-space:nowrap}
.ctk-cartbtn .cb-ic{flex:none;display:block}
.ctk-cartbtn .cb-r{display:flex;flex-direction:column;align-items:flex-end;
  justify-content:center;gap:1px;flex:none;padding:0 18px;color:#fff;text-align:right}
.ctk-cartbtn .cb-total{font-size:1.1rem;font-weight:700;
  font-variant-numeric:tabular-nums;white-space:nowrap}
.ctk-cartbtn .cb-cap{font-size:var(--fs-min);font-weight:600;opacity:.88}
/* Empty cart (portal.js toggles .empty): drop the "IVA incluido" caption. Also
   neutralize the GENERIC .empty rule — the carrito "empty cart" MESSAGE uses the
   same class name (.empty{padding:26px 0}), which was padding the empty pill to
   78px. Reset it so the pill stays the 48px rounded rectangle, matching the
   green bar at every width (Manu, 2026-07-03). */
.ctk-cartbtn.empty .cb-cap{display:none}
.ctk-cartbtn.empty{padding:0}
/* Over budget on the store (Manu): the cart bar is the "proceed" action, so it's
   DISABLED while over — you can't leave the store; reduce quantities on the list
   (steppers) and the red Disponible header tells you why. */
body.over .ctk-cartbtn{background:var(--line);color:var(--mute);pointer-events:none}
/* "IVA incluido" micro-label (D-23/D-24): the app shows only the all-in total;
   the Subtotal/IVA split lives on the factura. Quiet, muted, next to the total.
   It's a tappable button (dotted underline = "tap me") that explains itself via
   a small bubble (see .iva-tip + portal.js). */
.iva-incl{display:inline-block;font-family:inherit;font-size:var(--fs-min);font-weight:600;
  color:var(--gray);margin-left:6px;letter-spacing:.2px;padding:0;border:0;background:none;
  cursor:pointer;text-decoration:underline dotted;text-underline-offset:2px}
/* Tap-to-explain bubble: non-blocking, positioned by portal.js under the label. */
.iva-tip{position:absolute;z-index:1200;max-width:250px;background:var(--ink);color:#fff;
  font-size:var(--fs-min);line-height:1.4;font-weight:500;letter-spacing:.1px;padding:9px 12px;
  border-radius:10px;box-shadow:0 8px 24px rgba(31,42,68,.3)}
/* Retiro: estado carried from Datos shown fixed, with a "Cambiar" link that
   reveals the dropdown. */
.edo-fixed{display:flex;align-items:center;justify-content:space-between;gap:10px;
  width:100%;min-height:52px;border:1.5px solid var(--line);border-radius:10px;
  padding:0 14px;font-size:1.15rem;font-weight:600;color:var(--ink);background:#fff}
.edo-cambiar{font-size:.95rem;font-weight:700;color:var(--navy);text-decoration:none;flex:none}

/* ---------- T3 — leftover-balance warning modal ----------
   Non-blocking nudge shown at carrito checkout when the cart leaves unused
   benefit ("te sobran REF X"). Dismissable; the user can continue anyway. */
.ctk-modal{position:fixed;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;
  background:rgba(28,34,48,.45);padding:18px}
.ctk-modal[hidden]{display:none}
/* Desktop: the dim still covers the whole screen, but the card centers within
   the CONTENT column (right side) rather than the viewport — the left 42% is
   the brand panel (grid-template-columns: minmax(380px,42%) 1fr). The left
   padding shifts only the flex content, not the backdrop. */
@media(min-width:992px){.ctk-modal{padding-left:42%}}
.ctk-modal-card{background:var(--card);border-radius:16px;padding:26px 20px 20px;width:100%;max-width:340px;
  position:relative;text-align:center;box-shadow:0 8px 30px rgba(31,42,68,.25)}
.ctk-modal-x{position:absolute;top:8px;right:12px;border:none;background:none;font-size:26px;line-height:1;
  color:var(--mute);cursor:pointer;font-family:inherit}
.ctk-modal-ic{width:56px;height:56px;border-radius:50%;background:#FFF3E3;border:2px solid #FBD49A;
  color:#B5670F;display:flex;align-items:center;justify-content:center;font-size:1.8rem;font-weight:800;margin:2px auto 14px}
.ctk-modal-card h2{font-size:1.15rem;font-weight:700;margin-bottom:8px}
.ctk-modal-card h2 span{font-variant-numeric:tabular-nums;color:#B5670F}
.ctk-modal-card p{font-size:.95rem;color:var(--gray);line-height:1.5;margin-bottom:18px}
.ctk-modal-card .btn{margin-top:8px}
/* [hidden] pierde contra cualquier display explícito (la regla del UA es
   la de menor peso): el .btn de arriba lo revivía DENTRO del aviso
   bloqueante del mínimo, con el servidor rebotando detrás. Aquí manda. */
.ctk-modal-card [hidden]{display:none!important}

/* ---------- Filtros unificados (catalogo) ----------
   Antes: un embudo sin etiqueta que casi nadie descubría, y al activarlo un
   aviso verde de dos líneas que en un teléfono se comía el alto de dos
   tarjetas Y empujaba el listado hacia abajo justo cuando el usuario estaba
   mirándolo. Ahora: un botón CON la palabra «Filtros» y una insignia con el
   número de filtros activos, un solo panel para saldo + categoría, y el
   estado activo en fichas que no desplazan nada. */
.saldo-bar .sb-right{display:flex;align-items:center;gap:9px;min-width:0}
/* Con el botón «Filtros» dentro, la barra se aprieta a 390: el valor no se
   parte y el botón cede ancho antes que él. */
.saldo-bar .v{white-space:nowrap}
.saldo-bar .sb-lead{min-width:0}
@media(max-width:420px){
  /* Sin canaletas: a 360 los 16px de cada lado eran 32 de 360 —casi un
     décimo del ancho— gastados en aire junto a unas barras que ya llevan su
     propio relleno. El contenido va de borde a borde y el respiro queda
     DENTRO de cada tarjeta. */
  .wrap{padding-left:0;padding-right:0}
  /* Lo mismo abajo (Manu): el pie pierde sus canaletas y el botón llega a
     los dos bordes. Las esquinas de ABAJO se cuadran porque quedan contra
     el filo de la pantalla, y redondeadas dejarían ver el fondo por detrás;
     las de arriba se quedan, que ahí sí hay contenido debajo del que
     despegarse. Esto toca TODAS las pantallas con pie —carrito, datos,
     retiro, autoriza—, y es lo esperado: es la misma regla de borde a borde. */
  .ctk-foot{padding-left:0;padding-right:0}
  .ctk-cartbtn,.ctk-foot .btn,.ctk-foot .btn-primary{border-radius:10px 10px 0 0}
  .saldo-bar{gap:8px}
  .saldo-bar .sb-right{gap:7px}
  /* Sólo teléfono. La etiqueta y la cifra pasan a dos renglones porque a 390
     no caben en uno y «Disponible» salía cortado en «Disponi…».
     La barra NO crece: su alto lo fija el embudo (32px) y el renglón único
     de texto no llegaba a tanto, así que los dos renglones (.74 + 1.05rem a
     1.15 de interlínea) ocupan ese hueco que ya estaba ahí. Lo de abajo no
     se mueve. El embudo va en columna propia sobre los dos renglones para
     quedar centrado, y .sb-right con display:contents sube la cifra y el
     botón a la rejilla sin tocar el marcado, que en escritorio los quiere
     juntos en un renglón. */
  /* Un solo renglón. Se apilaba por esta regla, NO por falta de sitio:
     medido a 360 el renglón pide 254px de los 300 disponibles. Apilado, la
     barra costaba 139px de una pantalla de 780. */
  .saldo-bar.ctk-head .v{font-size:1.05rem;line-height:1.15}
  .saldo-bar.ctk-head .l{font-size:var(--fs-min);line-height:1.15;letter-spacing:.3px;
    overflow:visible;text-overflow:clip}
  /* El rótulo del saldo se calla en el catálogo: la pantalla 2/6 ya dijo
     cuánto hay, y aquí el sitio vale más (Manu). El de «Carrito (N)» y los
     demás modos se quedan — ésos no lo dijo nadie antes. */
  .saldo-bar.ctk-head .sb-row .l{display:none}
  .ctk-filtros{height:32px;padding:0 9px}
  .ctk-filtros .fl-b{min-width:20px;height:20px;font-size:var(--fs-min)}
}
.ctk-sheet .sh-title{display:flex;align-items:center;color:var(--ink)}
/* En la cabecera del panel NO hace falta cápsula: el embudo pelado. Azul
   relleno mientras está abierto, azul de contorno mientras se cierra —
   el relleno hereda de .ctk-filtros.on, que aquí pinta en --navy. */
.ctk-sheet .sh-funnel{height:auto;padding:0;border:none;background:none;
  color:var(--navy)}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
/* Sólo el embudo y, si hay filtros puestos, la insignia. El nombre accesible
   va en aria-label: sin texto visible, un botón mudo no se puede anunciar. */
.ctk-filtros{flex:none;display:inline-flex;align-items:center;gap:5px;
  height:34px;padding:0 10px;cursor:pointer;border-radius:999px;
  border:1px solid var(--navy);background:var(--navy);color:var(--card);
  font:inherit;font-size:.86rem;font-weight:700;white-space:nowrap;
  -webkit-tap-highlight-color:transparent;transition:background .18s ease,color .18s ease,border-color .18s ease}
/* La cápsula va SIEMPRE rellena — no cambia de estado. Lo que cambia es el
   embudo de adentro: relleno mientras el panel está abierto, sólo contorno
   al cerrarlo. El «fill» de la hoja le gana al fill="none" del propio SVG. */
.ctk-filtros .af-funnel{fill:none;transition:fill .28s var(--mo-ease)}
.ctk-filtros.on .af-funnel{fill:currentColor}
.ctk-filtros .fl-b{display:inline-flex;align-items:center;justify-content:center;
  min-width:20px;height:20px;padding:0 5px;border-radius:999px;
  background:var(--accent);color:#fff;font-size:var(--fs-min);font-weight:700}
@media(min-width:992px){.ctk-filtros{height:38px}}

/* Fichas del estado activo. No empujan el listado: por eso reemplazan al
   aviso, en vez de sumarse a él. En teléfono ruedan en horizontal antes que
   partirse en dos líneas. */
.ctk-chips{display:flex;gap:8px;margin:0 0 8px;overflow-x:auto;
  -webkit-overflow-scrolling:touch;scrollbar-width:none}
.ctk-chips::-webkit-scrollbar{display:none}
@media(min-width:992px){.ctk-chips{flex-wrap:wrap;overflow:visible}}
.ctk-chips .chip{display:inline-flex;align-items:center;gap:7px;flex:none;
  padding:6px 11px;border-radius:999px;cursor:pointer;font:inherit;
  font-size:var(--fs-min);font-weight:700;white-space:nowrap}
.ctk-chips .chip-t{white-space:nowrap}
.ctk-chips .chip-x{font-size:1rem;line-height:1;opacity:.75}
/* .chip-saldo eliminado (V35): la ficha de saldo ya no existe en el filtro. */
.ctk-chips .chip-cat{background:#EEF1F8;border:1px solid #D3DAEA;color:var(--navy)}
.ctk-count{margin:0 0 10px;color:var(--mute);font-size:.86rem}
/* Mismo cuento que el panel: el JS los apaga con .hidden, pero «hidden»
   sólo esconde por la regla del navegador y el «display» de aquí le gana.
   Por eso la insignia se quedaba pintada con un 0 aunque no hubiera ningún
   filtro puesto, y la fila de fichas dejaba su hueco vacía. El panel es la
   excepción a propósito: ése se esconde con visibility para poder deslizar. */
/* `.ctk-cats` va en la lista: su insignia también declara display, así que
   le gana al [hidden] del navegador y salía dibujada con un «0». No se veía
   antes porque la pantalla siempre se entraba con una categoría puesta. */
.ctk-filtros .fl-b[hidden],.ctk-cats .fl-b[hidden],
.ctk-chips[hidden],.ctk-count[hidden]{display:none}

/* El panel. En teléfono sube desde abajo y conserva el contexto detrás —
   nunca una pantalla completa. En pantalla ancha se ancla al botón. El tope
   de alto va en la RAÍZ y no en el cuerpo: si va sólo en el cuerpo, cabecera
   + cuerpo + pie desbordan el contenedor. */
.ctk-sheet-ov{position:fixed;inset:0;z-index:60;background:rgba(28,34,48,.42);
  opacity:1;visibility:visible;transition:opacity .22s ease,visibility 0s}
.ctk-sheet{position:fixed;left:0;right:0;bottom:0;z-index:61;
  display:flex;flex-direction:column;max-height:78%;
  background:var(--card);border-radius:20px 20px 0 0;
  box-shadow:0 -10px 34px rgba(28,34,48,.22);
  transform:translateY(0);visibility:visible;
  transition:transform var(--mo) var(--mo-ease),visibility 0s}
/* El atributo «hidden» esconde SÓLO por la regla del navegador
   [hidden]{display:none}, y cualquier «display» de la hoja le gana: con
   display:flex el panel quedaba siempre a la vista y openPanel() no tenía
   ningún efecto. Se esconde con visibility —fuera del foco, del tabulador y
   del lector de pantalla— en vez de display:none, porque display corta la
   transición en seco y no habría de dónde ni hacia dónde deslizar. El 0s
   con retardo mantiene el panel visible mientras baja, y lo apaga al final. */
.ctk-sheet[hidden]{transform:translateY(100%);visibility:hidden;
  transition:transform var(--mo) var(--mo-ease),visibility 0s linear var(--mo)}
.ctk-sheet-ov[hidden]{opacity:0;visibility:hidden;
  transition:opacity var(--mo) var(--mo-ease),visibility 0s linear var(--mo)}
body.sheet-open{overflow:hidden}
/* Respuesta al toque. En un equipo de gama media el hundido es la única
   señal de que el dedo llegó: el resto de la reacción (el carrito, el
   conteo) tarda en pintarse. Se queda fuera la cabecera del acordeón —
   es pegajosa, y transformarla la saca de su sitio al bajar. */
@media(prefers-reduced-motion:no-preference){
  .pg-catalogo .qty button,.pg-carrito .qty button,.pg-detalle .qty button,
  .vpick,.ctk-cats,.lb-cta,.ctk-sheet .sh-cta,.ctk-filtros{
    transition:transform .12s var(--mo-ease)}
  .pg-catalogo .qty button:active,.pg-carrito .qty button:active,
  .pg-detalle .qty button:active,.vpick:active,.ctk-cats:active,
  .lb-cta:active,.ctk-sheet .sh-cta:active,
  .ctk-filtros:active{transform:scale(.96)}
}

.ctk-sheet .sh-grab{flex:0 0 auto;width:38px;height:4px;border-radius:999px;
  background:var(--line);margin:10px auto 2px}
.ctk-sheet .sh-head{flex:0 0 auto;display:flex;align-items:center;
  justify-content:space-between;gap:12px;padding:8px 18px 4px}
.ctk-sheet .sh-head h2{margin:0;font-size:1.16rem;color:var(--ink)}
/* Dos líneas y más chico que el título: es una salida, no la acción
   principal del panel. Alineado a la derecha para que las dos líneas
   terminen a plomo con el borde. */
.ctk-sheet .sh-clear{border:none;background:none;cursor:pointer;font:inherit;min-height:48px;
  font-size:var(--fs-min);line-height:1.15;font-weight:700;color:var(--navy);
  padding:6px;text-align:right;flex:none}
.ctk-sheet .sh-body{flex:1;min-height:0;overflow-y:auto;padding:6px 18px 12px}
.ctk-sheet .sh-sec{margin:12px 0 7px;color:var(--mute);font-size:var(--fs-min);
  font-weight:700;letter-spacing:.06em;text-transform:uppercase}
.ctk-sheet .sh-foot{flex:0 0 auto;padding:12px 18px calc(14px + env(safe-area-inset-bottom));
  border-top:1px solid var(--line)}
/* Azul, no naranja: el naranja es el color de la accion del carrito, y esta
   CTA se dibuja JUSTO encima de la barra del carrito — dos naranjas apilados
   compitiendo por el mismo toque. Ademas su gemela del acordeon («Ver los N
   articulos», .lb-cta) ya era azul, asi que las dos puertas a los sueltos
   por fin dicen lo mismo. */
.ctk-sheet .sh-cta{width:100%;min-height:48px;border:none;border-radius:12px;
  cursor:pointer;background:var(--navy);color:#fff;font:inherit;
  font-size:1rem;font-weight:700}
.ctk-sheet .sh-cta:active{background:var(--navy-3)}
@media(min-width:992px){
  .ctk-sheet-ov{background:transparent}
  .ctk-sheet{left:auto;right:24px;bottom:auto;top:96px;width:330px;
    max-height:calc(100% - 86px);border-radius:14px;
    box-shadow:0 12px 34px rgba(28,34,48,.2)}
  .ctk-sheet .sh-grab{display:none}
  body.sheet-open{overflow:auto}
  /* Anclado al botón: se abre DESDE el embudo y vuelve a subir hacia él.
     El origen es la esquina superior derecha del panel, que es justo la que
     queda debajo del botón, así que al escalar desde ahí el panel parece
     salir de él. Antes sólo había un fundido de .14s: correcto pero
     imperceptible, por eso a 1440 se sentía que no había animación. */
  .ctk-sheet{transform-origin:100% 0;transform:scale(1) translateY(0);opacity:1;
    transition:opacity .18s ease,transform .18s ease,visibility 0s}
  .ctk-sheet[hidden]{transform:scale(.94) translateY(-4px);opacity:0;
    transition:opacity .18s ease,transform .18s ease,visibility 0s linear .18s}
}
@media(prefers-reduced-motion:reduce){
  .ctk-sheet,.ctk-sheet[hidden],
  .ctk-sheet-ov,.ctk-sheet-ov[hidden]{transition:visibility 0s}
}

/* Filas del panel. Área táctil >=44px en las dos. */
.ctk-sheet .sh-saldo,.ctk-sheet .sh-cat{display:flex;align-items:center;gap:12px;
  min-height:56px;padding:10px 12px;margin:0 0 8px;cursor:pointer;
  border:1px solid var(--line);border-radius:12px;background:var(--card)}
.ctk-sheet .sh-saldo{background:var(--green-bg);border-color:var(--green-bd)}
.ctk-sheet .sh-txt{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
.ctk-sheet .sh-txt b{font-size:.98rem;color:var(--ink)}
.ctk-sheet .sh-txt small{font-size:var(--fs-min);color:var(--gray)}
.ctk-sheet .sh-cat.on{background:#EEF1F8;border-color:var(--navy)}
/* Vacía SÓLO por el saldo: sigue a la vista, apagada, con su 0. Es la
   respuesta a «¿por qué desaparecieron los kits?». */
.ctk-sheet .sh-cat.is-empty{opacity:.5}
.ctk-sheet .sh-sw,.ctk-sheet .sh-cb{opacity:0;width:0;height:0;margin:0;
  padding:0;border:0;flex:none;pointer-events:none}
.ctk-sheet .sh-sw-ui{flex:none;width:44px;height:26px;border-radius:999px;
  background:#C9D0DE;position:relative;transition:background .18s ease}
.ctk-sheet .sh-sw-ui::after{content:"";position:absolute;top:3px;left:3px;
  width:20px;height:20px;border-radius:50%;background:#fff;
  transition:transform .18s ease}
.ctk-sheet .sh-sw:checked + .sh-sw-ui{background:var(--green)}
.ctk-sheet .sh-sw:checked + .sh-sw-ui::after{transform:translateX(18px)}
.ctk-sheet .sh-cb-ui{flex:none;width:22px;height:22px;border-radius:6px;
  border:2px solid #C9D0DE;background:#fff;position:relative}
.ctk-sheet .sh-cb:checked + .sh-cb-ui{background:var(--navy);border-color:var(--navy)}
.ctk-sheet .sh-cb:checked + .sh-cb-ui::after{content:"";position:absolute;
  left:6px;top:2px;width:5px;height:10px;border:solid #fff;
  border-width:0 2px 2px 0;transform:rotate(45deg)}
.ctk-sheet .sh-sw:focus-visible + .sh-sw-ui,
.ctk-sheet .sh-cb:focus-visible + .sh-cb-ui{outline:2px solid var(--navy);outline-offset:2px}
.af-clear{border:none;background:none;cursor:pointer;font:inherit;
  font-weight:700;color:var(--navy);text-decoration:underline;padding:0}
.af-empty{text-align:center;color:var(--gray);font-size:.95rem;padding:24px 8px}

/* ===== Glass chrome (Arbiter, 2026-06-29) ============================
   Floating surfaces get a frosted "liquid glass" look so they read as
   floating ABOVE the content plane, not stuck to it. Only things that
   float: the sticky Disponible header, the floating action CTAs (catalogo
   cart pill + carrito/retiro primary buttons), and modal dialogs. In-flow
   content (cards, totals, the autoriza panel, form fields) stays solid.
   Header + CTAs frost at >=992px, where they float over scrolling content;
   on mobile the action bar is a solid ribbon (nothing behind to frost).
   Modals frost at all widths (they float over the dimmed backdrop). The
   over-budget header stays SOLID red (a warning shouldn't go translucent),
   hence :not(.over). @supports falls back to solid fills where the engine
   lacks backdrop-filter, so the CTA text never loses contrast. */
/* El esmerilado ya NO es sólo de escritorio. La razón que lo limitaba —«en
   móvil no hay nada detrás que esmerilar»— caducó con el acordeón: ahora hay
   tarjetas pasando por debajo a cualquier ancho, y el efecto es lo que da a
   entender que la barra flota sobre el contenido en vez de cortarlo.
   SIN banda: existió para tapar los costados del acordeón cuando asomaban
   por los arcos de las esquinas, y sin bordes ya no hay línea que tapar. Se
   pintaba en la capa de la barra (z-index 5) por encima de la cabecera
   pegada (4), así que se veía como un renglón de relleno entre las dos. */
.saldo-bar.ctk-head:not(.over){
  background:rgba(225,245,238,.62);
  -webkit-backdrop-filter:blur(16px) saturate(150%);
  backdrop-filter:blur(16px) saturate(150%);
  border:1px solid rgba(255,255,255,.5);
  box-shadow:none}
@media(min-width:992px){
  .saldo-bar.ctk-head:not(.over){
    box-shadow:0 8px 24px rgba(31,42,68,.12)}
  /* Primary CTAs + cart pill: SOLID orange at >=992px (was frosted glass →
     pale/low-contrast vs the solid mobile buttons + solid-white modal).
     Manu, 2026-07-03. The green header above stays frosted (content scrolls
     under it); only the action surfaces go solid. */
  .ctk-cartbtn,.ctk-foot .btn-primary{
    background:var(--accent);
    border:none;
    box-shadow:0 10px 30px rgba(31,42,68,.22)}
}
/* Modal (Manu): SOLID approved white so the pop-up POPS against the dimmed
   backdrop — not the earlier frosted/translucent look. */
.ctk-modal-card{
  background:var(--card);
  border:1px solid var(--line)}
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  /* Sin backdrop-filter el verde translúcido se vería lavado sobre lo que
     pase por detrás: se cae a verde sólido, a cualquier ancho. */
  .saldo-bar.ctk-head:not(.over){background:var(--green-bg)}
  @media(min-width:992px){
    .ctk-cartbtn,.ctk-foot .btn-primary{background:var(--accent)}
  }
  .ctk-modal-card{background:var(--card)}
}

/* ===== Screen 00 — cover (Claude Design C1 @1440 / C2 @390) ==========
   The program's front door at /ctk. Full-bleed navy corporate gradient,
   its own centered lockup topbar (the app's blue ribbon / brand rail /
   footer are hidden under body.pg-cover), G3 disc watermark rising from
   the bottom-right, amber kicker, three-step preview (step 1 filled
   orange, 2-3 rings at 40%), one orange «Comenzar» CTA, the condiciones
   link (visible, never blocking) and the trust line. Mobile-first: the
   base rules ARE the @390 frame; >=992px is the @1440 frame. */
body.pg-cover{background:var(--navy-3)}
body.pg-cover .topbar,body.pg-cover .brand-panel,body.pg-cover footer{display:none}
body.pg-cover .wrap{max-width:none;padding:0;display:flex;flex-direction:column}
.cv{position:relative;overflow:hidden;flex:1;display:flex;flex-direction:column;
  color:var(--card);
  background:linear-gradient(160deg,var(--navy) 0%,var(--navy-2) 55%,var(--navy-3) 100%)}
.cv-wm{position:absolute;right:-60px;bottom:-100px;width:240px;height:240px;
  opacity:.07;color:var(--card);pointer-events:none}
.cv-top{padding:22px 22px 0;display:flex;flex-wrap:wrap;justify-content:center;position:relative}
.cv-lockup{display:flex;align-items:center;gap:11px}
.cv-mark{flex:none;width:38px;height:38px;display:block}
.cv-word{height:27px;width:auto;color:var(--card);display:block}
.cv-phone{display:flex;align-items:center;justify-content:center;gap:7px;font-size:var(--fs-min);font-weight:600;color:rgba(255,253,253,.7);text-decoration:none;order:3;width:100%;margin-top:14px}
.cv-wa{flex:none;width:16px;height:16px;fill:#25D366}
.cv-phone:hover{color:#fff}
.cv-hero{padding:30px 24px 34px;text-align:center;position:relative;flex:1}
.cv-kicker{font-size:var(--fs-min);font-weight:800;letter-spacing:2.5px;color:var(--amber);text-transform:uppercase}
.cv-hero h1{font-weight:800;font-size:30px;letter-spacing:-.6px;line-height:1.12;
  color:var(--card);margin:12px 0 0}
.cv-sub{font-size:14px;line-height:1.55;color:var(--sky);margin-top:12px}
.cv-steps{display:flex;flex-direction:column;gap:9px;margin-top:22px;text-align:left}
.cv-step{display:flex;align-items:center;gap:11px;background:rgba(255,253,253,.07);
  border:1px solid rgba(255,253,253,.14);border-radius:11px;padding:11px 14px}
.cv-step .c{width:26px;height:26px;border-radius:50%;flex:none;
  border:2px solid rgba(255,253,253,.4);display:inline-flex;align-items:center;
  justify-content:center;font-weight:800;font-size:var(--fs-min);color:var(--card)}
.cv-step.cur .c{background:var(--accent);border:none}
.cv-step .t{font-size:13.5px;font-weight:700;color:rgba(255,253,253,.85)}
.cv-step.cur .t{color:var(--card)}
.cv-act{margin-top:22px}
.cv-cta{display:flex;align-items:center;justify-content:center;height:52px;
  border-radius:12px;background:var(--accent);color:var(--card);font-size:16px;
  font-weight:700;text-decoration:none}
.cv-cta:active{background:var(--accent-dn)}
/* El acuerdo legal se abre desde aquí, así que es un control y le toca el
   piso de 48dp del §7. El relleno se come el margen que ya tenía (14px →
   2px) para que el aire de la portada quede como estaba. */
.cv-legal{display:inline-block;font-size:var(--fs-min);font-weight:600;
  color:rgba(255,253,253,.75);text-decoration:underline;text-underline-offset:3px;
  margin-top:2px;padding:15px 12px;cursor:pointer}
.cv-trust{display:flex;align-items:flex-start;gap:8px;margin-top:18px;text-align:left}
.cv-trust .tr-ic{flex:none;margin-top:1px;color:var(--mint)}
.cv-trust span{font-size:var(--fs-min);font-weight:600;color:rgba(255,253,253,.65);line-height:1.5}
/* Pie legal de la portada — el emisor, al fondo del azul. `.cv-hero` lleva
   `flex:1`, así que esta línea cae sola contra el borde inferior sin tener
   que posicionarla. Va apagada al MISMO .65 de la línea de confianza de
   arriba: mismo registro y mismo peso visual, y sobre el navy más claro
   del degradado mide 4,68:1 (AA texto normal). `position:relative` +
   z-index para pasar por encima de la filigrana, que se asoma justo por
   esta esquina. Las «Condiciones del programa» viven aquí desde que la
   guía ocupó la acción secundaria: heredan el color del pie para no
   competir con «Comenzar». */
.cv-foot{padding:0 24px 22px;text-align:center;position:relative;z-index:1;
  font-size:var(--fs-min);font-weight:600;color:rgba(255,253,253,.65)}
.cv-foot a{color:inherit;text-decoration:underline;text-underline-offset:3px;
  cursor:pointer}
.cv-foot a:hover{color:rgba(255,253,253,.9)}
/* The cover has no brand panel: recenter the shared modal (its >=992px rule
   offsets the card past the rail). */
body.pg-cover .ctk-modal{padding-left:18px}
@media(min-width:992px){
  body.pg-cover{display:flex;flex-direction:column}
  .cv{background:linear-gradient(135deg,var(--navy) 0%,var(--navy-2) 55%,var(--navy-3) 100%)}
  .cv-wm{right:-90px;bottom:-160px;width:440px;height:440px;opacity:.06}
  .cv-top{padding:22px 48px;justify-content:space-between;align-items:center}
  .cv-mark{width:44px;height:44px}
  .cv-word{height:32px}
  .cv-phone{order:0;width:auto;margin-top:0;text-align:right}
  .cv-hero{padding:54px 48px 74px;max-width:720px;text-align:left}
  .cv-kicker{font-size:var(--fs-min);letter-spacing:3px}
  .cv-hero h1{font-size:52px;letter-spacing:-1px;line-height:1.08;margin-top:16px}
  .cv-sub{font-size:18px;line-height:1.6;max-width:560px;margin-top:18px}
  .cv-steps{flex-direction:row;gap:26px;margin-top:30px}
  .cv-step{background:none;border:none;border-radius:0;padding:0;gap:10px}
  .cv-step .c{width:30px;height:30px;font-size:14px}
  .cv-step .t{font-size:14.5px;color:rgba(255,253,253,.8)}
  .cv-act{display:flex;align-items:center;gap:18px;margin-top:36px}
  .cv-cta{display:inline-flex;height:56px;border-radius:13px;padding:0 34px;font-size:17px}
  .cv-legal{margin-top:0;font-size:13.5px}
  .cv-trust{align-items:center;margin-top:26px}
  .cv-trust span{font-size:var(--fs-min)}
  /* Alineado a la izquierda con el resto del marco @1440, como .cv-hero. */
  .cv-foot{padding:0 48px 26px;text-align:left}
}

/* ===== C3 — check legal en el punto de compromiso (autoriza) =========
   The whole card is the tap target (a label over a hidden real checkbox,
   never pre-checked). portal.js mirrors the checked state as .ok on the
   card + the foot hint and enables/disables the submit; the server
   re-validates the acceptance on submit regardless. */
.lchk{display:flex;gap:13px;align-items:flex-start;margin-top:18px;
  border-radius:13px;padding:16px 18px;cursor:pointer;
  background:var(--card);border:1.5px solid var(--bd-soft)}
.lchk.ok{background:var(--green-bg);border-color:var(--mint)}
.lchk-in{position:absolute;opacity:0;width:1px;height:1px;pointer-events:none}
.lchk:has(.lchk-in:focus-visible){outline:3px solid #FBD49A}
.lchk-box{width:22px;height:22px;border-radius:6px;flex:none;margin-top:1px;
  display:inline-flex;align-items:center;justify-content:center;
  background:var(--card);border:2px solid var(--bd-box)}
.lchk-box svg{display:none}
.lchk.ok .lchk-box{background:var(--green);border-color:var(--green)}
.lchk.ok .lchk-box svg{display:block}
.lchk-t{font-size:13.5px;line-height:1.55;color:#3C4354}
.lchk-t a{font-weight:700;color:var(--navy);text-decoration:underline;
  text-underline-offset:2px}
/* Sin aceptar the submit is a flat gray no-go (spec bd-box gray, not the
   generic faded orange), and the hint under it says why; accepted flips
   both. The hint rides inside the sticky .ctk-foot at both widths. */
.ctk-foot #autoriza-go:disabled{background:var(--bd-box);color:var(--card);
  opacity:1;cursor:not-allowed;box-shadow:none}
.lchk-hint{text-align:center;font-size:var(--fs-min);font-weight:600;color:var(--mute);
  margin-top:6px}
.lchk-hint .h-on{display:none}
.lchk-hint.ok .h-off{display:none}
.lchk-hint.ok .h-on{display:inline}

/* Condiciones del programa — shared overlay (cover + autoriza). Reuses the
   .ctk-modal frame; left-aligned reading card, scrollable body. The legal
   text is a placeholder pending the client's final version. */
.legal-card{max-width:420px;text-align:left}
.legal-card h2{font-size:1.1rem;font-weight:800;margin:0 0 10px;padding-right:26px}
.legal-body{font-size:.9rem;color:var(--gray);line-height:1.6;max-height:60vh;
  overflow:auto}

/* ===== V35 — Niveles educativos (acordeón + Estilo libre) =============
   Screen A (#catalogo.modo-niveles): un grupo colapsable por nivel, con
   la variante «Estilo libre» como atajo a categorías en vez de combos.
   Screen B (#catalogo.modo-libre): la barra de Estilo libre + su hoja de
   filtros en bronce. Base 360px (probado a 412, NO 390); el salto a
   valores grandes va detrás de min-width:768px, el mismo quiebre que ya
   usa .wrap para pasar de móvil a ancho — no se inventa un breakpoint
   nuevo. Mobile-first, como el resto del archivo. */

/* --- Grupo del acordeón --- */
.nivgrp{--niv-c:#2B3663;--niv-tx:#2B3663;--niv-bd:#B5B9C8;--niv-bg:#E4E7F1;
  border-radius:13px;margin-bottom:14px}
/* Vacío: se queda visible y apagado — es la respuesta a «¿por qué no está
   el mío?», no un elemento oculto. */
.nivgrp.is-empty{opacity:.5}
/* Un nivel vacio por el saldo ya no se deja abrir (portal.js pone `disabled`
   en su cabecera). El cursor deja de prometer un toque que no lleva a nada;
   el apagado ya estaba diciendolo. */
.nivgrp.is-empty .nivhead{cursor:default}
/* ===== PALETA DE NIVELES — catálogo impreso del cliente (25-ago) ======
   PROVISIONAL. Tonos LEÍDOS DEL GRÁFICO APROBADO; ajustar con el archivo
   de marca cuando llegue. Todo el color de nivel del portal sale de este
   bloque y de ningún otro sitio: cuando llegue la marca se cambia aquí y
   se acabó.

   Cuatro variables por nivel, tres de ellas DERIVADAS del principal:

     --niv-c   principal, el tono del catálogo tal cual. Sólo donde el
               color va SOLO, sin texto encima: el punto del acordeón
               (.nv-dot), el de la tarjeta del pozo (.pv-dot) y el de
               Estilo libre (.lb-dot).
     --niv-bg  tinte, el principal al 11% aplanado sobre blanco. Fondo de
               la cabecera abierta, del cuerpo del grupo, de la tarjeta de
               detalle y del marco de Estilo libre.
     --niv-bd  el principal al 35% aplanado sobre blanco. Borde de ficha.
     --niv-tx  tono de LECTURA. Es el principal oscurecido lo justo — el
               mismo matiz, sólo menos luz — para que el texto del nivel
               sobre su tinte y el blanco sobre la insignia lleguen a
               4,5:1 (AA texto normal; .nv-t mide 18px y no califica como
               texto grande). En azul y morado no hace falta oscurecer y
               --niv-tx ES el principal; el magenta, el coral y el naranja
               puros se quedan entre 2,1:1 y 3,9:1 sobre su propio tinte,
               que es ilegible, así que ahí el principal se queda en el
               punto y el texto usa este tono.

   Contraste medido (texto sobre su tinte / blanco sobre el texto):
     uni 8,76 / 10,66   bac 4,82 / 5,57   pri 4,72 / 5,51
     pre 4,66 / 5,26    lib 4,82 / 5,28
   Un código sin línea propia cae en el navy por defecto de .nivgrp. */
/* Preescolar — coral */
.ctk-niv--pre{--niv-c:#F1655D;--niv-tx:#D41E14;--niv-bd:#FAC9C6;--niv-bg:#FDEEED}
/* Primaria — magenta */
.ctk-niv--pri{--niv-c:#D63384;--niv-tx:#C22672;--niv-bd:#F1B8D4;--niv-bg:#FAE9F1}
/* Bachillerato — morado */
.ctk-niv--bac{--niv-c:#7E57A2;--niv-tx:#7E57A2;--niv-bd:#D2C4DE;--niv-bg:#F1EDF5}
/* Universitario — azul violáceo profundo */
.ctk-niv--uni{--niv-c:#2E3192;--niv-tx:#2E3192;--niv-bd:#B6B7D9;--niv-bg:#E8E8F3}
/* Estilo libre — naranja */
.ctk-niv--lib{--niv-c:#F7941D;--niv-tx:#A15A05;--niv-bd:#FCDAB0;--niv-bg:#FEF3E6}
/* Sin bordes (Manu, 2026-08-03). El color del nivel vive en el punto, el
   título y el TINTE; el marco se retiró. De paso desaparece por completo la
   fuga de las esquinas: no hay línea que pueda asomar por el arco de la
   barra verde, así que la banda deja de estar sosteniendo nada. */
.nivhead{transition:background var(--mo) var(--mo-ease);
  display:flex;align-items:center;gap:10px;width:100%;min-height:48px;
  border:none;background:#fff;color:inherit;cursor:pointer;
  font:inherit;text-align:left;border-radius:13px;padding:15px}
/* Cerrado: las 4 esquinas redondeadas (es todo lo que se ve). Abierto: sólo
   las de arriba — el cuerpo continúa por debajo con el resto del borde. */
/* Abierta, sólo las esquinas de ARRIBA. Se probaron las cuatro para que el
   encuentro con el tinte del nivel siguiente no quedara cortado, y salió
   peor: contra su propio cuerpo las de abajo dejan una costura visible, y
   esa se ve siempre — la otra sólo al pasar por encima de otro nivel.
   Revertido a propósito (Manu, 2026-08-03). */
.nivgrp.abierto .nivhead{background:var(--niv-bg);border-radius:13px 13px 0 0}
/* El cuerpo lleva el TINTE del nivel, que es lo que ahora dice dónde
   empieza y dónde acaba el grupo: sin borde, el color es el marco. Se queda
   por debajo de su cabecera con un z-index propio. */
.nivgrp.abierto .nivbody{background:var(--niv-bg);
  border-radius:0 0 13px 13px;position:relative;z-index:0}
/* El anillo de foco, sólo con teclado. Al tocar con el dedo o el ratón el
   control se queda enfocado y el anillo azul del navegador se quedaba puesto
   hasta tocar fuera o pulsar Escape: se lee como un estado raro del nivel,
   no como una ayuda. Con :focus-visible el anillo aparece cuando de verdad
   sirve —al navegar con el tabulador— y no cuando ya se sabe dónde se tocó.
   El color es el mismo ámbar que usan los campos del formulario, no el azul
   del navegador, que además choca con el tinte de cada nivel. */
.nivhead:focus,.lb-cat:focus,.ctk-cats:focus,.vpick:focus,
.ctk-filtros:focus,.lb-cta:focus,.qty button:focus{outline:none}
.nivhead:focus-visible,.lb-cat:focus-visible,.ctk-cats:focus-visible,
.vpick:focus-visible,.ctk-filtros:focus-visible,.lb-cta:focus-visible,
.qty button:focus-visible{outline:3px solid #FBD49A;outline-offset:2px}
.nv-dot{flex:none;width:10px;height:10px;border-radius:50%;background:var(--niv-c)}
/* El TÍTULO va en --niv-tx, no en el principal: es texto y le toca 4,5:1
   sobre el tinte de su propia cabecera abierta. El punto de al lado sí
   lleva el principal — ahí el color va solo. */
.nv-t{flex:1;min-width:0;font-size:18px;font-weight:800;letter-spacing:-.3px;
  color:var(--niv-tx);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Lo elegido en este nivel. Dos cifras con dos oficios en la misma
   cabecera: esta dice cuánto llevo, la gris de al lado cuánto hay ahí —y
   además es la única señal por nivel de que el embudo del saldo actuó, así
   que no se sustituye, se acompaña. En el color del NIVEL y no en naranja:
   el naranja es de las acciones y justo debajo está la barra del carrito.
   El [hidden] se declara aparte porque este display le gana al del
   navegador — la misma trampa que dibujaba un «0» en Categorías. */
/* Rellena con --niv-tx y no con el principal: la cifra va en BLANCO
   encima, así que el fondo tiene que sostener 4,5:1 — el coral y el
   naranja puros no llegan. */
.nv-sel{flex:none;display:inline-flex;align-items:center;justify-content:center;
  min-width:22px;height:22px;padding:0 6px;border-radius:999px;
  background:var(--niv-tx);color:#fff;font-size:13.5px;font-weight:800;
  font-variant-numeric:tabular-nums}
.nv-sel[hidden]{display:none}
.nv-caret{flex:none;width:16px;height:16px;color:#8A93A3;
  transition:transform var(--mo) var(--mo-ease)}
.nivgrp.abierto .nv-caret{transform:rotate(180deg)}
/* El techo alto es el mismo recurso de las tarjetas: se anima max-height
   porque `auto` no se puede interpolar, y medir el alto real no sirve aquí
   —cuando se abre, las tarjetas de dentro siguen enrolladas del filtro y
   la medida sale corta. 4000px alcanzaba hasta que «Elija su estilo»
   empezó a dibujar los 101 sueltos (el pozo, 16-ago): un flex column con
   techo reparte el alto entre los hijos y APLASTABA cada tarjeta a ~30px.
   De ahí los dos ajustes: techo 20000 y flex-shrink:0 en las tarjetas. */
/* La separación va en el MARGEN de cada tarjeta, no en un `gap`. Con `gap`
   el hueco se paga por hijo exista o no: una tarjeta enrollada por el filtro
   mide 0 de alto pero se sigue cobrando su hueco, así que filtrar dejaba
   franjas de tinte vacío entre lo poco que quedaba. El margen sí lo cancela
   `.combo.is-rolled{margin-bottom:0}`, que ya existía y el `gap` estaba
   dejando sin efecto. De paso deja de sumarse al margen propio de las filas
   de Estilo libre, que llevaban hueco y margen a la vez. */
.nivbody{display:flex;flex-direction:column;padding:12px 15px 15px;
  overflow:hidden;max-height:20000px}
.nivbody .combo{flex-shrink:0}
.nivbody.plegado{max-height:0;padding-top:0;padding-bottom:0}
@media(prefers-reduced-motion:no-preference){
  .nivbody{transition:max-height var(--mo) var(--mo-ease),
    padding-top var(--mo) var(--mo-ease),
    padding-bottom var(--mo) var(--mo-ease)}
}
.nivbody[hidden]{display:none}
.nivbody .combo{margin-bottom:11px}
.nivbody .combo:last-child{margin-bottom:0}
@media(min-width:768px){
  .nivgrp{border-radius:14px}
  .nivhead{padding:17px 20px}
  .nivgrp.abierto .nivhead{border-radius:14px 14px 0 0}
  .nv-dot{width:11px;height:11px}
  .nv-t{font-size:21px}
  .nv-caret{width:17px;height:17px}
  .nivbody{padding:14px 20px 18px}
  .nivbody .combo{margin-bottom:14px}
  .nivbody .combo:last-child{margin-bottom:0}
}
/* La cabecera del grupo abierto se pega DEBAJO de la barra verde. El offset
   NO se escribe aquí: el alto de esa barra cambia (el aviso entra y sale) y
   un número fijo dejaría la cabecera flotando sobre ella o con un hueco.
   portal.js mide la barra y publica --niv-stick; el valor de respaldo sólo
   cubre el instante anterior al primer pintado. */
/* UNA sola ranura, no una pila. Apilarlas fue el intento anterior y está
   mal: cada cabecera se clavaba en `base + índice x alto`, dando por hecho
   que las de arriba seguían clavadas — pero una cabecera sólo se pega
   mientras SU grupo está en pantalla, así que al irse la de arriba la de
   abajo se quedaba clavada en la ranura 1 con un hueco encima por el que
   asomaban las tarjetas.
   Ahora todas se clavan en el mismo sitio: la que va llegando releva a la
   que se va, que es lo que hace el que está pegado justo debajo de la barra
   sea siempre el nivel que se está mirando. Mismo z-index: en igualdad, el
   navegador pinta encima la que viene después en el documento, que es
   justamente la que llega.
   El -1px cierra la costura con la barra: las dos posiciones se calculan por
   separado y el redondeo dejaba un hilo de fondo entre ellas. */
.nivgrp.abierto .nivhead{position:sticky;
  top:calc(var(--niv-stick,150px) - 1px);z-index:4}

/* --- «Estilo libre» dentro del acordeón: atajo a categorías --- */
.lb-intro{font-size:14.5px;color:#5A6372;margin-bottom:10px}
.lb-cat{display:flex;align-items:center;gap:13px;min-height:48px;
  padding:14px 16px;margin-bottom:8px;border-radius:12px;border:1px solid #E8EAEF;
  background:#fff;text-decoration:none;cursor:pointer}
.lb-cat.is-empty{opacity:.5}
.lb-txt{flex:1;min-width:0}
.lb-txt b{display:block;font-size:17px;font-weight:800;color:#16233F;line-height:1.25}
.lb-txt small{display:block;font-size:14px;color:#8A93A3;line-height:1.35;margin-top:2px}
/* Lo elegido en esa categoria: la MISMA insignia que la de cada nivel, no
   una cifra gris. Misma pregunta, misma forma — asi no hay que aprender
   dos lenguajes en la misma pantalla. El [hidden] va aparte porque este
   display le gana al del navegador y dibujaria un «0». */
.lb-n,.ctk-sheet .sh-n{flex:none;display:inline-flex;align-items:center;
  justify-content:center;min-width:22px;height:22px;padding:0 6px;
  border-radius:999px;background:var(--niv-tx);color:#fff;font-size:13.5px;
  font-weight:800;font-variant-numeric:tabular-nums}
.lb-n[hidden],.ctk-sheet .sh-n[hidden]{display:none}
.lb-go{flex:none;color:#C3C9D4}
/* Azul de la filigrana del panel lateral (Manu): el naranja de acción se
   reserva para la barra del carrito, y aquí el botón pertenece al panel en
   vez de posarse encima. Más claro que el navy de la marca a propósito —
   con el navy compite con el propio panel lateral a 1440. */
.lb-cta{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;
  min-height:50px;border-radius:11px;background:#46538A;color:#fff;
  font-size:16px;font-weight:800;text-decoration:none;margin-top:4px}
.lb-cta:active{background:#3A497D}
.lb-cta-t{white-space:nowrap}

/* --- Screen B: «Estilo libre» como continuación de su grupo --- */
/* El marco es el TINTE del nivel, sin borde — la misma regla que el 3-ago
   retiró los marcos del acordeón: el color del nivel vive en el punto, el
   título y el tinte. Aquí se hereda de .ctk-niv--lib, que la plantilla pone
   en #catalogo, así que si mañana cambia el color del nivel cambia también
   esta pantalla: un solo sitio. */
.lb-frame{background:var(--niv-bg);border-radius:13px}
/* lb-back y ctk-cats: 48px de alto, no 40 — regla dura de área táctil
   mínima le gana al tamaño visual de la referencia. */
/* La fila del título ES la cabecera del grupo, así que se pega en la MISMA
   ranura que una cabecera abierta del acordeón (portal.js publica
   --niv-stick midiendo la barra verde, en las dos pantallas). El -1px cierra
   la costura con la barra, igual que en .nivhead. Lleva el tinte de fondo:
   sin él, las tarjetas se verían pasar por debajo. */
.lb-bar{display:flex;align-items:center;gap:10px;padding:15px;
  background:var(--niv-bg);border-radius:13px 13px 0 0;
  position:sticky;top:calc(var(--niv-stick,150px) - 1px);z-index:4}
/* Sin marco gris: contra el tinte, el blanco ya separa el control del fondo
   y el borde sólo devolvía el idioma que se acaba de retirar. */
.lb-back{display:inline-flex;align-items:center;gap:6px;flex:none;min-height:48px;
  padding:8px 12px;border-radius:999px;border:none;background:#fff;
  color:#16233F;font-size:14px;font-weight:800;text-decoration:none}
/* El cuerpo repite el ritmo de .nivbody al pie de la letra (relleno y hueco):
   es el mismo grupo, así que las tarjetas tienen que caer donde caerían si
   el nivel se hubiese desplegado en el acordeón. */
.lb-body{display:flex;flex-direction:column;padding:12px 15px 15px}
.lb-body .combo{margin-bottom:11px}
.lb-body .combo:last-child{margin-bottom:0}
/* Con DOS clases, para ganarle a `.lb-body .combo` y a `.nivbody .combo`:
   `.combo.is-rolled{margin-bottom:0}` pesa lo mismo que ellas y, por ir
   antes en el archivo, perdía — la tarjeta enrollada medía 0 de alto pero
   seguía separando. Esta regla es la que de verdad cierra el hueco, y vale
   también dentro de los saltos de ancho, que siguen pesando 0,2,0. */
.lb-body .combo.is-rolled,.nivbody .combo.is-rolled{margin-bottom:0}
@media(min-width:768px){
  .lb-frame{border-radius:14px}
  .lb-bar{padding:17px 20px;border-radius:14px 14px 0 0}
  .lb-body{padding:14px 20px 18px}
  .lb-body .combo{margin-bottom:14px}
  .lb-body .combo:last-child{margin-bottom:0}
}
/* El punto y el título de esta pantalla son el acento DEL NIVEL, igual que
   en el acordeón: salen de --niv-c / --niv-tx (ctk-niv--lib, que la
   plantilla pone en #catalogo) y no de un hex propio. Estaban clavados en
   bronce, así que el marco cambiaba de color con el nivel y estos dos no
   — ahora la promesa del comentario de .lb-frame («un solo sitio») es
   cierta para toda la pantalla. */
.lb-dot{flex:none;width:10px;height:10px;border-radius:50%;background:var(--niv-c)}
/* El tamaño CEDE antes que la fila. «Elija su estilo» es un 25% más largo
   que lo que había, y entre «Categorías» y «Tipos» el hueco es fijo: a
   tamaño fijo la única salida era partir en dos líneas o cortar con
   puntos suspensivos, y las dos se leen como un defecto. Con clamp, en un
   teléfono de 360 baja a ~16px y entra entero; de 768 en adelante recupera
   los 20px de siempre. El corte se queda como último seguro, no como
   comportamiento normal. */
.lb-title{flex:1;min-width:0;font-size:clamp(15px,4.4vw,20px);
  font-weight:800;letter-spacing:-.2px;
  color:var(--niv-tx);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* `font-family:inherit` y no el atajo `font:inherit`: el atajo va al final y
   borraría el tamaño y el peso de esta misma regla. */
/* Sin el embudo dentro. Era el MISMO glifo que el del saldo en la barra verde
   —misma forma, dos trabajos distintos, en la misma pantalla—: el del saldo va
   solo (regla sellada) y este lleva su palabra, asi que la palabra basta y el
   embudo vuelve a significar una sola cosa. De paso son ~24px menos de ancho,
   que es donde 360 apretaba.

   Apagado NO lleva contorno: sin nada elegido el control OFRECE, y un borde
   permanente lo hacia parecer ya puesto. Encendido si se rellena — ahi si
   informa, y ademas trae su cifra. La capsula (999px) y los 34/38px son los
   del resto de los controles de esta familia; iba a 10px de radio y 48px de
   alto, el unico rectangulo entre capsulas y el mas gordo de todos.

   El relleno se mantiene igual en los dos estados a proposito: cambiarlo con
   el estado movia la fila entera al marcar la primera categoria. */
.ctk-cats{position:relative;display:inline-flex;align-items:center;gap:7px;
  flex:none;height:34px;padding:0 12px;border-radius:999px;
  border:1.5px solid transparent;background:none;
  font-family:inherit;color:var(--niv-tx);font-size:15px;font-weight:800;cursor:pointer;
  transition:background .18s ease,color .18s ease,border-color .18s ease}
/* Los 48px del dedo se sacan del FLUJO, como ya hace el embudo del saldo: si
   los ocupara la caja, este boton volveria a fijar el alto de la fila. */
.ctk-cats::after{content:"";position:absolute;inset:-7px -6px}
.ctk-cats.on{background:var(--niv-tx);border-color:var(--niv-tx);color:#fff}
@media(min-width:992px){.ctk-cats{height:38px}}
/* 23px/13.5px, no 22/12.5 del token — regla dura de mínimo 13.5px le gana. */
.ctk-cats .fl-b{flex:none;display:inline-flex;align-items:center;justify-content:center;
  width:23px;height:23px;border-radius:50%;background:var(--accent);color:#fff;
  font-size:13.5px;font-weight:800}
/* Las fichas de categoría de Estilo libre visten el NIVEL, no un bronce
   propio: era la queja literal del cliente —«los colores de las
   categorías, los mismos del catálogo»—, así que salen del mismo bloque
   de paleta que todo lo demás (tinte de fondo, borde al 35%, texto en el
   tono de lectura: 4,82:1). Fuera de modo-libre .chip-cat sigue azul con
   su regla de siempre; esta no la toca. */
#catalogo.modo-libre .ctk-chips .chip-cat{background:var(--niv-bg);
  border:1px solid var(--niv-bd);color:var(--niv-tx)}

/* --- Hoja de filtros en bronce (Estilo libre reutiliza .ctk-sheet) --- */
.ctk-sheet.sh-bronce{background:#F7F2E9}
.ctk-sheet.sh-bronce .sh-head{background:#F1E7D6;border-bottom:1px solid #8A6A3E}
.ctk-sheet.sh-bronce .sh-grab{background:#CBB58E}
.ctk-sheet.sh-bronce .sh-title{font-size:16px;font-weight:800;color:#16233F;gap:8px}
/* .sh-funnel-ic se fue con el glifo del panel (2026-08-03). */
.ctk-sheet.sh-bronce .sh-clear{color:#8A6A3E;text-decoration:underline;
  font-size:14px;font-weight:800}
.ctk-sheet.sh-bronce .sh-foot{border-top-color:#E3D6BC}
.ctk-sheet.sh-bronce .sh-cat{background:#fff;border:1.5px solid #E8EAEF}
.ctk-sheet.sh-bronce .sh-cb-ui{border-color:#C9D2DC}
.ctk-sheet.sh-bronce .sh-cat.on{background:#F1E7D6;border-color:#8A6A3E}
.ctk-sheet.sh-bronce .sh-cat.on .sh-cb-ui{background:#8A6A3E;border-color:#8A6A3E}

/* --- Barra verde: aviso de "sólo lo que alcanza" + embudo mudo --- */
/* La barra CON aviso es un BLOQUE, no un flex ni una rejilla: sus dos hijos
   son renglones enteros. No basta con `flex-direction:column` — a 420 y menos
   la barra sencilla es `display:grid` (los dos renglones «Disponible» /
   «REF …» con el embudo al lado), así que el aviso se sentaba en una columna
   a la derecha y sacaba la página 394px de ancho en una pantalla de 356.
   Bloque aquí, y la fila de arriba se encarga de su propio reparto. */
/* flex:1 — la barra es flex, así que sin esto la fila se encoge a su
   contenido (181px de 360 medidos) y su space-between no reparte nada: el
   saldo y el embudo quedaban juntos a la izquierda. Lo sostenía la regla
   display:block de la variante con aviso, que se fue con el aviso. */
.sb-row{display:flex;justify-content:space-between;align-items:center;gap:12px;
  flex:1;min-width:0}
.sb-row .sb-right{display:flex;align-items:center;gap:10px}
/* El embudo del saldo va SUELTO sobre el verde: sin cápsula, sin recuadro,
   sin insignia. Hereda de .ctk-filtros el giro de relleno con .on y le quita
   todo lo demás — la cápsula navy es del control viejo, el que abría un panel.
   Contorno apagado, relleno verde encendido; el ícono se queda en su tamaño y
   sólo crece el cuadrado táctil a 48px. */
/* El cuadrado táctil de 48px se saca del FLUJO con un ::after, en vez de
   ocuparlo con la caja del botón. Ocupándolo, el embudo era el hijo más alto
   de la barra y le fijaba el alto: la del catálogo salía más alta que la de
   la pantalla de saldo, que no lo lleva. El dedo sigue teniendo 48; la barra
   ya no. Mismo recurso que usan las cápsulas de variante en este archivo. */
.ctk-filtros.sb-funnel{position:relative;width:24px;height:24px;padding:0;
  justify-content:center;gap:0;border:none;background:none;color:var(--green)}
.ctk-filtros.sb-funnel::after{content:"";position:absolute;inset:-12px}
@media(min-width:992px){.ctk-filtros.sb-funnel{height:24px}}
/* La lupa: misma cápsula pelada que el embudo, mismo cuadrado táctil de
   48px vía ::after fuera del flujo — sólo cambia el ícono adentro. */
.ctk-filtros.sb-buscar{position:relative;width:24px;height:24px;padding:0;
  justify-content:center;gap:0;border:none;background:none;color:var(--green)}
.ctk-filtros.sb-buscar::after{content:"";position:absolute;inset:-12px}
@media(min-width:992px){.ctk-filtros.sb-buscar{height:24px}}

/* --- Barra verde: el aviso que baja con el embudo --- */
/* La barra CON aviso es un BLOQUE, no un flex: sus dos hijos son renglones
   enteros. `has-aviso` la marca desde la plantilla y NO se le quita al apagar
   el embudo — cambiar de modo de caja a mitad del deslizamiento da un salto.
   Lo que entra y sale es el ALTO del aviso, nada más. `.sb-row` se sigue
   repartiendo solo: es flex y como hijo de bloque ya ocupa el ancho entero. */
.saldo-bar.has-aviso{display:block}
/* El deslizamiento va por max-height + opacidad, con la curva del glide entre
   pantallas para que descubrir el aviso y cambiar de pantalla se sientan del
   mismo material. max-height y no una altura fija: a 360 el texto se va a dos
   renglones y ninguna cifra fija los cubre. overflow:hidden es lo que hace que
   el renglón se DESCUBRA en vez de aparecer de golpe. */
.sb-aviso{display:flex;align-items:flex-start;gap:9px;overflow:hidden;
  max-height:0;opacity:0;margin-top:0;padding-top:0;
  border-top:1px solid transparent;line-height:1.25;
  font-size:var(--fs-min);font-weight:700;color:var(--green)}
.sb-aviso svg{flex:none;stroke:currentColor;margin-top:2px}
/* Dos renglones, un solo bloque deslizante: «Le quedan» lidera, la cuenta
   de siempre («N de M») va debajo. Columna, no dos <p> — así el mecanismo
   de .sb-aviso (max-height + opacidad) sigue siendo uno solo. */
.af-body{display:flex;flex-direction:column;gap:2px}
/* La cifra en cifras tabulares: cambia con cada toque del «+» y sin esto el
   renglón entero se movía de sitio a cada paso. El separador lo pone el CSS y
   sólo si hay cifra — así, si el índice no llegara, el aviso se queda con su
   frase en vez de abrir con un punto suelto. */
.af-n{font-variant-numeric:tabular-nums}
.af-n:not(:empty) + .af-t::before{content:" · "}
.af-quedan{font-variant-numeric:tabular-nums}
/* 140px, no 80px: el renglón nuevo puede llegar a dos líneas a 360px igual
   que el de siempre (comentario de arriba), y ahora los dos comparten
   techo. */
.saldo-bar.aviso-on .sb-aviso{max-height:140px;opacity:1;
  margin-top:10px;padding-top:10px;border-top-color:#CFE6D9}
/* El movimiento se enciende DESPUÉS del primer pintado (portal.js pone
   data-mo). Sin esto, llegar a la otra pantalla con el embudo ya encendido
   deslizaría el aviso otra vez en cada navegación, como si se acabara de
   encender: el estado se conserva, así que no se vuelve a anunciar. */
.saldo-bar[data-mo] .sb-aviso{
  transition:max-height var(--mo) var(--mo-ease),
             opacity calc(var(--mo) * .55) ease,
             margin-top var(--mo) var(--mo-ease),
             padding-top var(--mo) var(--mo-ease)}
@media(prefers-reduced-motion:reduce){
  .saldo-bar[data-mo] .sb-aviso{transition:opacity .15s ease}
}

/* --- Barra verde: el renglón de búsqueda que baja con la lupa --- */
/* Hermano de .sb-aviso, MISMO mecanismo (max-height + opacidad) y no se
   toca el del aviso: los dos pueden estar abiertos a la vez, cada uno con
   su propio alto. Sin cápsula ni ícono propio adentro — un input y nada
   más (sellado, sin microcopia). */
.sb-search{overflow:hidden;max-height:0;opacity:0;margin-top:0;padding-top:0;
  border-top:1px solid transparent}
.saldo-bar.search-on .sb-search{max-height:80px;opacity:1;
  margin-top:10px;padding-top:10px;border-top-color:#CFE6D9}
.saldo-bar[data-mo] .sb-search{
  transition:max-height var(--mo) var(--mo-ease),
             opacity calc(var(--mo) * .55) ease,
             margin-top var(--mo) var(--mo-ease),
             padding-top var(--mo) var(--mo-ease)}
@media(prefers-reduced-motion:reduce){
  .saldo-bar[data-mo] .sb-search{transition:opacity .15s ease}
}
.sb-search input{width:100%;min-height:38px;border:1.5px solid var(--green-bd);
  border-radius:8px;padding:0 12px;font-size:.95rem;font-weight:600;
  color:var(--ink);background:#fff;font-family:inherit}
.sb-search input:focus{outline:3px solid #CFE6D9;border-color:var(--green)}

/* La etiqueta de saldo por ficha («Te alcanza» / «Excede tu saldo») se retiró
   el 2026-08-03: producto por producto le recordaba al beneficiario lo que no
   alcanza a pagar. Quien dice ahora que algo no cabe es el «+» apagado. */

/* ===== Búsqueda: la tienda aplanada (Arbiter, 2026-08-16) =============
   Con una búsqueda activa las dos pantallas dejan de mostrar SECCIONES: es
   una sola lista con lo que coincide, tienda entera. portal.js pone
   `.buscando` en #catalogo — nunca mueve un nodo de sitio, sólo decide qué
   se enrolla (mismo `.is-rolled` de siempre). Aquí sólo se apaga el
   ROTULADO de sección: nombre, insignia, flecha, y en el acordeón el atajo
   de Estilo libre (que navega a otra pantalla, y aquí ya estamos viendo su
   contenido mezclado con el resto). El PUNTO de color de cada cabecera se
   queda — es la única marca de procedencia que pide el Arbiter, y ya
   existía; no hace falta una nueva. */
#catalogo.buscando .nivhead{pointer-events:none;cursor:default;min-height:0;
  padding:8px 15px}
#catalogo.buscando .nivhead .nv-t,
#catalogo.buscando .nivhead .nv-sel,
#catalogo.buscando .nivhead .nv-caret{display:none}
#catalogo.buscando .nivgrp{margin-bottom:2px}
/* Un nivel sin ni una coincidencia no deja ni el punto: sin nombre que lo
   explique, un punto suelto sin tarjetas debajo es ruido, no una marca de
   procedencia. `.is-empty` ya lo calcula portal.js en cada pintado. */
#catalogo.buscando .nivgrp.is-empty{display:none}
#catalogo.buscando .lb-intro,
#catalogo.buscando .lb-cat,
#catalogo.buscando .lb-cta{display:none}
/* Estilo libre (pantalla B): la búsqueda tapa el panel de categorías —
   mientras se aplana la tienda, «Tipos» no tiene nada que filtrar (no hay
   taxonomía) y las fichas de categoría activa dejarían de significar lo
   que dicen. */
#catalogo.modo-libre.buscando .ctk-cats,
#catalogo.modo-libre.buscando #filtros-chips,
#catalogo.modo-libre.buscando #filtros-count{display:none}
/* El pozo de combos: oculto por defecto (`hidden` en el HTML). `.buscando`
   es lo único que lo saca — un `display` explícito le gana al `[hidden]`
   del navegador (misma regla que ya usan `.ctk-sheet`/`.vpanel` en este
   archivo), así que basta con la clase; ninguna tarjeta de dentro se ve a
   menos que además coincida con la búsqueda (`.is-rolled` por tarjeta,
   igual que las propias de la pantalla). */
#pool{display:none}
#catalogo.modo-libre.buscando #pool{display:block}
