/* ===========================================================
   Perigami Polyhedra Unfolder
   Tema heredado de EquirecTime — "consola de nave"
   =========================================================== */
@font-face {
  font-family: 'Orbitron';
  src: url('../assets/fonts/Orbitron.ttf') format('truetype');
  font-weight: 400 900;
  font-display: swap;
}

:root {
  /* Paleta de marca (ver assets/ui/paleta de colores.svg): azules oscuros
     de fondo/estructura, naranjas para el acento interactivo — el mismo
     reparto de color que usa el logotipo (azul = wordmark, naranja =
     subtítulo). --amber pasa a un azul medio (antes naranja) para que el
     acento naranja quede reservado sólo a lo interactivo/activo. */
  --bg: #0f1826;
  --fg: #dce6f2;
  --muted: #7894c1;
  --cyan: #dc883e;
  --cyan-2: #c0732f;
  --amber: #5a83c2;
  --line: rgba(90, 131, 194, 0.18);
  --line-soft: rgba(90, 131, 194, 0.10);
  --field: rgba(90, 131, 194, 0.05);
  --panel: rgba(15, 24, 38, 0.82);
  --glow: 0 0 18px rgba(220, 136, 62, 0.3);
  --mono: ui-monospace, "Cascadia Code", "Segoe UI Mono", "SF Mono", Consolas, "Liberation Mono", monospace;
  --display: 'Orbitron', var(--mono);
  /* Mosaicos de los menús de carga (ver .carga-menu): hueco entre mosaicos y
     ancho mínimo de columna — la cantidad de columnas sale sola del ancho del
     menú (auto-fill), no hay que calcularla. */
  --solid-gap: 2px;
  --col-min: 76px;
  /* Posición del logo (esquina superior izquierda del visor): distancia al
     borde izquierdo (x) y al de arriba (y). En móvil, ver el @media de 640px
     de "Superpuesto al visor 3D". */
  --logo-x: 14px;
  --logo-y: 14px;
  /* 0 = panel de controles colapsado, 1 = abierto — animado en panels.js con
     easeInOutExpo al hacer clic en la flecha de su pestaña. El 1 de acá es
     sólo el estado antes de que corra el JS. */
  --viewer-open-t: 1;
  /* Color sólido compartido por el visor 3D y los dos paneles (lateral +
     inferior) — antes el visor era negro puro y los paneles un tinte
     translúcido distinto; ahora todos son la MISMA superficie plana, y del
     mismo #0f1826 que --bg (el fondo de la paleta de marca, ver assets/ui/
     paleta de colores.svg) — no un tinte aparte. */
  --surface: var(--bg);
  /* Ancho/alto de cada panel cuando está colapsado: ni 0 ni su tamaño
     completo — lo justo para que se siga viendo la muesca + flecha de su
     pestaña, nada del contenido de adentro (grilla de sólidos / toggles). */
  --panel-collapsed: 18px;
  /* Escala de toda la interfaz — ver html{font-size} más abajo. Tocar este
     único número agranda o achica texto e íconos a la vez. */
  --ui-scale: 1.15;
}
* { box-sizing: border-box; }

html { color-scheme: dark; }
html, body {
  margin: 0; height: 100%; width: 100%;
  background: var(--bg); color: var(--fg);
  font-family: var(--mono);
  -webkit-font-smoothing: antialiased;
  overscroll-behavior: none;
}
body { display: flex; flex-direction: column; overflow: hidden; }

/* Escala global de la interfaz.
   Antes esto era `body { zoom: 1.15 }`. `zoom` es cómodo pero no es CSS
   estándar de toda la vida (Firefox recién lo soporta desde la 126) y no
   respeta el tamaño de letra que el usuario haya configurado en su navegador
   —que es justo lo que hay que respetar cuando alguien necesita texto más
   grande—. Acá la escala sale de agrandar la raíz: 115% de LO QUE EL USUARIO
   TENGA puesto, no de un 16px fijo. Todos los font-size de esta hoja están en
   rem, así que el resultado visual es el mismo que daba el zoom (Npx × 1.15),
   pero además crece si el usuario sube el tamaño de letra del navegador. */
html { font-size: calc(100% * var(--ui-scale)); }

/* ─── Superpuesto al visor 3D ─────────────────────── */
/* Ya no hay cabecera: logo, vistas, carga y descarga flotan sobre el visor
   (.viewer-stage es position:relative). Escritorio: logo arriba a la
   izquierda, vistas abajo al centro, carga abajo a la izquierda y descarga
   abajo a la derecha, los dos grupos pegados a su borde lateral y al borde
   superior del panel de controles. Móvil: ver el @media de 640px al final
   de esta sección. Encima del lienzo y de la pestaña del panel (4), debajo
   de los menús de carga (12). */
.visor-logo {
  position: absolute; top: var(--logo-y); left: var(--logo-x); z-index: 8;
  margin: 0; display: flex; align-items: center;
}
.visor-vistas {
  position: absolute; bottom: 14px; left: 50%; transform: translateX(-50%); z-index: 8;
  display: flex; align-items: center; justify-content: center;
}
.visor-vistas .vt-views { flex-wrap: nowrap; }
/* Si entre la carga y la descarga no queda hueco, las vistas suben por
   encima de esa fila. */
@media (max-width: 900px) {
  .visor-vistas { bottom: 46px; }
  #viewerToast { bottom: 84px; }
}
/* gap:0 a propósito: el espacio entre botones lo pone el margin negativo de
   .output-tile + .output-tile (ver ahí el porqué), no esta propiedad — si
   los dos se suman, el hueco visible deja de ser el que se calculó. */
.visor-entrada, .visor-salida {
  position: absolute; bottom: 0; z-index: 8;
  display: flex; gap: 0;
}
.visor-entrada { left: 0; }
.carga-velo { display: none; }
.visor-salida { right: 0; }

/* El logo ES el botón "Acerca de". Reset total de apariencia de botón: sólo
   envuelve la imagen, sin bisel ni relleno propios. */
.btn-logo {
  position: relative;
  width: auto; padding: 0; flex: 0 0 auto;
  display: flex; align-items: center;
  background: transparent; clip-path: none;
  /* Los <button> traen su propio font-size chico por defecto en el user
     agent stylesheet — .brand-logo mide su alto en em (2.1em), así que sin
     esto heredaba ESE tamaño en vez del de .visor-logo y el logo se veía
     achicado. */
  font: inherit;
}
.btn-logo::before { display: none; }
/* El glow del hover genérico de button (ver más abajo) no va acá: el logo se
   presenta limpio, sin resplandor en ningún estado. Hace falta :not(:disabled)
   para ganarle en especificidad a esa regla. */
.btn-logo:hover:not(:disabled) { filter: none; }

/* El logo crece apenas al pasar el mouse y se hunde al pulsarlo. :active
   cubre también el toque en móvil, donde no hay hover. */
.brand-logo { height: 2.1em; width: auto; display: block; transition: transform .22s ease; }
.btn-logo:is(:hover, :focus-visible) .brand-logo { transform: scale(1.06); }
.btn-logo:active .brand-logo { transform: scale(.95); transition-duration: .08s; }

/* Selector de idioma: mismo interruptor pastilla+perilla que el resto de la
   app (.vt-sw), con "ES"/"EN" rotulados a los lados en vez de un texto que
   cambia — igual patrón que el toggle de 3 tiempos de wireframe (.vt-group
   --wire), pero de 2. Se resalta el lado activo, la perilla se corre. */
.vt-group--lang {
  /* Mismo gap que .vt-sw entre su track y su label (ver más abajo) — así el
     texto ES/EN queda a la misma distancia del interruptor que "Costuras",
     "Grilla" y el resto de los toggles con label adentro del botón. */
  flex: 0 0 auto; gap: 7px; padding: 3px 8px;
}
.vt-group--lang .vt-wire__lbl { font-size: 0.5625rem; font-weight: 700; }
.vt-group--lang[data-state="0"] .vt-wire__lbl:first-child,
.vt-group--lang[data-state="1"] .vt-wire__lbl:last-child {
  color: var(--cyan);
}
/* A diferencia del resto de los .vt-sw, este interruptor no es un on/off de
   verdad (siempre hay UN idioma elegido) — así que se ve "prendido" (pastilla
   y perilla iluminadas) en los DOS lados, sin importar cuál esté activo; sólo
   la POSICIÓN de la perilla (heredada de .is-on) indica cuál es. */
.lang-toggle .vt-sw__track {
  background: color-mix(in srgb, var(--cyan) 25%, var(--field));
  border-color: var(--cyan);
}
.lang-toggle .vt-sw__knob {
  background: var(--cyan);
  box-shadow: 0 0 7px var(--cyan);
}
/* Entrada: "Cargar imagen" y "Cargar sólido", la misma familia de trapecios
   que la salida (36 px de alto, 15° de inclinación, pegados con el huequito
   de ~3 px), rellenos y con texto oscuro. Los dos en el naranja del PDF:
   pesan lo mismo, no hay uno principal. El que tiene su menú abierto se
   enciende en el naranja vivo de acento, con resplandor. Imagen: base ancha
   arriba y el lado izquierdo recto, contra el borde del visor (como PNG,
   que va contra el derecho); Sólido: base ancha abajo. El relleno usa la
   regla base de button (--brd de fondo, ::before con --fill). */
.input-tile {
  --tile-h: 36px;
  --tile-slant: calc(var(--tile-h) * tan(15deg));
  --o: polygon(0 0, 100% 0, calc(100% - var(--tile-slant)) 100%, 0 100%);
  --i: var(--o);
  --brd: #c0732f; --fill: #c0732f;
  width: auto; height: var(--tile-h); flex: 0 0 auto;
  padding: 0 24px 0 16px; gap: 6px;
  color: #0f1826;
}
#btnCargarSolido {
  --o: polygon(var(--tile-slant) 0, calc(100% - var(--tile-slant)) 0, 100% 100%, 0 100%);
  margin-left: calc(3px - var(--tile-slant));
  padding: 0 22px;
}
.input-tile svg { width: 1.7em; height: 1.7em; flex: 0 0 auto; }
.input-tile span { font-size: 0.5625rem; letter-spacing: .8px; text-transform: uppercase; white-space: nowrap; }
/* Mismo hover que la salida: un poco más claro, sin cambiar de color. */
.input-tile:hover:not(:disabled) { --brd: #c0732f; --fill: #c0732f; color: #0f1826; filter: brightness(1.12); }
.input-tile.is-on, .input-tile.is-on:hover:not(:disabled),
#btnCargarSolido.is-on, #btnCargarSolido.is-on:hover:not(:disabled) {
  --brd: var(--cyan); --fill: var(--cyan); color: var(--bg);
  filter: drop-shadow(0 0 7px rgba(220, 136, 62, .45));
}

@media (max-width: 640px) {
  /* Carga: una fila a todo el ancho, pegada al borde de arriba, mitad y
     mitad. Imagen con el lado izquierdo recto y Sólido con el derecho, cada
     uno contra su borde. */
  .visor-entrada { top: 0; bottom: auto; left: 0; right: 0; }
  .visor-entrada .input-tile { flex: 1 1 0; min-width: 0; justify-content: center; padding: 0 16px 0 10px; }
  #btnCargarSolido { --o: polygon(var(--tile-slant) 0, 100% 0, 100% 100%, 0 100%); padding: 0 10px 0 16px; }
  /* El logo (perihedralogo_es/en.svg ya trae nombre y subtítulo), debajo de
     la carga y más chico. */
  /* Posición del logo en móvil: debajo de la fila de carga (36px). */
  :root { --logo-x: 10px; --logo-y: 48px; }
  .brand-logo { height: 1.8em; }
  /* Vistas en una sola línea casi sobre el panel. Con left:50% el ancho
     disponible era la mitad del visor y se partían en dos: acá van de borde
     a borde, centradas. 7 × 38 px + huecos = 290 px, caben desde 320. */
  .visor-vistas { bottom: 6px; left: 0; right: 0; transform: none; }

  /* 3. Con un menú de carga abierto, el resto se oscurece y desenfoca. Un
     elemento de verdad y no un ::after fijo a pantalla completa: ésos los
     apagan algunos bloqueadores. Encima de todo menos el menú (12) y sus
     botones; la barra de descarga baja para quedar debajo del velo. */
  .carga-velo {
    display: block; position: fixed; inset: 0; z-index: 11;
    background: rgba(4, 9, 17, .55);
    -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
    opacity: 0; pointer-events: none; transition: opacity .32s ease;
  }
  body:has(.carga-menu.is-abierto) .carga-velo { opacity: 1; }
  body:has(.carga-menu.is-abierto) .visor-entrada { z-index: 13; }
  body:has(.carga-menu.is-abierto) .visor-salida { z-index: 10; }

  /* PDF/OBJ/PNG en una barra fija al pie del viewport, a todo el ancho y
     sin márgenes: un tercio cada uno, PDF con el lado izquierdo recto (PNG
     ya lo trae a la derecha). Alto = los 36px del botón (--tile-h); <main>
     reserva ese mismo hueco con padding-bottom para que el visor no quede
     tapado. */
  .visor-salida {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 15;
    padding: 0 0 env(safe-area-inset-bottom, 0px);
    background: var(--surface);
  }
  .visor-salida .output-tile { flex: 1 1 0; min-width: 0; justify-content: center; padding: 0 10px; }
  #btnNet { clip-path: polygon(0 0, calc(100% - var(--tile-slant)) 0, 100% 100%, 0 100%); }
  main { padding-bottom: calc(36px + env(safe-area-inset-bottom, 0px)); }
  /* La pestaña del panel de controles sube por encima de la barra fija de
     salida: si no, la barra tapa justo la muesca con la que se abre. */
  .panel-tab--h { bottom: 6px; }
}

/* Contenido del menú "Cargar imagen": mosaico, progreso y calcomanía. */
.panel-image { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.panel-image .progress { width: 100%; }

/* Calcomanía: botón y, cuando hay una, sus dos sliders y acciones. */
.decal { display: flex; flex-direction: column; gap: 6px; margin-top: 4px; }
.decal__controls { display: flex; flex-direction: column; gap: 6px; }
.decal__controls[hidden] { display: none; }
.decal-sliders .vt-slider { width: 100%; }
#imageGrid.sliders-al-lado .decal-sliders { gap: 2px; }
/* Situar / Quitar, superpuestos en la parte de abajo del mosaico "Calcomanía"
   (imageGrid.js los lleva a su celda) mientras haya una puesta. Apilados y
   alternando trapecios como PDF y OBJ en la cabecera: Situar con la base ancha
   abajo y el naranja del PDF, Quitar con la base ancha arriba y el marrón del
   OBJ — las dos bases anchas se encuentran en el medio. Fondo opaco: van
   encima del ícono del mosaico. */
/* Tamaño y giro de la calcomanía, como una celda más del mosaico: por defecto
   una fila entera justo debajo de la primera; con columnas de sobra
   (.sliders-al-lado, la pone imageGrid.js), en la primera fila al lado de
   "Calcomanía", en 2 columnas que se le quitan a "Imagen local". */
#imageGrid .decal-sliders { grid-column: 1 / -1; padding: 2px 4px 4px; }
#imageGrid .decal-sliders[hidden] { display: none; }
#imageGrid.sliders-al-lado .image-tile--upload { grid-column: 1 / -5; }
#imageGrid.sliders-al-lado .decal-sliders {
  grid-column: -5 / -3; grid-row: 1;
  justify-content: center; padding: 0 6px;
}
.image-tile-celda { position: relative; display: flex; min-width: 0; }
.image-tile-celda > .image-tile { flex: 1 1 auto; min-width: 0; }
.decal-acciones {
  position: absolute; left: 3px; right: 3px; bottom: 3px; z-index: 1;
  display: flex; flex-direction: column; gap: 2px;
}
.decal-acciones[hidden] { display: none; }
.decal-acciones .vt-btn {
  --tile-h: 20px;
  width: 100%; padding: 0 4px; justify-content: center;
  letter-spacing: .5px; color: #0f1826;
}
#btnDecalPlace {
  --o: polygon(var(--tile-slant) 0, calc(100% - var(--tile-slant)) 0, 100% 100%, 0 100%);
  --i: var(--o); --brd: #c0732f; --fill: #c0732f;
}
#btnDecalRemove {
  --o: polygon(0 0, 100% 0, calc(100% - var(--tile-slant)) 100%, var(--tile-slant) 100%);
  --i: var(--o); --brd: #884c18; --fill: #884c18;
}
#btnDecalPlace:hover:not(:disabled) { --brd: #c0732f; --fill: #c0732f; color: #0f1826; filter: brightness(1.12); }
#btnDecalRemove:hover:not(:disabled) { --brd: #884c18; --fill: #884c18; color: #0f1826; filter: brightness(1.12); }
/* "Situar" armado: el naranja vivo de acento y su resplandor, como .vt-btn.is-on. */
#btnDecalPlace.is-on, #btnDecalPlace.is-on:hover:not(:disabled) {
  --brd: var(--cyan); --fill: var(--cyan); color: var(--bg); box-shadow: none;
  filter: drop-shadow(0 0 6px rgba(220, 136, 62, .6));
}
.panel-image .progress-track { min-width: 36px; }

/* ─── Main layout ────────────────────────────────── */
main {
  display: flex; flex: 1 1 auto; min-height: 0;
  position: relative;   /* referencia de los menús de carga, absolutos */
}

/* ─── Menús de carga (ver js/menusCarga.js) ─────────── */
/* En escritorio salen hacia arriba desde los botones de carga (abajo a la
   izquierda del visor): el recorte (clip-path) crece desde el borde de abajo,
   como si el menú se desenrollara desde los botones, y se repliega igual al
   cerrar. Los -60px de los costados dejan ver la sombra. Encima del visor y
   de lo superpuesto, debajo de la ficha de un sólido (15) y los modales. */
.carga-menu {
  position: absolute; left: 0; bottom: 36px; z-index: 12;
  width: min(560px, calc(100vw - 16px));
  max-height: calc(100% - 36px - 10px);
  display: flex; flex-direction: column;
  background: var(--surface);
  border: 1px solid #884c18; border-left: 0;
  clip-path: inset(100% -60px 0 -60px);
  opacity: 0;
  transition: clip-path .32s cubic-bezier(.4, 0, .2, 1), opacity .22s ease;
}
.carga-menu.is-abierto { clip-path: inset(-60px -60px -60px -60px); opacity: 1; }
/* Escritorio: las imágenes, en tres columnas, con miniaturas más grandes (el
   menú crece hacia arriba lo que haga falta). */
@media (min-width: 641px) {
  #menuImagen .solid-grid, #menuImagen .solid-grid__tiles { grid-template-columns: repeat(3, 1fr); }
  /* Último recurso contra el scroll (ver ajustarMosaicos): la grilla entera
     más angosta, así las miniaturas se achican sin perder proporción. */
  #menuImagen .solid-grid { width: calc(100% * var(--icono-esc, 1)); margin-inline: auto; }
}
.carga-menu[hidden] { display: none; }
.carga-menu__cuerpo {
  overflow-y: auto; overscroll-behavior: contain;
  padding: max(2px, calc(10px * var(--aire, 1))) 12px max(3px, calc(12px * var(--aire, 1))) 10px;
  scrollbar-width: thin; scrollbar-color: #884c18 transparent;
}
.carga-menu__cuerpo::-webkit-scrollbar { width: 6px; }
.carga-menu__cuerpo::-webkit-scrollbar-track { background: transparent; }
.carga-menu__cuerpo::-webkit-scrollbar-thumb { background: #884c18; border-radius: 3px; }
/* En móvil la carga está arriba: el menú baja desde ella, a todo el ancho. */
@media (max-width: 640px) {
  .carga-menu {
    /* fixed y no absolute: así se sale del recorte de #viewer (overflow:
       hidden) y puede bajar hasta el borde inferior de la ventana, tapando
       el panel de controles y la barra de descarga. Por encima de esa barra
       (15); el velo (11) queda debajo. Sin el +safe-area, la barra de gestos
       del celular no tapa la última fila. */
    position: fixed; z-index: 16;
    top: 36px; bottom: auto; left: 0; width: 100%; border-left: 0; border-right: 0;
    max-height: calc(100vh - 36px - env(safe-area-inset-bottom, 0px));
    max-height: calc(100dvh - 36px - env(safe-area-inset-bottom, 0px));
    /* Siempre a toda altura: lo que sobre lo reparte ajustarMosaicos
       (menusCarga.js) en mosaicos más grandes y más aire entre filas y grupos. */
    height: calc(100vh - 36px - env(safe-area-inset-bottom, 0px));
    height: calc(100dvh - 36px - env(safe-area-inset-bottom, 0px));
    clip-path: inset(0 -60px 100% -60px);
  }
  .carga-menu.is-abierto { clip-path: inset(0 -60px -60px -60px); }
  .carga-menu__cuerpo { flex: 1 1 auto; min-height: 0; }
  .carga-menu { --solid-gap: 6px; }
}

/* ─── Pestaña de abrir/cerrar del panel de controles ──── */
/* Una muesca cortada en la línea divisoria, con una flecha adentro — en vez
   del botón rectangular de antes. El "corte" es la propia línea de contorno
   (.panel-tab__line, stroke var(--line)) desviándose hacia adentro del panel
   y volviendo; el fondo sólido (--surface, el mismo color del visor Y de los
   paneles, ver :root) tapa lo que haya detrás para que se vea como un hueco
   de verdad, no un ícono flotando encima. */
/* Sin transition de bottom: la posición depende de una variable que ya viene
   animada cuadro a cuadro desde JS (--viewer-open-t en panels.js) — una
   transition acá encima sólo metería un segundo suavizado atrás del primero,
   y el resultado se ve con vibración/retraso en vez de un movimiento parejo. */
/* El fondo opaco y el recorte de la muesca viven en ::before, NO en el propio
   botón: clip-path recorta también el área sensible al puntero, así que con el
   recorte sobre el botón el blanco de clic real serían los ~13×50 px visibles
   y no los 24×72 de la caja. Separándolos, la caja se puede agrandar sin
   deformar ni correr el dibujo — ver --tab-hit-* más abajo. */
.panel-tab {
  position: absolute; z-index: 6; padding: 0;
  background: transparent;
  clip-path: none;
  width: var(--tab-hit-w, var(--tab-w));
  height: var(--tab-hit-h, var(--tab-h));
}
.panel-tab::before {
  content: '';
  position: absolute; z-index: -1;
  inset: auto; top: 50%; left: 50%;
  width: var(--tab-w); height: var(--tab-h);
  transform: translate(-50%, -50%);
  background: var(--surface);
  clip-path: var(--tab-clip);
}
.panel-tab__line { stroke: #884c18; }
/* Se espeja la FLECHA con scale(±1) (ver --chev-sx/--chev-sy más abajo), no el
   <svg> entero: la muesca siempre entra hacia el panel, y espejar todo la
   sacaría fuera del recorte dejándola apuntando al visor. */
.panel-tab__chevron {
  stroke: #884c18; stroke-width: 2;
  transition: stroke .15s, transform .1s;
  transform-box: fill-box; transform-origin: center;
  transform: scale(calc(var(--chev-sx, 1) * .78), calc(var(--chev-sy, 1) * .78));
}
.panel-tab:hover .panel-tab__chevron { stroke: #fff; }
/* El botón base anima :hover con un glow y :active con translateY(1px): el
   glow no pega con esta pestaña chica, y el translateY pisaría el transform
   que la centra sobre el borde. Acá no se mueve ni brilla — sólo la flecha
   cambia de color y se achica al presionar. El :not(:disabled) iguala la
   especificidad de las reglas base de button, que si no ganarían. */
.panel-tab:hover:not(:disabled), .panel-tab:active:not(:disabled) { transform: var(--tab-pos); filter: none; }
.panel-tab:active:not(:disabled) .panel-tab__chevron {
  stroke: #fff;
  transform: scale(calc(var(--chev-sx, 1) * .78 * .82), calc(var(--chev-sy, 1) * .78 * .82));
}

/* Horizontal (panel inferior): centrada en ancho, ANCLADA al borde inferior
   de .viewer-stage (no del panel: como .viewer-stage es flex:1 1 auto, su
   borde inferior YA está siempre justo en el límite con .viewer-panel, sea
   cual sea la altura de éste — no hace falta saber esa altura a mano). Mismo
   criterio de clip-path que la vertical, girado 90°. */
.panel-tab--h {
  --tab-w: 72px; --tab-h: 24px;
  /* Acá el recorte va ARRIBA (no abajo como en la vertical): la muesca entra
     hacia el panel, que está debajo — lo que hay que dejar sin tapar es el
     visor 3D, que está encima. */
  --tab-clip: inset(11px 11px 0 11px);
  --tab-pos: translate(-50%, 50%);
  /* z-index menor que los menús de carga (12): con uno abierto, la pestaña
     queda debajo y no flota sobre los mosaicos. */
  bottom: 0; left: 50%; transform: var(--tab-pos); z-index: 4;
}
#viewer[data-panel-open="false"] .panel-tab--h { --chev-sy: -1; }

/* Con el dedo, la muesca visible (13px de ancho la vertical, 13 de alto la
   horizontal) es imposible de acertar. Se agranda SÓLO la caja del botón en
   la dimensión angosta de cada una — la otra ya pasa de sobra — hasta los
   44px que recomiendan las guías táctiles. El dibujo no se mueve ni cambia
   de tamaño: vive en ::before, centrado y con su propio tamaño fijo.
   (pointer: coarse) y no un ancho de pantalla: el problema es el dedo contra
   el mouse, no cuántos píxeles mide el viewport — así también entra una
   tablet ancha con pantalla táctil. */
@media (pointer: coarse) {
  .panel-tab--h { --tab-hit-h: 44px; }
}

/* Salida (PDF / OBJ / PNG), abajo a la derecha del visor: ícono + texto en línea, compacto.
   Fondo naranja sólido (no el tinte translúcido de otros botones): PDF es
   el de mayor jerarquía (#c0732f, el naranja más vivo de la paleta), OBJ/PNG
   van en el marrón (#884c18) — los dos con texto oscuro (#0f1826) encima.

   Trapezoidales, alternando el sentido: PDF con la base ancha abajo, OBJ con
   la base ancha arriba, PNG otra vez ancha abajo pero con el lado derecho
   recto (no en punta, para no repetir tres veces la misma silueta seguida).
   --tile-slant es la altura del botón por tan(15°) — el lado inclinado
   queda a 15° de la vertical, casi recto. La altura se fija (no la decide
   el contenido) para que esa cuenta sea exacta, no una aproximación. */
.output-tile {
  flex: 0 0 auto; width: auto;
  flex-direction: row; align-items: center; gap: 6px;
  --tile-h: 36px;
  --tile-slant: calc(var(--tile-h) * tan(15deg));
  height: var(--tile-h);
  padding: 0 26px;
  background: #884c18; border: 1px solid #884c18;
  /* Azul más oscuro de la paleta, el mismo en los tres botones de salida. */
  color: #0f1826;
  transition: color .15s, border-color .15s, background .15s;
  clip-path: polygon(
    var(--tile-slant) 0, calc(100% - var(--tile-slant)) 0,
    100% 100%, 0 100%
  );
}
.output-tile::before { content: none; }
/* Junto al de al lado en vez de con el gap normal (6px): como el lado
   inclinado angosta el botón, ese mismo gap dejaba un hueco de gap+slant en
   V entre dos botones vecinos — mucho más de lo que se ve en la línea
   recta. El margen negativo los acerca hasta un huequito de ~3px parejo en
   toda la altura (gap real = margin + slant, así que margin = 3px - slant). */
.output-tile + .output-tile { margin-left: calc(3px - var(--tile-slant)); }
/* OBJ: mismo trapecio que PDF pero espejado en Y — base ancha ARRIBA. */
#btnOBJ {
  clip-path: polygon(
    0 0, 100% 0,
    calc(100% - var(--tile-slant)) 100%, var(--tile-slant) 100%
  );
}
/* PNG: base ancha abajo otra vez (como PDF), pero el lado derecho queda
   recto — sin inclinación — así no se repite la misma silueta tres veces. */
#btnPNG {
  clip-path: polygon(var(--tile-slant) 0, 100% 0, 100% 100%, 0 100%);
  padding-right: 18px;   /* del lado recto no hace falta tanto aire */
}
.output-tile span {
  font-size: 0.5625rem; letter-spacing: .8px; text-transform: uppercase; white-space: nowrap;
}
.output-tile:hover:not(:disabled) {
  filter: brightness(1.12); border-color: #884c18;
  background: #884c18; color: #0f1826;
}
/* PDF es el de mayor jerarquía: mismo rótulo oscuro, sobre el naranja vivo. */
.output-tile.primary {
  border-color: #c0732f; color: #0f1826;
  background: #c0732f;
}
.output-tile.primary:hover:not(:disabled) {
  background: #c0732f; filter: brightness(1.12) drop-shadow(0 0 7px rgba(192,115,47,.45));
}
.output-tile:disabled { opacity: .35; }

/* ─── Botones: contorno de dos capas del sistema ───── */
/* La forma por defecto es el trapecio de la familia de los botones de la app
   (lado izquierdo inclinado, derecho recto, como Descargar PDF), no el bisel de
   esquinas cortadas de antes: así un botón nuevo sin forma propia ya nace del
   mismo lenguaje. La mayoría la pisa con su propio clip-path. */
button {
  --o: polygon(9px 0, 100% 0, 100% 100%, 0 100%);
  --i: polygon(8.4px 0, 100% 0, 100% 100%, 0 100%);   /* la del ::before, que ya va 1px adentro */
  --fill: var(--field);
  width: 100%; padding: 8px 10px;
  background: var(--brd, var(--line)); clip-path: var(--o);
  color: var(--fg); font-family: var(--mono);
  font-size: 0.65625rem; letter-spacing: 1.4px; text-transform: uppercase;
  cursor: pointer; transition: color .15s, filter .15s;
  display: flex; align-items: center; justify-content: center; gap: 7px;
}
button:hover:not(:disabled) {
  --brd: var(--cyan); --fill: rgba(220,136,62,0.14); color: var(--bg);
  filter: drop-shadow(0 0 7px rgba(220,136,62,.4));
}
button:active:not(:disabled) { transform: translateY(1px); }
button:disabled { opacity: .35; cursor: not-allowed; }

/* Primario: relleno cyan sólido, texto oscuro */
button.primary { --brd: var(--cyan); --fill: var(--cyan); color: var(--bg); }
button.primary:hover:not(:disabled) {
  --fill: var(--cyan); color: var(--bg);
  filter: brightness(1.12) drop-shadow(0 0 10px rgba(220,136,62,.55));
}

/* Borde uniforme en las esquinas cortadas: capa de relleno 1px hacia dentro */
button { position: relative; border: none !important; isolation: isolate; }
button::before {
  content: ""; position: absolute; inset: 1px; z-index: -1;
  clip-path: var(--i); background: var(--fill);
}

/* ─── Campos ─────────────────────────────────────── */
select {
  width: 100%; padding: 6px 9px;
  border: 1px solid var(--line); background: var(--field); color: var(--fg);
  font-family: var(--mono); font-size: 0.6875rem; letter-spacing: .5px;
  outline: none; appearance: none; -webkit-appearance: none; border-radius: 0;
  transition: border-color .15s, box-shadow .15s;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'><path fill='%23dc883e' d='M6 8 0 0h12z'/></svg>");
  background-repeat: no-repeat; background-position: right 10px center;
  padding-right: 26px; cursor: pointer;
}
select:focus { border-color: var(--cyan); box-shadow: var(--glow); }
select:disabled { opacity: .38; cursor: not-allowed; }
option { background: #0c1524; color: var(--fg); }

/* ─── Mosaico de sólidos (reemplaza el <select>) ──── */
/* Columnas solas, por ancho: tantas como quepan de al menos --col-min
   (auto-fill), así el mismo mosaico sirve en el menú de escritorio y en el de
   móvil. Letra e ícono quedan fijos (ver .solid-tile__name/svg más abajo): un
   menú más ancho da más columnas, no mosaicos más grandes. Mismo mecanismo
   para #imageGrid, que comparte .solid-grid. La etiqueta de grupo ocupa la
   fila completa, así el grupo siguiente arranca alineado con los de arriba. */
.solid-grid { display: grid; width: 100%; grid-template-columns: repeat(auto-fill, minmax(var(--col-min), 1fr)); gap: var(--fila-gap, calc(var(--solid-gap) * var(--aire, 1))) var(--solid-gap); align-items: stretch; }
.solid-grid__glabel {
  grid-column: 1 / -1;
  display: flex; align-items: center; gap: 6px;
  font-size: 0.5625rem; font-weight: 700; letter-spacing: 1px;
  text-transform: uppercase; color: var(--cyan-2);
  margin-top: var(--grupo-gap, calc(2px * var(--aire, 1)));
}
.solid-grid__glabel:first-child { margin-top: 0; }
.solid-grid__glabel::after { content: ""; flex: 1; height: 0; border-top: 1px dashed var(--line); }
/* En el mosaico de sólidos el rótulo de grupo es un botón que pliega el grupo
   (ver "Grupos plegables" en solidGrid.js): se le quita todo lo de botón y
   queda igual al rótulo de siempre, más una flecha y el conteo. */
button.solid-grid__glabel {
  width: 100%; padding: calc(2px * var(--aire, 1)) 0; justify-content: flex-start;
  background: transparent; clip-path: none; cursor: pointer;
  font-size: 0.5625rem; letter-spacing: 1px; color: var(--cyan-2);
}
button.solid-grid__glabel::before { content: none; }
button.solid-grid__glabel:hover:not(:disabled) { color: var(--cyan); filter: none; }
button.solid-grid__glabel:active:not(:disabled) { transform: none; }
.solid-grid__chev {
  width: 8px; height: 8px; flex: 0 0 auto;
  fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round;
  transition: transform .2s ease;
}
.solid-grid__glabel[aria-expanded="true"] .solid-grid__chev { transform: rotate(90deg); }
.solid-grid__count { color: var(--muted); font-weight: 400; }
/* Cuerpo de un grupo en acordeón (ver acordeon.js): una grilla de una sola
   fila que pasa de 0fr a 1fr, la forma de animar el alto de algo que no se
   conoce de antemano. Adentro, la misma grilla de columnas del mosaico. El
   relleno de 3 px (compensado con margen negativo) deja ver entero el
   contorno de foco y el "pop" de un mosaico, que overflow:hidden recortaría. */
.solid-grid__cuerpo {
  grid-column: 1 / -1;
  display: grid; grid-template-rows: 0fr;
  opacity: 0;
  transition: grid-template-rows .32s cubic-bezier(.4, 0, .2, 1), opacity .22s ease;
}
.solid-grid__cuerpo.is-abierto { grid-template-rows: 1fr; opacity: 1; }
.solid-grid__tiles {
  min-height: 0; overflow: hidden;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--col-min), 1fr));
  gap: var(--fila-gap, calc(var(--solid-gap) * var(--aire, 1))) var(--solid-gap); align-items: stretch;
  padding: 3px; margin: -3px;
}

/* Ficha de un sólido al pasar el mouse (ver _mostrarFicha en solidGrid.js):
   flota junto al mosaico, por encima de los paneles y por debajo de los
   modales. No recibe el puntero: nunca tapa el sólido de al lado. */
.solid-info {
  position: fixed; z-index: 15; width: max-content; max-width: 240px;
  padding: 9px 12px 8px;
  background: rgba(4, 9, 17, 0.95); border: 1px solid var(--cyan);
  pointer-events: none;
  font-size: 0.5625rem; line-height: 1.5; color: var(--muted);
  animation: toast-entra-fijo .15s ease-out both;
}
.solid-info[hidden] { display: none; }
/* Al irse se desvanece (ver _ocultarFicha en solidGrid.js). */
.solid-info.is-saliendo { opacity: 0; transition: opacity .15s ease; }
.solid-info__nombre {
  display: block; font-family: var(--display); font-size: 0.6875rem; font-weight: 700;
  letter-spacing: .5px; color: var(--fg);
}
.solid-info__grupo {
  display: block; margin-bottom: 6px;
  text-transform: uppercase; letter-spacing: 1px; color: var(--cyan-2);
}
.solid-info dl {
  display: grid; grid-template-columns: auto auto; justify-content: start;
  gap: 1px 12px; margin: 0 0 5px;
}
.solid-info dd { margin: 0; color: var(--fg); text-align: right; font-variant-numeric: tabular-nums; }
.solid-info p { margin: 0 0 5px; }
.solid-info__tipos { color: var(--fg); }
.solid-info__nivel { margin: 0 !important; padding-top: 5px; border-top: 1px dashed var(--line); }
.solid-info__nivel b { color: var(--cyan); font-weight: 400; letter-spacing: 1px; }
.solid-tile {
  display: flex; flex-direction: column; align-items: center; gap: calc(2px * var(--aire, 1));
  width: auto; min-width: 0; padding: calc(2px * var(--aire, 1)) 2px calc(3px * var(--aire, 1)); clip-path: none;
  background: transparent; border: 1px solid var(--line);
  color: var(--muted);
  transition: color .15s, border-color .15s, background .15s, transform .18s ease, box-shadow .18s ease;
}
.solid-tile::before { content: none; }
/* Tamaño fijo (44 px: con los grupos en acordeón sobra alto para íconos más
   grandes), sea cual sea el ancho de columna: una columna más ancha da más
   aire alrededor, no un ícono más grande. Lo que cambia con el ancho es
   cuántas palabras entran por línea del nombre, no el cuerpo de la letra. */
.solid-tile svg { width: 100%; height: auto; max-width: calc(44px * var(--icono-esc, 1)); display: block; }
/* Los íconos de "Cargar imagen" y "Ninguna" (mismo .solid-tile) siguen en su tamaño. */
.image-tile svg { max-width: calc(30px * var(--icono-esc, 1)); }
/* Mismo hueco que ocupará el <svg> una vez resuelto (ver _lazyIcons en
   solidGrid.js): sin esto, el ícono "aparecería" empujando el nombre hacia
   abajo apenas se resuelve, en vez de ya tener su lugar reservado. */
.solid-tile__icon-slot { display: block; width: 100%; max-width: calc(44px * var(--icono-esc, 1)); aspect-ratio: 1; margin: 0 auto; }
.solid-tile__name {
  width: 100%; min-width: 0; font-size: 0.5625rem; line-height: 1.08; text-align: center;
  text-transform: none; letter-spacing: 0;
  /* Dos líneas en vez de una sola con "…": el nombre completo entra en la
     mayoría de los casos, y cuando no, el corte a las 2 líneas (line-clamp)
     se ve mejor que perder la mitad de la palabra a mitad de línea. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; white-space: normal; word-break: break-word;
  min-height: calc(1.08em * 2);
  color: var(--fg);
}
.solid-tile:hover:not(:disabled) {
  color: var(--fg); filter: none; border-color: var(--cyan);
  background: color-mix(in srgb, var(--cyan) 8%, var(--field));
  box-shadow: 0 4px 10px rgba(220, 136, 62, .2);
}
.solid-tile:active { transform: none; }
.solid-tile.is-active {
  border-color: var(--cyan);
  background: color-mix(in srgb, var(--cyan) 16%, var(--field));
  box-shadow: 0 0 0 1px var(--cyan);
}
.solid-tile.is-active .solid-tile__name { color: var(--cyan); }
.solid-tile:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }

/* ─── Mosaico de imágenes base (mismo tile que los sólidos) ──── */
/* .image-tile hereda tamaño y estados de .solid-tile; acá sólo lo que cambia:
   una miniatura apaisada en vez de un SVG calculado, y un ícono de carga. */
.image-tile__thumb {
  width: 100%; aspect-ratio: 2.5 / 1; overflow: hidden;
  border-radius: 2px; background: transparent;
  display: flex; align-items: center; justify-content: center;
}
.image-tile__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.image-tile__thumb--upload, .image-tile__thumb--none { color: var(--muted); }
.image-tile:hover .image-tile__thumb--upload,
.image-tile:hover .image-tile__thumb--none { color: var(--cyan); }
/* Con 3 columnas "Imagen local" ya no tiene de dónde ensancharse (1 / -3 es
   una sola celda): igual queda primera, a la izquierda. */
.image-tile.is-active .image-tile__thumb--upload,
.image-tile.is-active .image-tile__thumb--none { color: var(--cyan); }
/* "Imagen local" ensanchado: de la primera columna hasta la antepenúltima,
   dejando las dos últimas para "Calcomanía" y "Ninguna". Funciona con
   cualquier cantidad de columnas porque -3 siempre apunta a la antepenúltima. */
.image-tile--upload { grid-column: 1 / -3; justify-content: center; }
/* Ícono chico y fijo, no estirado por el ancho de VARIAS columnas: el
   thumb de una miniatura real usa aspect-ratio 2/1 sobre el 100% de su
   ancho a propósito, pero acá ese mismo ancho es el de toda la fila menos
   "Ninguna" — heredarlo dejaría esta fila mucho más alta que las demás. */
.image-tile__thumb--upload { width: 36px; height: 36px; aspect-ratio: auto; flex: 0 0 auto; }
.image-tile--upload.is-dragover {
  border-color: var(--cyan);
  background: color-mix(in srgb, var(--cyan) 12%, var(--field));
}

/* ─── Visor 3D ───────────────────────────────────── */
#viewer {
  flex: 1 1 auto; position: relative; min-height: 260px;
  background: var(--surface);
  display: flex; flex-direction: column;
  overflow: hidden;
}
@media (min-width: 820px) { #viewer { min-height: 0; } }

/* Sólido 3D + hint: la parte de #viewer que se encoge para dejarle sitio fijo
   al panel de controles de abajo. */
.viewer-stage {
  flex: 1 1 auto; min-height: 120px; position: relative;
  background: var(--surface);
  display: flex; align-items: center; justify-content: center;
}
#threeCanvas { width: 100%; height: 100%; display: block; cursor: grab; }
#threeCanvas:active { cursor: grabbing; }
#viewerHint {
  position: absolute; margin: 0;
  color: var(--muted); font-size: 0.625rem;
  letter-spacing: 1.5px; text-transform: uppercase;
  pointer-events: none; max-width: 80%; text-align: center; line-height: 1.7;
  text-shadow: 0 1px 4px rgba(0,0,0,.9);
}
/* Soltar una imagen: marco punteado sobre todo el visor mientras se arrastra
   un archivo por la ventana (ver "Soltar o pegar una imagen" en app.js). */
.drop-hint {
  position: absolute; inset: 12px; z-index: 7;
  display: none; align-items: center; justify-content: center;
  border: 2px dashed var(--cyan); background: rgba(15, 24, 38, .72);
  pointer-events: none;
  font-family: var(--display); font-size: 0.75rem; letter-spacing: 2px;
  text-transform: uppercase; color: var(--cyan);
}
body.is-soltando .drop-hint { display: flex; animation: toast-entra-fijo .18s ease-out both; }
@keyframes toast-entra-fijo { from { opacity: 0; } to { opacity: 1; } }
#viewerHint.error {
  color: var(--amber); text-transform: none; letter-spacing: .5px;
  background: rgba(4, 9, 17, 0.9);
  border: 1px solid rgba(90, 131, 194, 0.4);
  padding: 10px 14px; pointer-events: auto; user-select: text;
}

/* ── Panel de controles del visor (toggles + slides + hints) ─────── */
/* Replegable como el panel de sólidos, pero colapsando en ALTO y a
   --panel-collapsed (no a 0): queda la tira justa para la muesca y la flecha
   de su pestaña. max-height y no height, para animar sin medir el alto real
   del contenido desde JS; el tope abierto (380px) es holgado a propósito —
   con el idioma en inglés y la ventana angosta la fila de toggles envuelve
   más, y un tope justo cortaría los hints de abajo. El contenido vive
   en .viewer-panel__inner y se atenúa aparte (opacity, no display): si no,
   asomaría un pedazo cortado de la primera fila de toggles. */
.viewer-panel {
  flex: 0 0 auto; overflow: hidden;
  /* Animado cuadro a cuadro por panels.js con easeInOutExpo, no por una
     transition de CSS: --viewer-open-t (0 cerrado, 1 abierto). */
  max-height: calc(var(--panel-collapsed) + var(--viewer-open-t, 1) * (380px - var(--panel-collapsed)));
  background: var(--surface);
  /* La línea se ve SIEMPRE, abierto o colapsado — es la línea en la que
     encaja la muesca de la pestaña. Marrón de marca fijo (#884c18), no
     var(--line), igual que el borde de los menús de carga. */
  border-top: 1px solid #884c18;
}
.viewer-panel__inner {
  padding: 18px 14px 8px;
  opacity: 1; transition: opacity .15s ease;
}
#viewer[data-panel-open="false"] .viewer-panel__inner { opacity: 0; pointer-events: none; }

/* Barra de controles: dos bloques (Textura | Vista 3D) separados por una
   línea. Con espacio van los dos en una sola línea; si no, cada bloque baja
   entero a la suya (flex-wrap entre bloques), y dentro de cada uno los
   controles se acomodan solos. */
.vt-barra {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: 8px 22px;
}
.vt-bloque {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: 6px 14px; min-width: 0;
}
/* La línea entre bloques: marrón de marca, más alta que los separadores de
   adentro, para que se lea como "otro grupo". */
.vt-sep--bloque { min-height: 30px; background: #884c18; }

.vt-sep {
  width: 1px; align-self: stretch; min-height: 16px;
  background: var(--line);
}

/* Agrupar no pinta: el reparto de la fila lo marcan los separadores (.vt-sep),
   no un fondo por grupo. Los .vt-group--* sólo ajustan espaciado. */
.vt-group {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 7px; padding: 3px 8px; border-radius: 3px;
}
.vt-group--poles { gap: 8px; }

/* Modo de vista 3D: aparte de los demás toggles (grupo propio, al final de la
   fila) y con los dos extremos siempre rotulados — "Wireframe" e "Imagen" no
   cambian de texto ni tamaño al clickear, sólo se resalta cuál lado está
   activo y se mueve la perilla del medio; nada más en la fila se reacomoda. */
.vt-group--wire { gap: 8px; }
.vt-wire__lbl {
  font-size: 0.5625rem; letter-spacing: .5px; text-transform: none;
  color: var(--muted); white-space: nowrap; transition: color .18s;
}
/* En "ambos" (estado central) las dos etiquetas están "prendidas" — ahí sí
   están las dos activas a la vez; en los extremos, sólo la que corresponde. */
.vt-group--wire[data-state="0"] .vt-wire__lbl:first-child,
.vt-group--wire[data-state="1"] .vt-wire__lbl,
.vt-group--wire[data-state="2"] .vt-wire__lbl:last-child {
  color: var(--amber); font-weight: 700;
}

/* Interruptor: pastilla + perilla, siguiendo el mismo lenguaje visual que el
   resto de la app (--cyan al activarse, con resplandor). */
.vt-sw {
  display: inline-flex; align-items: center; gap: 7px;
  background: transparent; border: none; cursor: pointer; padding: 2px 0;
  width: auto; clip-path: none;
  font-family: var(--mono); font-size: 0.625rem; letter-spacing: .3px;
  text-transform: none;
  color: var(--muted); transition: color .18s;
}
.vt-sw::before { content: none; }
/* display:inline-flex de arriba (una regla de autor) le gana al display:none
   por defecto del atributo hidden (regla de user-agent) si no se repite acá
   — mismo patrón que .vt-btn[hidden]/.progress[hidden] más abajo. Lo
   necesita #btnBoxFlaps (oculto en sólidos sin tapa/fondo, ver app.js). */
.vt-sw[hidden] { display: none; }
.vt-sw:hover:not(:disabled) { color: var(--fg); filter: none; background: transparent; }
.vt-sw:active { transform: none; }
.vt-sw__lbl { line-height: 1.2; white-space: nowrap; font-size: 0.59375rem; }
.vt-sw__track {
  position: relative; width: 27px; height: 13px; border-radius: 999px;
  background: var(--field); border: 1px solid var(--line);
  transition: background .2s ease, border-color .2s ease;
  flex-shrink: 0;
}
.vt-sw__knob {
  position: absolute; top: 1px; left: 1px; width: 9px; height: 9px; border-radius: 50%;
  background: var(--muted);
  transition: transform .2s cubic-bezier(.34,1.56,.64,1), background .2s ease;
}
.vt-sw.is-on { color: var(--fg); }
.vt-sw.is-on .vt-sw__track { background: color-mix(in srgb, var(--cyan) 25%, var(--field)); border-color: var(--cyan); }
.vt-sw.is-on .vt-sw__knob { transform: translateX(14px); background: var(--cyan); box-shadow: 0 0 7px var(--cyan); }
.vt-sw:focus-visible { outline: 2px solid var(--cyan); outline-offset: 3px; border-radius: 3px; }

/* Interruptor de 3 tiempos (wireframe): pastilla más ancha, 3 paradas del
   mismo tamaño de paso (16px) que ya usa el de 2 — el centro (estado por
   defecto, "ambos") en cian; los dos extremos en ámbar, para que se lean
   como "modo especial" y no como un simple on/off. */
.vt-sw3 .vt-sw__track { width: 44px; }
.vt-sw3.is-s1 .vt-sw__track { background: color-mix(in srgb, var(--cyan) 25%, var(--field)); border-color: var(--cyan); }
.vt-sw3.is-s0 .vt-sw__track,
.vt-sw3.is-s2 .vt-sw__track { background: color-mix(in srgb, var(--amber) 22%, var(--field)); border-color: var(--amber); }
.vt-sw3.is-s0 .vt-sw__knob { transform: translateX(0);   background: var(--amber); box-shadow: 0 0 7px var(--amber); }
.vt-sw3.is-s1 .vt-sw__knob { transform: translateX(16px); background: var(--cyan);  box-shadow: 0 0 7px var(--cyan); }
.vt-sw3.is-s2 .vt-sw__knob { transform: translateX(32px); background: var(--amber); box-shadow: 0 0 7px var(--amber); }
.vt-sw3.is-s0, .vt-sw3.is-s1, .vt-sw3.is-s2 { color: var(--fg); }

/* Slides de posición: mismo ancho, valor numérico a la derecha de la etiqueta */
.vt-slider {
  display: flex; flex-direction: column; gap: 3px;
  width: min(220px, 44vw);
}
.vt-slider__lbl {
  font-size: 0.5625rem; letter-spacing: 1.2px; text-transform: uppercase;
  color: var(--muted);
  display: flex; align-items: center; gap: 2px;
}
.vt-slider__lbl b { color: var(--cyan); font-weight: 600; letter-spacing: 0; }
/* En el panel de controles, toda etiqueta va DEBAJO de su control, centrada
   y a la misma distancia en todos (slides, interruptores, el de tres tiempos
   y reflejar). Cada control es una grilla de tres filas de alto FIJO:
   hueco (--lbl-h) / control (--ctl-h) / etiqueta (--lbl-h). El hueco de
   arriba, del mismo alto que la etiqueta, deja el control a media altura de
   la fila, alineado con "Fijar polos", que no lleva etiqueta. El hueco es un
   ::after (el ::before de los botones es su relleno) llevado a la fila 1. */
.vt-barra {
  --lbl-h: calc(0.5625rem * 1.3);
  --ctl-h: 26px;
}
@media (pointer: coarse) { .vt-barra { --ctl-h: 38px; } }
.vt-barra .vt-slider,
.vt-barra .vt-sw:not(.vt-sw3),
.vt-barra .vt-icon,
.vt-barra .vt-group--wire {
  display: grid; grid-template-rows: var(--lbl-h) var(--ctl-h) var(--lbl-h);
  align-items: center; justify-items: center; row-gap: 3px;
  padding: 0;
}
.vt-barra .vt-sw[hidden] { display: none; }
.vt-barra .vt-slider::after,
.vt-barra .vt-sw:not(.vt-sw3)::after,
.vt-barra .vt-icon::after,
.vt-barra .vt-group--wire::after { content: ""; grid-row: 1; grid-column: 1 / -1; }
.vt-barra .vt-slider input[type="range"] { grid-row: 2; justify-self: stretch; margin: 0; }
.vt-barra .vt-sw__track, .vt-barra .vt-icon svg { grid-row: 2; }
/* Las etiquetas, todas iguales: misma letra y mismo alto de línea. */
.vt-barra .vt-slider__lbl, .vt-barra .vt-sw__lbl, .vt-barra .vt-wire__lbl {
  grid-row: 3; justify-content: center;
  font-size: 0.5625rem; line-height: var(--lbl-h); white-space: nowrap;
}
/* Tres tiempos: el interruptor al centro y "Wireframe" / "Imagen" debajo,
   cada uno hacia su extremo. */
.vt-barra .vt-group--wire { grid-template-columns: auto auto; column-gap: 12px; }
.vt-barra .vt-group--wire .vt-sw3 { grid-row: 2; grid-column: 1 / -1; justify-self: center; padding: 0; }
.vt-barra .vt-group--wire .vt-wire__lbl:first-child { grid-column: 1; justify-self: start; }
.vt-barra .vt-group--wire .vt-wire__lbl:last-child { grid-column: 2; justify-self: end; }
/* Reflejar: sólo el ícono, sin el cuadro alrededor; encendido, en naranja. */
.vt-barra .vt-icon,
.vt-barra .vt-icon:hover:not(:disabled),
.vt-barra .vt-icon.is-on,
.vt-barra .vt-icon.is-on:hover:not(:disabled) {
  width: auto; height: auto; border: 0; background: transparent; box-shadow: none;
}
/* Sin las mayúsculas ni el espaciado de letra de los botones (regla base de
   button): el rótulo, como el de los interruptores. */
.vt-barra .vt-icon { color: var(--muted); font-size: 0.625rem; letter-spacing: .3px; text-transform: none; }
.vt-barra .vt-icon:hover:not(:disabled) { color: var(--fg); }
.vt-barra .vt-icon.is-on, .vt-barra .vt-icon.is-on:hover:not(:disabled) { color: var(--cyan); }
.vt-barra .vt-icon.is-on .vt-sw__lbl { color: var(--fg); }
.vt-barra .vt-icon:active { transform: none; }
/* El valor numérico es clickeable: abre un campo para escribir el grado
   exacto, en vez de depender sólo de arrastrar el slide a ojo. */
.vt-slider__val {
  cursor: pointer; border-radius: 3px; padding: 1px 3px; margin: -1px -3px;
  transition: background .15s, color .15s;
}
.vt-slider__val:hover, .vt-slider__val:focus-visible {
  background: color-mix(in srgb, var(--cyan) 20%, transparent);
  outline: none;
}
.vt-slider__edit {
  width: 48px; padding: 1px 4px; margin-left: 2px;
  background: var(--field); border: 1px solid var(--cyan); color: var(--cyan);
  font-family: var(--mono); font-size: 0.625rem; font-weight: 600; letter-spacing: 0;
  border-radius: 3px; outline: none; text-align: right;
}
.vt-slider__edit::-webkit-outer-spin-button,
.vt-slider__edit::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.vt-slider__edit[type="number"] { -moz-appearance: textfield; appearance: textfield; }
.vt-slider input[type="range"] {
  -webkit-appearance: none; appearance: none; width: 100%; height: 4px;
  background: var(--field); border: 1px solid var(--line); outline: none; cursor: pointer;
  margin: 2px 0 0; border-radius: 999px;
}
/* Sin esto, con teclado no se veía qué slide tenía el foco (outline: none arriba). */
.vt-slider input[type="range"]:focus-visible, #uiScale:focus-visible {
  outline: 2px solid var(--cyan); outline-offset: 4px;
}
.vt-slider input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; width: 13px; height: 13px;
  background: var(--cyan); border: 0; border-radius: 50%;
  box-shadow: var(--glow); cursor: pointer;
}
.vt-slider input[type="range"]::-moz-range-thumb {
  width: 13px; height: 13px; border: 0; border-radius: 50%;
  background: var(--cyan); box-shadow: var(--glow); cursor: pointer;
}
.vt-slider input[type="range"]::-moz-range-track {
  height: 4px; background: var(--field); border: 1px solid var(--line); border-radius: 999px;
}

/* Posición X / Rotación Y: un escalón más discretas que Desplegar —el
   control protagonista del visor— por color y peso. Ancho justo para que los
   dos bloques entren en una línea en una pantalla de escritorio. */
.vt-bloque--slides .vt-slider:not(.vt-slider--fold) { width: min(170px, 42vw); opacity: .82; }

/* Desplegar: pista y perilla más grandes, en el mismo naranja que los toggles
   (--cyan); se destaca por tamaño y color, no por aislarse en su propia fila. */
.vt-slider--fold { width: min(210px, 48vw); }
/* La etiqueta, del mismo tamaño y en el mismo lugar que las de Posición X y
   Rotación Y; sólo más brillante. */
.vt-slider--fold .vt-slider__lbl { color: var(--fg); }
.vt-slider--fold input[type="range"] {
  height: 7px; border-color: var(--cyan); border-radius: 999px;
  background: color-mix(in srgb, var(--cyan) 12%, var(--field));
}
.vt-slider--fold input[type="range"]::-webkit-slider-thumb {
  width: 17px; height: 17px; background: var(--cyan);
  box-shadow: 0 0 12px rgba(220, 136, 62, .6);
}
.vt-slider--fold input[type="range"]::-moz-range-thumb {
  width: 17px; height: 17px; background: var(--cyan);
  box-shadow: 0 0 12px rgba(220, 136, 62, .6);
}
.vt-slider--fold input[type="range"]::-moz-range-track {
  height: 7px; background: color-mix(in srgb, var(--cyan) 12%, var(--field)); border-color: var(--cyan);
  border-radius: 999px;
}

/* Con el dedo, una pista de 4 px y una perilla de 13 px son imposibles de
   agarrar. El <input> crece a 26 px de alto (toda esa franja responde al toque)
   y la pista se dibuja aparte, más gruesa, centrada en él. */
@media (pointer: coarse) {
  .vt-slider input[type="range"], #uiScale {
    height: 26px; background: transparent; border: none;
  }
  .vt-slider input[type="range"]::-webkit-slider-runnable-track, #uiScale::-webkit-slider-runnable-track {
    height: 7px; background: var(--field); border: 1px solid var(--line); border-radius: 999px;
  }
  .vt-slider--fold input[type="range"]::-webkit-slider-runnable-track {
    border-color: var(--cyan); background: color-mix(in srgb, var(--cyan) 12%, var(--field));
  }
  .vt-slider input[type="range"]::-webkit-slider-thumb, #uiScale::-webkit-slider-thumb {
    width: 22px; height: 22px; margin-top: -9.5px;
  }
  .vt-slider input[type="range"]::-moz-range-track, #uiScale::-moz-range-track { height: 7px; }
  .vt-slider input[type="range"]::-moz-range-thumb, #uiScale::-moz-range-thumb { width: 22px; height: 22px; }
  /* Interruptores y botones de texto: más alto el blanco, igual el dibujo. */
  .vt-sw { padding: 9px 0; }
  .vt-btn { --tile-h: 34px; }
  .net-toolbar__zoom { height: 38px; }
}

/* Vistas predefinidas: botones cuadrados con ícono, mismo grupo visual que
   los slides de posición/rotación (misma fila, se leen como "parte de la
   misma herramienta" de encuadre de la textura). */
.vt-views { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.vt-view {
  width: 26px; height: 26px; padding: 0; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  background: var(--field); clip-path: none;
  border: 1px solid var(--line);
  color: var(--muted); transition: color .15s, border-color .15s, background .15s;
}
.vt-view::before { content: none; }
.vt-view svg { fill: none; stroke: currentColor; stroke-width: 1.6; }
/* Los <svg> del HTML traen width/height en px fijos, así que no escalarían
   con --ui-scale (que actúa vía font-size). Dimensionarlos en em los ata al
   tamaño de letra de su botón y vuelven a crecer junto con el resto — es lo
   que antes hacía gratis el `zoom`. Sólo los íconos de chrome de la UI: los
   de .solid-tile y .panel-tab tienen su propio dimensionado a propósito. */
.vt-view svg,
.output-tile svg,
.vt-sw svg { width: 1.7em; height: 1.7em; }
.vt-view:hover:not(:disabled) {
  color: var(--cyan); border-color: var(--cyan);
  background: color-mix(in srgb, var(--cyan) 12%, var(--field));
  filter: none;
}
.vt-view:active { transform: translateY(1px); }
.vt-view:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }
/* Con el dedo, 26px es poco. Se agranda el botón entero (no un área
   invisible por encima): son siete pegados con 4px de separación, y un área
   sensible más ancha que el botón se solaparía con la del vecino — el toque
   entre dos caería en cualquiera de los dos. .vt-views ya trae flex-wrap,
   así que si no entran en una línea bajan a la siguiente. */
@media (pointer: coarse) {
  .vt-view { width: 38px; height: 38px; }
}

/* Botones de ícono con estado (reflejar en X / en Y). Mismo cuadrado con
   borde que .vt-view, el lenguaje de botón de ícono que ya usa la app; lo que
   suman es el estado encendido, porque éstos sí recuerdan si están puestos. */
.vt-icon {
  width: 26px; height: 26px; padding: 0; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  background: var(--field); clip-path: none;
  border: 1px solid var(--line);
  color: var(--muted); transition: color .15s, border-color .15s, background .15s;
}
.vt-icon::before { content: none; }
.vt-icon svg { fill: none; stroke: currentColor; stroke-width: 1.6; width: 1.7em; height: 1.7em; }
.vt-icon:hover:not(:disabled) {
  color: var(--cyan); border-color: var(--cyan);
  background: color-mix(in srgb, var(--cyan) 12%, var(--field));
  filter: none;
}
.vt-icon:active { transform: translateY(1px); }
.vt-icon:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }
/* Encendido: relleno naranja y texto oscuro, el mismo par que usa .vt-sw.is-on
   para su pastilla — así "puesto" se lee igual sea interruptor o ícono. */
.vt-icon.is-on {
  color: var(--bg); background: var(--cyan); border-color: var(--cyan);
  box-shadow: var(--glow);
}
.vt-icon.is-on:hover:not(:disabled) { color: var(--bg); background: var(--cyan); }
@media (pointer: coarse) { .vt-icon { width: 38px; height: 38px; } }

/* Botón compacto de texto dentro de la fila de toggles ("Fijar polos",
   "Restaurar polos"): tiene que leerse como BOTÓN —algo que se pulsa para que
   pase una cosa— y no como interruptor, que es lo que hay a su lado.

   Trapecio con el lado DERECHO recto y sólo el izquierdo inclinado, misma
   familia que #btnDownloadPDF. El contorno no puede ser un `border`: un border
   no sigue a un clip-path y desaparecería justo en el lado inclinado. Se usa la
   maquinaria que ya trae `button` — el fondo del botón hace de borde (--brd) y
   su ::before, 1px hacia dentro, pone el relleno (--fill), los dos recortados
   con la misma forma (--o / --i). */
.vt-btn {
  --tile-h: 24px;
  --tile-slant: calc(var(--tile-h) * tan(15deg));
  /* Trapecio simétrico (base ancha abajo): no va pegado a ningún borde, así
     que ningún lado tiene por qué ser recto. */
  --o: polygon(var(--tile-slant) 0, calc(100% - var(--tile-slant)) 0, 100% 100%, 0 100%);
  --i: var(--o);
  --brd: var(--line);
  --fill: var(--field);
  width: auto; height: var(--tile-h); flex: 0 0 auto;
  padding: 0 16px;
  font-size: 0.5625rem; letter-spacing: 1px; text-transform: uppercase;
  color: var(--muted);
  transition: color .15s, background .15s;
}
/* Rótulo oscuro, no naranja: --fill sale translúcido (--field ya lo es, y
   color-mix conserva el alfa), así que en hover se ve a través suyo el --brd
   naranja del propio botón — un rótulo naranja encima quedaba ilegible. */
.vt-btn:hover:not(:disabled) {
  --brd: var(--cyan);
  --fill: color-mix(in srgb, var(--cyan) 12%, var(--field));
  color: var(--bg); filter: none;
}
.vt-btn:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }
.vt-btn[hidden] { display: none; }
/* "Fijar polos" armado: cambia lo que hace un clic normal sobre el visor, así
   que se marca fuerte — no es un estado que convenga pasar por alto. */
.vt-btn.is-on {
  --brd: var(--cyan); --fill: var(--cyan);
  color: var(--bg); box-shadow: var(--glow);
}
.vt-btn.is-on:hover:not(:disabled) {
  --brd: var(--cyan); --fill: var(--cyan); color: var(--bg);
}

/* Franja de ayudas, debajo de los toggles/slides.
   Flujo de texto corrido, NO flex: como flex, cada ayuda era un ítem propio y
   al no entrar en el ancho bajaba entera a la línea siguiente — en móvil eso
   daba tres líneas, una por ayuda. En flujo normal envuelve por donde toque,
   como cualquier párrafo, y las tres se leen como una sola frase separada por
   puntos. */
.viewer-hints {
  display: block; text-align: center;
  margin-top: 6px; padding-top: 5px;
  border-top: 1px solid var(--line-soft);
  font-family: var(--mono); font-size: 0.5625rem; letter-spacing: .3px;
  line-height: 1.7;
  color: var(--muted);
}
/* El separador es del CSS y no del HTML: así no hay que meterlo en las cadenas
   traducidas, y el primero nunca lo lleva. */
.viewer-hints span + span::before {
  content: "∙ "; color: var(--cyan); font-weight: 700;
}
.viewer-hints b { color: var(--muted); font-weight: 600; }
/* Los tres hints son casi todos atajos de teclado (doble clic, Ctrl/Cmd+clic):
   en táctil —móvil y tablet— no se pueden usar, así que el texto no informa,
   confunde. `pointer: coarse` y no un ancho de pantalla: un tablet ancho sigue
   siendo táctil, mismo criterio que ya usa el resto de la hoja. */
@media (pointer: coarse) {
  .viewer-hints { display: none; }
}

/* Confirmaciones pasajeras, aparte de #viewerHint (ver index.html): mismo
   sitio y mismo aire, pero sin la caja de error — no interrumpe, informa. */
#viewerToast {
  position: absolute; left: 50%; bottom: 56px; transform: translateX(-50%);
  margin: 0; z-index: 6; max-width: 80%; text-align: center;
  padding: 7px 14px;
  background: rgba(4, 9, 17, 0.92); border: 1px solid var(--cyan);
  color: var(--cyan); font-size: 0.5625rem; letter-spacing: 1px;
  text-transform: uppercase; pointer-events: none;
}
#viewerToast[hidden] { display: none; }
/* Entra subiendo y sale bajando con la misma curva (ver showToast en uiKit.js). */
#viewerToast { animation: toast-entra .22s cubic-bezier(.2,.8,.3,1) both; }
#viewerToast.is-saliendo { animation: toast-sale .22s ease-in both; }
@keyframes toast-entra { from { opacity: 0; transform: translate(-50%, 8px); } to { opacity: 1; transform: translate(-50%, 0); } }
@keyframes toast-sale  { from { opacity: 1; transform: translate(-50%, 0); } to { opacity: 0; transform: translate(-50%, 8px); } }

/* Un mosaico recién elegido late una vez: confirma el clic sin moverse de lugar
   (ver pop() en uiKit.js). */
.is-pop { animation: mosaico-pop .28s cubic-bezier(.34,1.56,.64,1); }
@keyframes mosaico-pop { 0% { transform: scale(.9); } 60% { transform: scale(1.05); } 100% { transform: scale(1); } }

@media (max-width: 640px) {
  .viewer-panel__inner { padding: 18px 10px 7px; }
  /* Cada bloque a todo el ancho, uno debajo del otro (la línea entre bloques
     sobra: ya los separa el salto). Textura: los dos slides lado a lado y
     debajo grilla, ecuador, reflejar y polos. Vista 3D: modo + costuras y,
     al lado si cabe, Desplegar. */
  .vt-barra { gap: 8px; }
  .vt-sep--bloque { display: none; }
  /* Mosaicos de los menús de carga: el ancho de columna lo ajusta
     menusCarga.js al más grande con el que el menú cabe sin desplazarse;
     los íconos crecen con él. */
  .solid-tile svg, .solid-tile__icon-slot { max-width: calc(var(--col-min) * .55); }
  .image-tile svg { max-width: calc(var(--col-min) * .39); }
  .vt-bloque { flex: 1 1 100%; gap: 6px 10px; }
  .vt-bloque .vt-group { padding: 2px 4px; }
  /* Los tres slides en una fila, a partes iguales. */
  .vt-bloque--slides { flex-wrap: nowrap; }
  .vt-bloque--slides .vt-slider { flex: 1 1 0; width: auto; min-width: 0; }
  .vt-sw__lbl { font-size: 0.5625rem; }
  .viewer-hints { font-size: 0.5625rem; }
}

/* ─── Backdrop + modal ───────────────────────────── */
/* Mismo tratamiento para las CUATRO overlays (desarrollo, acerca de,
   contacto, calidad): el resto de la página queda desenfocado y oscurecido
   detrás de cualquier modal, no sólo el del desarrollo. #modalTierOverlay
   faltaba acá (sólo tenía su propio z-index más abajo, sin posición ni fondo
   ni backdrop-filter): sin caja, no tenía con qué bloquear el clic ni
   desenfocar nada — el modal de desarrollo se seguía viendo nítido y
   usable DETRÁS del de calidad. */
#modalOverlay, #modalAboutOverlay, #modalContactOverlay, #modalTierOverlay {
  position: fixed; inset: 0; z-index: 20;
  background: rgba(2, 5, 11, 0.55);
  backdrop-filter: blur(10px) saturate(.85);
  -webkit-backdrop-filter: blur(10px) saturate(.85);
}
#modalOverlay[hidden], #modalAboutOverlay[hidden], #modalContactOverlay[hidden],
#modalTierOverlay[hidden], .modal[hidden] { display: none; }

.modal {
  position: fixed; left: 50%; top: 50%; z-index: 21;
  transform: translate(-50%, -50%);
  width: min(1100px, 95vw); max-height: 92vh;
  display: flex; flex-direction: column;
  background: var(--panel);
  border: 1px solid var(--line);
  backdrop-filter: blur(20px) saturate(1.2);
  -webkit-backdrop-filter: blur(20px) saturate(1.2);
}
.modal-inner { display: flex; flex-direction: column; min-height: 0; }

.modal-header {
  flex: 0 0 auto;
  /* position:relative es el ancla de la X de cerrar (position:absolute, ver
     .modal-close más abajo) — así el botón queda SIEMPRE centrado
     verticalmente contra el borde derecho, sin importar cuánto crezca la
     cabecera (título en dos líneas, chips de datos del desarrollo). Top
     fijo en vez de 50%+transform se descuadraría apenas la cabecera cambie
     de alto — por eso el centrado es real, no un número calibrado a mano.
     Todo modal con .modal-header hereda esto solo: desarrollo, calidad y
     contacto. */
  position: relative;
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px;
  padding: 10px 44px 8px 16px; border-bottom: 1px solid var(--line);
}
.modal-header h2 {
  margin: 0; font-family: var(--display); font-weight: 700;
  font-size: 0.75rem; letter-spacing: 2px; text-transform: uppercase; color: var(--cyan);
  display: flex; align-items: center; gap: 8px;
}
.modal-title-logo { height: 2.6rem; width: auto; display: block; }
/* La X de cerrar de los CUATRO modales. Va por clase y no por id: con una
   lista de ids, el modal siguiente que se agregue nace con el aspecto de botón
   normal (borde biselado, relleno) hasta que alguien se acuerde de sumarlo —
   que es exactamente lo que le pasó al de calidad. */
.modal-close {
  width: 26px; height: 26px; padding: 0; flex: 0 0 auto;
  background: transparent; clip-path: none; color: var(--muted); font-size: 0.875rem;
}
.modal-close::before { display: none; }
.modal-close:hover, .modal-close:active { color: #fff; filter: none; }
/* Los tres modales con cabecera de título (desarrollo, calidad, contacto)
   centran su X contra el borde derecho; "Acerca de" es distinto — ahí el
   botón va dentro de la franja de controles SOBRE LA FOTO de portada
   (.modal-hero__controls), y ahí lo posiciona el flex del contenedor. */
#btnCloseModal, #btnCloseTier, #btnCloseContact {
  position: absolute; top: 50%; right: 10px; transform: translateY(-50%);
}
/* El press genérico (button:active { transform: translateY(1px) }) pisaría
   ese translateY(-50%) — se cancela acá, no se combina: sólo para estos
   tres, que son los únicos que centran por transform. */
#btnCloseModal:active, #btnCloseTier:active, #btnCloseContact:active {
  transform: translateY(-50%);
}

.net-hint {
  margin: 0; padding: 6px 16px;
  border-bottom: 1px dashed var(--line-soft);
  font-family: var(--mono); font-size: 0.5625rem; letter-spacing: .3px;
  color: var(--muted); text-align: center;
}
.net-hint b { color: var(--muted); font-weight: 600; }

/* Barra del desarrollo: edición de costuras | ayuda | hojas | zoom. En
   pantallas angostas la ayuda baja a su propia línea (flex-basis) y los
   grupos de botones quedan juntos arriba. */
.net-toolbar {
  display: flex; flex-wrap: wrap; align-items: center; gap: 5px 12px;
  padding: 5px 16px; border-bottom: 1px dashed var(--line-soft);
}
.net-toolbar .net-hint { flex: 1 1 220px; order: 0; padding: 0; border: 0; }
.net-toolbar__grupo { display: flex; align-items: center; gap: 4px; flex: 0 0 auto; }
.net-toolbar__grupo[hidden] { display: none; }
.net-toolbar__lbl {
  min-width: 5.6em; text-align: center; font-variant-numeric: tabular-nums;
  font-size: 0.5625rem; letter-spacing: 1px; text-transform: uppercase; color: var(--muted);
}
/* El porcentaje es a la vez el indicador y el botón de "ajustar": texto
   plano, sin forma de botón, para que no compita con los íconos de al lado. */
.net-toolbar__zoom {
  width: auto; min-width: 4.4em; height: 26px; padding: 0 4px;
  background: transparent; clip-path: none;
  font-size: 0.5625rem; letter-spacing: .5px; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.net-toolbar__zoom::before { content: none; }
.net-toolbar__zoom:hover:not(:disabled) { color: var(--cyan); filter: none; background: transparent; }
@media (max-width: 640px) {
  .net-toolbar .net-hint { order: 1; flex-basis: 100%; }
}

/* Zoom y paneo de la vista previa: el lienzo se transforma con origen en su
   esquina (ver _aplicarVista en app.js) y el cuerpo del modal recorta lo que
   sobresale. touch-action: none deja los dos dedos para el pellizco. */
#modalNet .modal-body { overflow: hidden; }
#netCanvas { transform-origin: 0 0; touch-action: none; }
#netCanvas.is-ampliado { cursor: grab; }
#netCanvas.is-paneando { cursor: grabbing; }

.modal-body {
  flex: 1 1 auto; min-height: 0; overflow: auto;
  padding: 14px 16px;
  display: flex; align-items: center; justify-content: center;
  background: repeating-conic-gradient(#0a1220 0% 25%, #070d18 0% 50%) 50% / 18px 18px;
}
#netCanvas {
  max-width: 100%; max-height: 68vh;
  background: #fff;
  cursor: default;
}

.modal-footer {
  flex: 0 0 auto;
  display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap;
  padding: 10px 16px calc(10px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--line);
}
/* En escritorio el botón queda al final de la misma fila que los campos
   (flex:0 auto, sin crecer, así que cae naturalmente a la derecha). En
   móvil/tablet los campos ya ocupan toda la fila y el botón se envuelve
   solo en la suya — ahí, sin justify-content, caería pegado a la
   izquierda. */
@media (max-width: 900px) {
  .modal-footer { justify-content: flex-end; }
}
.modal-footer .field { display: flex; flex-direction: column; gap: 4px; flex: 1 1 150px; }
.modal-footer .field > span {
  font-size: 0.5625rem; letter-spacing: 1.5px; text-transform: uppercase; color: var(--muted);
}
.modal-footer button { width: auto; padding: 8px 16px; flex: 0 0 auto; }
/* Mismo naranja del botón de PDF en la cabecera (.output-tile.primary): acá
   es el único botón, así que no hace falta --brd/--fill, se pisa directo. */
#btnDownloadPDF { --brd: #c0732f; --fill: #c0732f; color: #0f1826; }
#btnDownloadPDF:hover:not(:disabled) { --fill: #c0732f; color: #0f1826; filter: brightness(1.12); }
/* Trapezoidal, misma familia que los botones de descarga de la cabecera
   (.output-tile) y que #btnOpenContact — acá va contra el borde DERECHO del
   modal, así que ese lado queda recto y sólo el izquierdo se inclina. */
#btnDownloadPDF {
  --tile-h: 34px;
  --tile-slant: calc(var(--tile-h) * tan(15deg));
  height: var(--tile-h);
  padding: 0 16px 0 22px;
  clip-path: polygon(var(--tile-slant) 0, 100% 0, 100% 100%, 0 100%);
}

/* Cortes SVG/DXF, pegado a la izquierda de Descargar PDF como OBJ al de PDF
   en la cabecera (.output-tile): el mismo marrón secundario y la base ancha
   ARRIBA, así su lado derecho corre paralelo al inclinado de PDF. PDF sigue
   siendo el principal: naranja vivo y el único con la forma "de cierre". */
.net-salida { display: flex; flex: 0 0 auto; align-items: flex-end; }
#btnDownloadCuts {
  --brd: #884c18; --fill: #884c18; color: #0f1826;
  --tile-h: 34px;
  --tile-slant: calc(var(--tile-h) * tan(15deg));
  height: var(--tile-h);
  padding: 0 22px;
  clip-path: polygon(0 0, 100% 0, calc(100% - var(--tile-slant)) 100%, var(--tile-slant) 100%);
}
#btnDownloadCuts:hover:not(:disabled) { --brd: #884c18; --fill: #884c18; color: #0f1826; filter: brightness(1.12); }
/* Mismo huequito parejo de ~3px que entre los botones de la cabecera. */
#btnDownloadCuts + #btnDownloadPDF { margin-left: calc(3px - var(--tile-slant)); }

/* ─── Modales "Acerca de" y "Contacto" ─────────────── */
/* Más angostos que el del desarrollo (min(1100px,95vw)): no hace falta tanto
   ancho para un párrafo de texto y un formulario de tres campos. */
.modal--about, .modal--contact { width: min(480px, 92vw); max-height: 88vh; }
.modal--about .modal-body, .modal--contact .modal-body {
  display: block; background: none; padding: 16px 18px 18px;
}

/* El modal "Acerca de" no lleva <h2> ni cabecera de texto: la imagen og,
   pegada al borde superior y del mismo ancho que el modal, hace de título.
   El toggle de idioma queda arriba a la izquierda y la X de cerrar arriba a
   la derecha, los dos superpuestos sobre la imagen. */
.modal-hero { position: relative; flex: 0 0 auto; line-height: 0; }
.modal-hero__img { display: block; width: 100%; height: auto; }
.modal-hero__controls {
  position: absolute; top: 10px; z-index: 1;
  display: flex; align-items: center; gap: 10px;
  padding: 5px 8px;
}
.modal-hero__controls--left { left: 10px; }
.modal-hero__controls--right { right: 10px; }

/* Las X de cerrar son de 26px, chicas para el dedo. Acá SÍ conviene un área
   invisible más grande que el botón (al revés que en .vt-view): están solas en
   su esquina, sin vecinos con los que solaparse, y agrandar el botón correría
   la X de la esquina o taparía más foto en el modal. */
@media (pointer: coarse) {
  .modal-close { position: relative; }
  #btnCloseModal, #btnCloseTier, #btnCloseContact { position: absolute; }
  .modal-close::after {
    content: ''; position: absolute;
    top: 50%; left: 50%; transform: translate(-50%, -50%);
    width: 44px; height: 44px;
  }
}
/* Sin fondo detrás (tapaba la imagen): sombra en vez de placa sólida para
   que sigan leyéndose sobre una foto de brillo variable. */
.modal-hero__controls .vt-wire__lbl, .modal-hero__controls .vt-sw__lbl {
  color: #d6e0ec; text-shadow: 0 1px 4px rgba(0,0,0,.85);
}
/* La X, aparte: naranja de acento en reposo (así se distingue del texto
   blanquecino del toggle de idioma, que va al lado) y blanca en hover/click —
   mismo criterio de los otros tres modales, sólo que ahí el fondo propio del
   botón hacía ese trabajo; acá, sin fondo, lo hace el color solo. */
.modal-hero__controls #btnCloseAbout {
  color: var(--cyan); text-shadow: 0 1px 4px rgba(0,0,0,.85);
}
.modal-hero__controls #btnCloseAbout:hover, .modal-hero__controls #btnCloseAbout:active {
  color: #fff;
}
.modal-hero__controls .vt-sw__track { box-shadow: 0 1px 4px rgba(0,0,0,.85); }

.about-lead { margin: 0 0 14px; font-size: 0.6875rem; line-height: 1.6; color: var(--fg); }
.about-lead strong { color: var(--cyan); font-weight: 700; }

.about-group { margin-bottom: 14px; }
.about-group h3 {
  margin: 0 0 6px; font-family: var(--display); font-weight: 700;
  font-size: 0.5625rem; letter-spacing: 1.5px; text-transform: uppercase; color: var(--cyan-2);
}
.about-credits { margin: 0; padding: 0; list-style: none; font-size: 0.625rem; line-height: 1.7; color: var(--muted); }
.about-credits b { color: var(--fg); font-weight: 600; }
.about-credits a { color: inherit; text-decoration: underline dotted; text-underline-offset: 2px; }
.about-credits a:hover, .about-credits a:focus-visible { color: var(--cyan); }
/* "Se abre en una ventana nueva": del alto de la letra, pegado al texto. */
.about-credits a .ext { width: 0.9em; height: 0.9em; margin-left: 0.2em; vertical-align: -0.1em; }

.about-author { margin: 0; font-size: 0.625rem; line-height: 1.7; color: var(--muted); }

/* Atajos de teclado. Dos columnas en cuanto hay ancho: son ocho filas cortas
   y en una sola columna estiran el modal más de lo que aporta. */
.kbd-list {
  margin: 0; padding: 0; list-style: none;
  display: grid; grid-template-columns: 1fr; gap: 4px 16px;
  font-size: 0.5625rem; line-height: 1.3; color: var(--muted);
}
@media (min-width: 420px) { .kbd-list { grid-template-columns: 1fr 1fr; } }
/* Si la tecla y su descripción no caben en una línea, la descripción baja
   pegada (1 px entre líneas), no separada como si fuera otro atajo. */
.kbd-list li { display: flex; align-items: center; gap: 1px 4px; flex-wrap: wrap; }
.kbd-list span { color: var(--muted); }
kbd {
  font-family: var(--mono); font-size: 0.5625rem; font-weight: 700;
  line-height: 1; padding: 3px 5px; white-space: nowrap;
  color: var(--fg); background: var(--field);
  border: 1px solid var(--line); border-bottom-width: 2px; border-radius: 3px;
}

/* Escala global de la interfaz: escribe --ui-scale en :root (ver
   html{font-size} arriba), así que mueve texto e íconos a la vez. */
.ui-scale { display: flex; flex-direction: column; gap: 5px; }
.ui-scale__row { display: flex; align-items: center; gap: 10px; }
.ui-scale__row b {
  color: var(--cyan); font-size: 0.625rem; font-weight: 700;
  font-variant-numeric: tabular-nums; min-width: 42px;
}
#uiScale {
  -webkit-appearance: none; appearance: none; flex: 1 1 auto; height: 4px;
  background: var(--field); border: 1px solid var(--line); outline: none;
  cursor: pointer; border-radius: 999px;
}
#uiScale::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; width: 13px; height: 13px;
  background: var(--cyan); border: 0; border-radius: 50%;
  box-shadow: var(--glow); cursor: pointer;
}
#uiScale::-moz-range-thumb {
  width: 13px; height: 13px; border: 0; border-radius: 50%;
  background: var(--cyan); box-shadow: var(--glow); cursor: pointer;
}
#uiScale::-moz-range-track {
  height: 4px; background: var(--field); border: 1px solid var(--line); border-radius: 999px;
}
.about-author b { color: var(--fg); font-weight: 600; }
/* Sin línea encima, y con márgenes negativos que anulan el padding de
   .modal-body (16px 18px 18px): Contactar queda pegado a la esquina inferior
   izquierda del modal. */
.about-footer {
  display: flex; align-items: center; gap: 8px; margin: 14px -18px -18px;
}
.about-footer button { width: auto; padding: 8px 16px; flex: 0 0 auto; }
/* Trapezoidal, misma familia que los botones de descarga de la cabecera
   (.output-tile, ver ahí el porqué de la cuenta): acá va contra el borde
   IZQUIERDO del modal, así que ese lado queda recto y sólo el derecho se
   inclina — lo opuesto de Descargar PDF, que va contra el derecho. */
#btnOpenContact {
  --tile-h: 34px;
  --tile-slant: calc(var(--tile-h) * tan(15deg));
  height: var(--tile-h);
  padding: 0 22px 0 16px;
  clip-path: polygon(0 0, calc(100% - var(--tile-slant)) 0, 100% 100%, 0 100%);
}
.about-footer .perigami-link {
  margin-left: auto; margin-right: 18px; margin-bottom: 18px; align-self: flex-end; display: flex; align-items: center; width: auto; flex: 0 0 auto;
  background: none; clip-path: none; padding: 0;
}
.about-footer .perigami-link::before { display: none; }
.about-footer .perigami-link img { height: 1.9em; width: auto; opacity: .7; transition: opacity .15s; }
.about-footer .perigami-link:hover img { opacity: 1; }

/* ─── Modal de compra: elegir versión ──────────────── */
/* Por encima del modal del desarrollo (z-index 21/20): en el caso del PDF éste
   se abre ENCIMA de aquél, no en su lugar. */
#modalTierOverlay { z-index: 30; }
.modal--tier { z-index: 31; width: min(620px, 94vw); }
.modal--tier .modal-body { display: block; background: none; padding: 16px 18px 18px; }

/* Más aire lateral en móvil. Va acá, DESPUÉS de las tres reglas de ancho de
   arriba: mismo peso de selector, así que el orden es lo único que decide, y
   puesto antes (junto al resto de lo móvil) no aplicaba nada. */
@media (max-width: 640px) {
  .modal { width: min(1100px, 88vw); }
  .modal--about, .modal--contact { width: min(480px, 88vw); }
  .modal--tier { width: min(620px, 88vw); }
}

/* Texto sólo para lectores de pantalla (la etiqueta del correo, los radios). */
.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;
}

/* ─── Elegir versión: tres opciones de menos a más ───────────────────
   Misma organización que el modal de compra de Caleidociclero, con el
   aspecto de Perihedra (ver el comentario en index.html). Cada opción es un
   <label> con su radio escondido: toda la tarjeta es el blanco del clic. */
.tier-intro { margin: 0 0 12px; font-size: 0.625rem; line-height: 1.5; color: var(--muted); }
.tier-opciones { display: grid; gap: 8px; margin-bottom: 12px; }
.tier-opcion {
  display: block; cursor: pointer;
  padding: 10px 14px 11px;
  background: var(--field); border: 1px solid var(--line);
  transition: border-color .15s, background .15s, box-shadow .15s;
}
.tier-opcion:hover { border-color: color-mix(in srgb, var(--cyan) 55%, var(--line)); }
.tier-opcion:has(input:checked) {
  border-color: var(--cyan);
  background: color-mix(in srgb, var(--cyan) 9%, var(--field));
  box-shadow: 0 0 0 1px var(--cyan) inset;
}
.tier-opcion:has(input:focus-visible) { outline: 2px solid var(--cyan); outline-offset: 2px; }
/* Cortes SVG/DXF no tiene versión gratis: la opción se ve, pero no se elige. */
.tier-opcion:has(input:disabled) { opacity: .45; cursor: not-allowed; }
.tier-opcion__cabeza {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: 2px 10px; margin-bottom: 4px;
}
.tier-opcion__nombre {
  display: flex; align-items: center; flex-wrap: wrap; gap: 4px 7px;
  font-family: var(--display); font-size: 0.6875rem; font-weight: 700; letter-spacing: 1px;
  color: var(--fg);
}
.tier-opcion__icono { display: block; width: 16px; height: 16px; flex: 0 0 auto; }
.tier-opcion__precio {
  font-size: 0.8125rem; font-weight: 700; white-space: nowrap; text-align: right;
  color: var(--cyan);
}
/* Gratis: apagada mientras no es la elegida — está, pero no compite. */
.tier-opcion--gratis:not(:has(input:checked)) :is(.tier-opcion__nombre, .tier-opcion__precio) { color: var(--muted); }
.tier-opcion--gratis:not(:has(input:checked)) .tier-opcion__icono { opacity: .6; }
.tier-opcion--gratis:has(input:checked) {
  border-color: var(--muted); background: var(--field); box-shadow: 0 0 0 1px var(--muted) inset;
}
.tier-opcion--rec { border-color: color-mix(in srgb, var(--cyan) 45%, var(--line)); }
.tier-etiqueta {
  font-family: var(--mono); font-size: 0.5625rem; font-weight: 700;
  letter-spacing: 1px; text-transform: uppercase;
  padding: 2px 6px; border: 1px solid var(--line); color: var(--muted);
}
/* "Recomendado": pestaña trapezoidal colgada del borde SUPERIOR de la
   tarjeta (ancha arriba), con la inclinación de 15° de todos los trapecios
   de la app. Sólo se sube —top = −(padding-top + borde)— para quedar montada
   sobre el borde sin agrandar la tarjeta; por eso esta cabeza alinea arriba. */
.tier-opcion--rec :is(.tier-opcion__cabeza, .tier-opcion__nombre) { align-items: flex-start; }
.tier-etiqueta--rec {
  --tile-h: 18px;
  --tile-slant: calc(var(--tile-h) * tan(15deg));
  position: relative; top: -11px;
  display: flex; align-items: center; height: var(--tile-h); padding: 0 12px;
  border: 0; background: var(--cyan); color: var(--bg);
  clip-path: polygon(0 0, 100% 0, calc(100% - var(--tile-slant)) 100%, var(--tile-slant) 100%);
}
.tier-opcion__specs {
  display: block; margin: 0; padding: 0; list-style: none;
  font-size: 0.625rem; line-height: 1.6; color: var(--muted);
}
.tier-opcion__specs > li::before { content: "✓"; display: inline-block; width: 15px; color: var(--cyan); font-weight: 700; }
.tier-opcion__specs > li.no::before { content: "–"; color: var(--muted); }
.tier-herr-lead { display: block; margin-bottom: 5px; color: var(--fg); }
.tier-herrs { display: flex; flex-wrap: wrap; gap: 4px; margin: 0 0 6px; padding: 0; list-style: none; }
.tier-herr {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 7px 3px 5px;
  border: 1px solid var(--line);
  font-size: 0.5625rem; color: var(--fg); text-decoration: none;
  transition: border-color .15s, background .15s;
}
.tier-herr img { display: block; width: 14px; height: 14px; object-fit: contain; }
.tier-herr:hover { border-color: var(--cyan); background: color-mix(in srgb, var(--cyan) 10%, transparent); }
.tier-herr:focus-visible { outline: 1px solid var(--cyan); outline-offset: 1px; }
.tier-herr--aqui { border-color: color-mix(in srgb, var(--cyan) 55%, var(--line)); background: color-mix(in srgb, var(--cyan) 10%, transparent); }
.tier-futuras { display: block; }
.tier-futuras::before { content: "✓"; display: inline-block; width: 15px; color: var(--cyan); font-weight: 700; }
.tier-mas { display: inline-block; vertical-align: -6px; margin-left: 4px; opacity: .8; transition: opacity .15s; }
.tier-mas img { display: block; height: 20px; width: auto; }
.tier-mas:hover { opacity: 1; }
/* Entrada escalonada de las tres al abrir. */
#modalTier:not([hidden]) .tier-opcion { animation: tier-entra .4s cubic-bezier(.2, .7, .3, 1) both; }
#modalTier:not([hidden]) .tier-opcion:nth-child(2) { animation-delay: .07s; }
#modalTier:not([hidden]) .tier-opcion:nth-child(3) { animation-delay: .14s; }
@keyframes tier-entra { from { opacity: 0; transform: translateY(6px); } }

.tier-correo[hidden] { display: none; }
/* Pie: "¿Ya compraste?" a la izquierda y el botón de pagar pegado a la
   esquina inferior derecha del modal (cancela el padding del cuerpo: su
   lado recto queda contra el borde real, como Descargar PDF). */
.tier-pie {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end;
  gap: 6px 12px; margin: 4px -18px -18px 0;
}
.tier-pie .tier-link { margin-right: auto; font-size: 0.625rem; text-align: left; }
#btnTierPlus {
  --tile-h: 34px;
  --tile-slant: calc(var(--tile-h) * tan(15deg));
  width: auto; height: var(--tile-h);
  padding: 0 18px 0 24px;
  clip-path: polygon(var(--tile-slant) 0, 100% 0, 100% 100%, 0 100%);
}

/* El link: ni botón trapezoidal ni tecla, para que no compita con el CTA
   primario de arriba. clip-path/::before en none porque sigue siendo un
   <button> — sin apagarlos, el bisel genérico de fondo (ver el ::before de
   la regla base de button, más arriba) se veía como un borde fantasma al
   pasar el mouse, aunque el propio botón no tuviera background ni border. */
.tier-link {
  display: block; width: auto;
  background: none; border: none; padding: 0; margin: 0;
  font-family: inherit; font-size: 0.5625rem; line-height: 1.2; text-align: right;
  text-transform: none; letter-spacing: normal;
  color: var(--cyan-2); text-decoration: underline; cursor: pointer;
  clip-path: none;
}
.tier-link::before { content: none; }
/* button.tier-link (no .tier-link a secas): button:hover:not(:disabled) de la
   regla base (clase+pseudoclase+pseudoclase+elemento) es MÁS específico que
   .tier-link:hover sola — ganaba ella, dejando el texto oscuro (var(--bg)) en
   vez de blanco. Con el elemento sumado acá empatan en especificidad, y esta
   regla gana por venir después en el archivo. */
button.tier-link:hover, button.tier-link:active, button.tier-link:focus-visible {
  color: #fff; filter: none;
}

/* ─── "¿Ya compraste?" y pantalla de resultado ─────────────────────── */
/* Comparten el body del modal con .tier-compra; tierModal.js muestra sólo uno
   de los tres a la vez (ver comentario en index.html). */
.tier-restaurar { max-width: 360px; margin: 0 auto; }
.tier-restaurar > p:first-child { margin: 0 0 12px; font-size: 0.625rem; color: var(--muted); line-height: 1.5; }
/* Mismo trapecio que #btnTierPlus (borde derecho recto): el bisel
   genérico de esquinas cortadas (--o/--i, ver la regla base de button más
   arriba) ya no se usa en ningún botón de la app. .tier-restaurar no es flex
   (a diferencia de .tier-pie), así que el auto-margin es lo que alinea a la
   derecha un botón de ancho propio en vez de align-self. */
.tier-restaurar button {
  width: auto; margin: 4px 0 0 auto;
  --tile-h: 34px;
  --tile-slant: calc(var(--tile-h) * tan(15deg));
  height: var(--tile-h);
  padding: 0 16px 0 22px;
  clip-path: polygon(var(--tile-slant) 0, 100% 0, 100% 100%, 0 100%);
}

.tier-resultado { text-align: center; padding: 6px 4px 2px; max-width: 360px; margin: 0 auto; }
.tier-resultado__icono { width: 44px; height: 44px; margin: 0 auto 12px; }
.tier-resultado__icono--ok { color: var(--cyan); }
.tier-resultado__icono--error { color: #c0732f; }
.tier-resultado__icono--espera { color: var(--muted); animation: tier-girar 1.4s linear infinite; }
@keyframes tier-girar { to { transform: rotate(360deg); } }
.tier-resultado h3 {
  margin: 0 0 8px; font-family: var(--display); font-weight: 700;
  font-size: 0.6875rem; letter-spacing: 1px; text-transform: uppercase; color: var(--fg);
}
.tier-resultado p { margin: 0 0 14px; font-size: 0.625rem; color: var(--muted); line-height: 1.5; }
.tier-resultado button { width: auto; padding: 8px 16px 8px 22px; }

/* ─── Formulario de contacto ────────────────────────── */
.form-field { display: flex; flex-direction: column; gap: 4px; margin-bottom: 10px; }
.form-field span {
  font-size: 0.5625rem; letter-spacing: 1.5px; text-transform: uppercase; color: var(--muted);
}
.form-field input, .form-field textarea {
  width: 100%; padding: 7px 9px;
  border: 1px solid var(--line); background: var(--field); color: var(--fg);
  font-family: var(--mono); font-size: 0.6875rem; letter-spacing: .3px;
  outline: none; border-radius: 0; resize: vertical;
  transition: border-color .15s, box-shadow .15s;
}
.form-field textarea { min-height: 80px; }
.form-field input:focus, .form-field textarea:focus { border-color: var(--cyan); box-shadow: var(--glow); }
/* Honeypot: existe en el DOM (los bots de formulario lo rellenan igual),
   pero invisible y fuera del orden de tabulación para una persona real. */
.hp-field { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.captcha-row {
  display: flex; align-items: center; gap: 8px; margin-bottom: 10px;
  font-size: 0.625rem; color: var(--muted);
}
.captcha-row input {
  width: 64px; padding: 7px 9px;
  border: 1px solid var(--line); background: var(--field); color: var(--fg);
  font-family: var(--mono); font-size: 0.6875rem; text-align: center;
}
.captcha-row input:focus { border-color: var(--cyan); box-shadow: var(--glow); outline: none; }

.contact-status {
  margin: 0 0 10px; padding: 8px 10px; font-size: 0.625rem; line-height: 1.5;
  border: 1px solid var(--line);
}
/* --amber ya no es un tono cálido (ver :root) así que no sirve para marcar
   error — se usa el marrón claro de la paleta (mismo tono que --cyan-2): el
   más oscuro (#a45f22) no llegaba al contraste AA (3.6:1) sobre --bg. */
.contact-status.error { color: #c0732f; border-color: rgba(192,115,47,.4); }
.contact-status.ok { color: var(--cyan); border-color: var(--line); }
.contact-status[hidden] { display: none; }

/* Mismo trapecio que #btnOpenContact ("Contactar" en el modal de info): borde
   izquierdo recto y sólo el derecho inclinado. Los dos son el botón que cierra
   su formulario/pantalla, así que se leen como el mismo gesto. */
#btnSendContact {
  --tile-h: 34px;
  --tile-slant: calc(var(--tile-h) * tan(15deg));
  width: auto; height: var(--tile-h);
  padding: 0 22px 0 16px;
  clip-path: polygon(0 0, calc(100% - var(--tile-slant)) 0, 100% 100%, 0 100%);
}

/* Datos del desarrollo en la cabecera del modal (caras, piezas, uniones,
   tamaño…): misma familia trapezoidal que los botones de descarga — un chip
   por detalle, casi pegados entre sí (ver el margen negativo más abajo),
   alternando el sentido igual que PDF/OBJ/PNG, con los dos extremos del grupo
   rectos en vez de en punta. */
.net-info { display: flex; flex-wrap: wrap; row-gap: 5px; }
.net-info__chip {
  --tile-h: 24px;
  --tile-slant: calc(var(--tile-h) * tan(15deg));
  height: var(--tile-h);
  display: flex; align-items: center;
  padding: 0 14px;
  background: var(--line); color: var(--muted);
  font-size: 0.5625rem; letter-spacing: 1px; white-space: nowrap;
  clip-path: polygon(
    var(--tile-slant) 0, calc(100% - var(--tile-slant)) 0,
    100% 100%, 0 100%
  );
}
.net-info__chip b { color: var(--cyan); font-weight: 700; }
/* Pares: espejado en Y (base ancha arriba) — mismo criterio que #btnOBJ. */
.net-info__chip:nth-child(even) {
  clip-path: polygon(
    0 0, 100% 0,
    calc(100% - var(--tile-slant)) 100%, var(--tile-slant) 100%
  );
}
/* Casi pegados: el margen negativo compensa el ancho que el propio bisel le
   resta al hueco entre dos chips vecinos — ver la nota de la misma regla en
   .output-tile. */
.net-info__chip + .net-info__chip { margin-left: calc(3px - var(--tile-slant)); }
/* El primero siempre es impar (base ancha abajo): recto el lado que da
   hacia afuera del grupo (izquierda). */
.net-info__chip:first-child {
  clip-path: polygon(0 0, calc(100% - var(--tile-slant)) 0, 100% 100%, 0 100%);
}
/* El último puede caer en un chip par o impar según cuántos detalles haya
   esta vez (con hojas/sin pestaña, a veces son 6, a veces 4) — cubre los
   dos casos, siempre recto del lado derecho. */
.net-info__chip:last-child:nth-child(odd) {
  clip-path: polygon(var(--tile-slant) 0, 100% 0, 100% 100%, 0 100%);
}
.net-info__chip:last-child:nth-child(even) {
  clip-path: polygon(0 0, 100% 0, 100% 100%, var(--tile-slant) 100%);
}
/* Un solo detalle (caso raro, pero posible): los dos bordes rectos. */
.net-info__chip:first-child:last-child { clip-path: none; }

/* ─── Progreso ───────────────────────────────────── */
.progress { display: flex; align-items: center; gap: 8px; }
.progress[hidden] { display: none; }
.progress-track {
  flex: 1 1 auto; height: 4px; min-width: 60px;
  background: var(--field); border: 1px solid var(--line);
  overflow: hidden;
}
.progress-fill {
  height: 100%; width: 0%;
  background: repeating-linear-gradient(
    135deg,
    var(--cyan) 0 6px,
    color-mix(in srgb, var(--cyan) 55%, transparent) 6px 12px
  );
  background-size: 200% 100%;
  box-shadow: var(--glow);
  transition: width .12s linear;
  animation: progress-stripes 900ms linear infinite;
}
/* Franja "de corte" en diagonal en vez de una barra lisa — el mismo motivo
   de líneas rectas del papel recortándose, no un genérico spinner. */
@keyframes progress-stripes { from { background-position: 0 0; } to { background-position: -24px 0; } }
@media (prefers-reduced-motion: reduce) { .progress-fill { animation: none; } }
.progress-pct {
  font-size: 0.5625rem; letter-spacing: 1px; color: var(--cyan);
  font-variant-numeric: tabular-nums; min-width: 32px; text-align: right;
}
.progress-stage {
  font-size: 0.5625rem; letter-spacing: 1.5px; text-transform: uppercase;
  color: var(--muted); white-space: nowrap;
}

/* Panel de progreso sobre la vista del desarrollo */
.net-overlay {
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  display: flex; align-items: center; gap: 14px;
  width: min(420px, 80%); padding: 14px 18px;
  background: rgba(4, 9, 17, 0.92);
  border: 1px solid var(--line);
}
.net-overlay[hidden] { display: none; }
.modal-body { position: relative; }

/* Ícono temático del cálculo: un sólido girando, en vez del típico spinner
   genérico — "buscando el mejor desarrollo" prueba muchas orientaciones
   de corte, igual que este poliedro dando vueltas. */
.net-overlay__icon {
  flex: 0 0 auto; color: var(--cyan);
  animation: net-overlay-spin 2.4s linear infinite;
  filter: drop-shadow(0 0 6px rgba(220, 136, 62, .45));
}
@keyframes net-overlay-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .net-overlay__icon { animation: none; } }
.net-overlay__text {
  flex: 1 1 auto; min-width: 0;
  display: flex; flex-direction: column; gap: 6px;
}

/* ─── Preloaders ─────────────────────────────────── */
/* El de arranque (#appLoader) vive en el <style> del <head> de index.html:
   tiene que verse antes de que llegue esta hoja. Ese mismo <style> esconde
   todo lo demás hasta acá; esta regla (mismo selector) lo vuelve a mostrar. */
body > :not(#appLoader) { visibility: visible; }

/* Mapeo de imagen: mismo overlay que .net-overlay pero acotado al visor 3D
   (no a un modal de 420px) y sin barra de progreso — el mapeo es un solo
   paso síncrono, no algo que se pueda medir en tramos como el cálculo del
   desarrollo. */
.viewer-loading { width: auto; max-width: 90%; padding: 10px 16px; gap: 10px; }
.viewer-loading .net-overlay__icon { width: 26px; height: 26px; }

button:focus-visible, select:focus-visible {
  outline: 2px solid var(--cyan); outline-offset: 2px;
}

/* ─── Movimiento reducido ────────────────────────── */
/* Quien pidió menos animación en su sistema (mareos, vértigo, sensibilidad
   vestibular) no debería tragarse las transiciones de los paneles ni los
   barridos. No se anula del todo con `animation: none`: dejarlas en un
   instante conserva los eventos de fin de transición que algún día pueda
   estar esperando el JS, sin que se vea movimiento. Las animaciones que
   dispara el propio JS (barrido de bienvenida, ir al desarrollo plano) se
   saltan aparte — ver prefersReducedMotion() en app.js. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}
