// ── DATA REAL — Navarro ERP v5 ────────────────────────

// ── CONFIGURACIÓN GLOBAL (editable en Ajustes) ───────
function getConfig() {
  const def = {
    iva: 19, sprtAncho: 80, sprtMargen: 3, moneda: 'CLP',
    tiendaNombre: 'Navarro Joyería Fina',
    tiendaDireccion: 'Calle Comercio 123, Santa Cruz',
    tiendaWeb: 'navarro-joyeria.cl',
  };
  try { return { ...def, ...JSON.parse(localStorage.getItem('navarro_config') || '{}') }; }
  catch { return def; }
}
function setConfig(partial) {
  const cur = getConfig();
  localStorage.setItem('navarro_config', JSON.stringify({ ...cur, ...partial }));
}

const TIENDAS = [
  { id:'navarro', nombre:'Navarro Joyería Fina',  sigla:'NJF', direccion:'Calle Comercio 123, Santa Cruz', color:'#c9a84c' },
  { id:'rubi',    nombre:'Joyería Rubí · Taller', sigla:'JRT', direccion:'Calle Comercio 125, Santa Cruz', color:'#9b6b6b' },
];

// ── PRECIOS METALES CHILE — 29 Abril 2026 ───────────────────────────────────
// Fuentes oficiales:
//   USD/CLP : SII (sii.cl) → $895,07 (27-abr-2026, último valor publicado)
//   UF      : SII (sii.cl) → $40.120,20 (30-abr-2026)
//   Oro     : cotizacionrealoro.com → Oro 24K spot $132.591 CLP/g (29-abr-2026 22:39 UTC)
//   Plata   : cotizacionrealoro.com → Plata 925 spot $2,1265 USD/g = $1.903 CLP/g
//
// Spot CLP/g por quilate (sobre base 24K = $132.591):
//   22K (92%): $121.983 · 21.6K (90%): $119.332 · 18K (75%): $99.443
//   14K (58%): $76.903  · 12K (50%): $66.296   · 10K (42%): $55.688
//
// Fórmula live: goldUSD/oz × USD/CLP / 31.1035 × pureza
// Fuente API: mindicador.cl (USD/CLP, UF) + metals.live (XAU/XAG USD/oz)
//
// Márgenes de mercado joyería Chile (sobre spot):
//   calle ×1.08 · mayorista ×1.14 · mercado local ×1.23 · premium ×1.43 · online ×1.33
function calcTiersOro(spotCLPg) {
  return {
    bolsa:    { v: Math.round(spotCLPg),          label:'Spot bolsa'      },
    calle:    { v: Math.round(spotCLPg * 1.08),   label:'Precio calle'    },
    mayorista:{ v: Math.round(spotCLPg * 1.14),   label:'Mayorista'       },
    local:    { v: Math.round(spotCLPg * 1.23),   label:'Mercado local'   },
    premium:  { v: Math.round(spotCLPg * 1.43),   label:'Joyería premium' },
    online:   { v: Math.round(spotCLPg * 1.33),   label:'Online'          },
  };
}
function calcTiersPlata(spotCLPg) {
  return {
    bolsa:    { v: Math.round(spotCLPg),          label:'Spot bolsa'    },
    calle:    { v: Math.round(spotCLPg * 1.12),   label:'Precio calle'  },
    mayorista:{ v: Math.round(spotCLPg * 1.35),   label:'Mayorista'     },
    local:    { v: Math.round(spotCLPg * 1.68),   label:'Mercado local' },
    premium:  { v: Math.round(spotCLPg * 2.37),   label:'Premium'       },
    online:   { v: Math.round(spotCLPg * 2.00),   label:'Online'        },
  };
}
const PRECIO_METALES = {
  'Oro 18K': {
    // 24K=$132.591 × 0.75 = $99.443 spot bolsa
    valor:122000, unidad:'g', variacion:-1.7, color:'#c9a84c',
    spotUSDoz:4689, usdClp:895,
    spotCLPg:99443,
    precioLocal:122000,
    fuente:'cotizacionrealoro.com · SII', fechaActualizacion:'2026-04-29',
    tiers: calcTiersOro(99443),
    nota:'18K · 75% pureza · spot $99.443/g (cotizacionrealoro.com 29-abr)',
  },
  'Oro 14K': {
    // 24K=$132.591 × 0.58 = $76.903 spot bolsa
    valor:94800, unidad:'g', variacion:-1.7, color:'#d4a843',
    spotCLPg:76903,
    precioLocal:94800,
    fuente:'cotizacionrealoro.com · SII', fechaActualizacion:'2026-04-29',
    tiers: calcTiersOro(76903),
    nota:'14K · 58% pureza · spot $76.903/g',
  },
  'Oro Blanco 18K': {
    // 18K spot + 5% (rodio + aleación blanqueante)
    valor:127000, unidad:'g', variacion:-1.6, color:'#e0e8f0',
    spotCLPg:104415,  // 99443 × 1.05
    precioLocal:127000,
    fuente:'cotizacionrealoro.com · SII', fechaActualizacion:'2026-04-29',
    tiers: calcTiersOro(104415),
    nota:'18K blanco + rodio · spot ~$104.415/g',
  },
  'Oro Rosado 18K': {
    // Mismo spot que 18K amarillo (aleación Cu no cambia pureza Au)
    valor:123000, unidad:'g', variacion:-1.7, color:'#d4906a',
    spotCLPg:99443,
    precioLocal:123000,
    fuente:'cotizacionrealoro.com · SII', fechaActualizacion:'2026-04-29',
    tiers: calcTiersOro(99443),
    nota:'18K rosado · aleación Cu+Au · mismo spot que 18K',
  },
  'Plata 925': {
    // Silver $71,50 USD/oz → $71.50/31.1035 = $2.298 USD/g pura → ×0.925 = $2.126 USD/g 925
    // × USD/CLP $895 = $1.903 CLP/g spot
    valor:3200, unidad:'g', variacion:-6.1, color:'#b0b8c8',
    spotUSDoz:71.50, usdClp:895,
    spotCLPg:1903,
    precioLocal:3200,
    fuente:'cotizacionrealoro.com · SII', fechaActualizacion:'2026-04-29',
    tiers: calcTiersPlata(1903),
    nota:'925 · 92.5% pureza · spot $1.903/g · $71,50 USD/oz plata',
  },
  'Platino 950': {
    // Platinum ~$980 USD/oz × $895 / 31.1035 × 0.95 ≈ $26.700/g
    valor:32000, unidad:'g', variacion:+0.4, color:'#d0d8e0',
    spotCLPg:26700,
    precioLocal:32000,
    fuente:'Mercado joyería Chile', fechaActualizacion:'2026-04-29',
    tiers: calcTiersOro(26700),
    nota:'Platino 950 · spot estimado ~$26.700/g',
  },
  'Paladio': {
    // Palladium ~$930 USD/oz × $895 / 31.1035 × 0.95 ≈ $25.400/g
    valor:31000, unidad:'g', variacion:+0.2, color:'#c8d0d8',
    spotCLPg:25400,
    precioLocal:31000,
    fuente:'Mercado joyería Chile', fechaActualizacion:'2026-04-29',
    tiers: calcTiersOro(25400),
    nota:'Paladio · spot estimado ~$25.400/g',
  },
  'Cobre': {
    valor:20, unidad:'g', variacion:+0.5, color:'#b8733a',
    spotCLPg:10,
    precioLocal:20,
    fuente:'Mercado local', fechaActualizacion:'2026-04-29',
    tiers:{ bolsa:{v:10,label:'Spot bolsa'}, calle:{v:13,label:'Precio calle'}, mayorista:{v:16,label:'Mayorista'}, local:{v:20,label:'Mercado local'}, premium:{v:30,label:'Artesanal'} },
    nota:'Cobre artesanal Chile',
  },
  'Latón': {
    valor:17, unidad:'g', variacion:0, color:'#CDB53B',
    spotCLPg:7,
    precioLocal:17,
    fuente:'Mercado local', fechaActualizacion:'2026-04-29',
    tiers:{ bolsa:{v:7,label:'Spot bolsa'}, calle:{v:10,label:'Precio calle'}, mayorista:{v:13,label:'Mayorista'}, local:{v:17,label:'Mercado local'}, premium:{v:24,label:'Artesanal'} },
    nota:'Latón (Cu+Zn) artesanal Chile',
  },
};
const INDICADORES = {
  dolar: { valor:895.07,   variacion:-0.4 },  // SII 27-abr-2026
  uf:    { valor:40120.20, variacion:+0.1 },  // SII 30-abr-2026
};

const METALES     = ['Plata','Oro','Cobre','Latón','Platino','Acero','Mixto'];
const COLECCIONES = [];
const MEDIOS_PAGO = [
  { id:'efectivo',      label:'Efectivo',      icon:'💵' },
  { id:'debito',        label:'Débito',         icon:'💳' },
  { id:'credito',       label:'Crédito',        icon:'💳' },
  { id:'transferencia', label:'Transferencia',  icon:'🏦' },
  { id:'mercadopago',   label:'Mercado Pago',   icon:'🔵' },
  { id:'mixto',         label:'Mixto',          icon:'⚖️' },
];
const TECNICOS = ['Don Pedro','Valentina T.','Juan C.'];

const CATS_MAESTRAS = [
  'ANILLO','ARGOLLA','ALIANZA','AROS','AROS LARGOS','AROS TOPE',
  'DIJE COLGADOR','COLGADOR','CHARM','RELICARIO','MEDALLA','CRUZ','LLAMADOR DE ÁNGEL',
  'CADENA','COLLAR','GARGANTILLA','PULSERA','BRAZALETE','ESCLAVA','TOBILLERA',
  'PRENDEDOR','BROCHE','PIN','ROSARIO','TIARA','CONJUNTO','PACK',
  'ACCESORIO','SERVICIO TALLER','OTRO'
];
const METALES_MAESTROS = [
  {code:'AG',name:'Plata'},  {code:'AU',name:'Oro'},    {code:'CU',name:'Cobre'},
  {code:'LT',name:'Latón'},  {code:'PT',name:'Platino'},{code:'AC',name:'Acero'},
  {code:'PD',name:'Paladio'},{code:'TI',name:'Titanio'},{code:'MIX',name:'Mixto'},
];
const PIEDRAS_MAESTRAS = [
  'Circón','Circones blancos','Circones de color','Piedra reconstituida',
  'Lapislázuli','Lapislázuli reconstituido','Malaquita','Turquesa',
  'Ojo de tigre','Aventurina','Cuarzo rosa','Amatista','Ónix','Perla',
  'Nácar','Esmeralda','Rubí','Zafiro','Cornalina','Pirita','Hematita',
  'Jade','Ámbar','Coral','Jaspe','Resina','Esmalte','Vidrio','Cristal',
  'Madera','Cuero','Cordón','Sin piedra','Otro'
];
const ESTADOS_PROD = ['En vitrina','En bodega','Reservado','Vendido','En reparación','Baja'];

// ── Cuentas de usuario ────────────────────────────────
const USUARIOS_DEFAULT = [
  { id:'admin',     nombre:'Administrador', usuario:'admin',    clave:'navarro2026', rol:'admin',    tienda:'navarro', color:'#c9a84c' },
  { id:'vendedor1', nombre:'Vendedor/a',    usuario:'vendedor', clave:'venta123',    rol:'vendedor', tienda:'navarro', color:'#2980b9' },
  { id:'taller1',   nombre:'Taller',        usuario:'taller',   clave:'taller123',   rol:'taller',   tienda:'rubi',    color:'#9b6b6b' },
];

// ── Supabase Cloud Sync — datos accesibles desde cualquier dispositivo ──
// Tabla: kv_store (id text PK, data jsonb, updated_at timestamptz)
const SB_URL = 'https://lyhngnbapewndrdjzcmr.supabase.co';
const SB_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imx5aG5nbmJhcGV3bmRyZGp6Y21yIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODA5MDY3NjEsImV4cCI6MjA5NjQ4Mjc2MX0.TzgOnPHjeMEEvxnok_ILYNutwF7UrHzjRQv8mkNb81w';
const SB_HEADERS = { 'apikey': SB_KEY, 'Authorization': 'Bearer '+SB_KEY, 'Content-Type': 'application/json' };
const SB_MAX_BYTES = 1500000; // saltar payloads gigantes (fotos en base64) — esos quedan solo locales
const SB_FOTOS_BUCKET = 'fotos-productos';

// Convierte un dataURL base64 (ej: foto de producto) en un archivo subido a Supabase Storage,
// y devuelve la URL pública (string corto que sí cabe en kv_store y se sincroniza normal).
async function sbSubirFoto(id, dataUrl) {
  try {
    if (!dataUrl || !dataUrl.startsWith('data:')) return dataUrl || null;
    const res = await fetch(dataUrl);
    const blob = await res.blob();
    const ext = (blob.type.split('/')[1] || 'jpg').replace('jpeg','jpg');
    const path = 'producto-' + id + '.' + ext;
    const up = await fetch(SB_URL+'/storage/v1/object/'+SB_FOTOS_BUCKET+'/'+path+'?upsert=true', {
      method: 'POST',
      headers: { 'apikey': SB_KEY, 'Authorization': 'Bearer '+SB_KEY, 'Content-Type': blob.type, 'x-upsert': 'true' },
      body: blob
    });
    if (!up.ok) return dataUrl; // si falla la subida, se queda con el base64 local (no se pierde la foto)
    return SB_URL+'/storage/v1/object/public/'+SB_FOTOS_BUCKET+'/'+path+'?t='+Date.now();
  } catch { return dataUrl || null; }
}

// Claves que NO se sincronizan a la nube — son específicas de cada dispositivo
// (sesión, configuración local, bitácora de seguridad, layout de etiquetas)
const SB_KEYS_LOCALES = new Set([
  'navarro_session', 'navarro_login_intentos', 'navarro_login_bitacora',
  'navarro_godex_label_layout', 'navarro_godex_barcode_type',
  'navarro_ajustes_impresoras', 'navarro_ajustes_lector',
  'navarro_ajustes_ticket', 'navarro_ajustes_doc',
]);

// ── Timestamps de sincronización — guardados fuera del prefijo navarro_ para no sincronizarse ──
// Registra cuándo fue el último push exitoso por clave.
// Esto evita que sbPullAll sobreescriba datos locales más nuevos que los de la nube.
const SB_TS_KEY = '__sb_push_ts'; // clave en localStorage — no tiene prefijo navarro_, no se sincroniza
function getSbTs() { try { return JSON.parse(localStorage.getItem(SB_TS_KEY)||'{}'); } catch { return {}; } }
function setSbTs(fullKey, ts) {
  try { const t=getSbTs(); t[fullKey]=ts; localStorage.setItem(SB_TS_KEY, JSON.stringify(t)); } catch {}
}

// Sube una clave al store en la nube y registra el timestamp del push
function sbPush(fullKey, val) {
  try {
    if (SB_KEYS_LOCALES.has(fullKey)) return;
    const json = JSON.stringify(val);
    if (!json || json.length > SB_MAX_BYTES) return;
    const nowIso = new Date().toISOString();
    const nowMs  = Date.now();
    setSbTs(fullKey, nowMs); // registrar intento de push — protege datos locales en el pull siguiente
    if (window.__setSyncStatus) window.__setSyncStatus('syncing');
    fetch(SB_URL+'/rest/v1/kv_store?on_conflict=id', {
      method: 'POST',
      headers: { ...SB_HEADERS, 'Prefer': 'resolution=merge-duplicates,return=minimal' },
      body: JSON.stringify({ id: fullKey, data: val, updated_at: nowIso })
    }).then(r => {
      if (r.ok) {
        if (window.__setSyncStatus) window.__setSyncStatus('ok');
      } else {
        if (window.__setSyncStatus) window.__setSyncStatus('error');
        const t=getSbTs(); delete t[fullKey]; localStorage.setItem(SB_TS_KEY, JSON.stringify(t));
      }
    }).catch(()=>{
      if (window.__setSyncStatus) window.__setSyncStatus('error');
      const t=getSbTs(); delete t[fullKey]; localStorage.setItem(SB_TS_KEY, JSON.stringify(t));
    });
  } catch {}
}

// Descarga desde la nube — SOLO sobreescribe local si la nube es más nueva que nuestro último push
// Esto evita perder productos recién creados que aún no llegaron a la nube
async function sbPullAll() {
  try {
    const res = await fetch(SB_URL+'/rest/v1/kv_store?select=id,data,updated_at', { headers: SB_HEADERS });
    if (!res.ok) return false;
    const rows = await res.json();
    const localTs = getSbTs();
    let changed = false;
    for (const row of rows) {
      if (!row.id || !row.id.startsWith('navarro_')) continue;
      if (SB_KEYS_LOCALES.has(row.id)) continue;
      const cloudMs = new Date(row.updated_at).getTime();
      const lastPushMs = localTs[row.id] || 0;
      // Solo actualizar local si la nube tiene datos más nuevos que nuestro último push
      if (cloudMs <= lastPushMs) continue; // local es más reciente — no tocar
      const incoming = JSON.stringify(row.data);
      const current  = localStorage.getItem(row.id);
      if (incoming !== current) {
        localStorage.setItem(row.id, incoming);
        setSbTs(row.id, cloudMs); // registrar que ahora estamos sincronizados con esta versión
        changed = true;
      }
    }
    return changed;
  } catch { return false; }
}

// Migración inicial: sube TODOS los datos locales actuales a la nube (un solo uso, botón en Ajustes)
async function sbPushAll() {
  let count = 0, omitted = 0;
  for (let i=0; i<localStorage.length; i++) {
    const k = localStorage.key(i);
    if (!k || !k.startsWith('navarro_')) continue;
    try {
      const raw = localStorage.getItem(k);
      if (!raw || raw.length > SB_MAX_BYTES) { omitted++; continue; }
      const val = JSON.parse(raw);
      const nowIso = new Date().toISOString();
      const r = await fetch(SB_URL+'/rest/v1/kv_store?on_conflict=id', {
        method: 'POST',
        headers: { ...SB_HEADERS, 'Prefer': 'resolution=merge-duplicates,return=minimal' },
        body: JSON.stringify({ id: k, data: val, updated_at: nowIso })
      });
      if (r.ok) { setSbTs(k, Date.now()); count++; } else omitted++;
    } catch { omitted++; }
  }
  return { count, omitted };
}

// ── localStorage helpers (con sincronización automática a la nube) ───
function lsGet(key, def) {
  try { const v=localStorage.getItem('navarro_'+key); return v?JSON.parse(v):def; } catch { return def; }
}
function lsSet(key, val) {
  try { localStorage.setItem('navarro_'+key, JSON.stringify(val)); } catch {}
  sbPush('navarro_'+key, val);
}

// ── Helpers globales ──────────────────────────────────
function clp(n) { return '$'+Math.round(n||0).toLocaleString('es-CL'); }
function desglosarIVA(total) { const neto=Math.round(total/1.19); return {neto,iva:total-neto}; }
function hoy() { return new Date().toISOString().slice(0,10); }

function generarSKU(metal_code, categoria, piedra, inventario) {
  const catMap={'ANILLO':'AN','ARGOLLA':'AL','ALIANZA':'AL','AROS':'AR','AROS LARGOS':'ARL','AROS TOPE':'ART','DIJE COLGADOR':'DJ','COLGADOR':'DJ','CHARM':'DJ','RELICARIO':'DJ','MEDALLA':'DJ','CRUZ':'DJ','LLAMADOR DE ÁNGEL':'DJ','CADENA':'CA','COLLAR':'CO','GARGANTILLA':'CO','PULSERA':'PU','BRAZALETE':'BR','ESCLAVA':'ES','TOBILLERA':'TO','PRENDEDOR':'PR','BROCHE':'PR','ROSARIO':'RS','TIARA':'TI','CONJUNTO':'CJ','PACK':'PK','ACCESORIO':'AC','SERVICIO TALLER':'ST'};
  const pieMap={'Circón':'CI','Circones blancos':'CI','Lapislázuli':'LA','Lapislázuli reconstituido':'LA','Malaquita':'MA','Turquesa':'TU','Ojo de tigre':'OT','Piedra reconstituida':'PR','Cuarzo rosa':'CU','Amatista':'AM','Ónix':'ON','Perla':'PE','Esmeralda':'ES','Rubí':'RU','Zafiro':'ZA','Aventurina':'AV','Cornalina':'CO','Madera':'WO'};
  const m=(metal_code||'AG').toUpperCase();
  const c=catMap[categoria]||categoria.slice(0,2).toUpperCase();
  const p=pieMap[piedra]||'';
  const prefix=m+c+p;
  const existing=(inventario||[]).filter(x=>x.sku&&x.sku.toUpperCase().startsWith(prefix)).map(x=>parseInt(x.sku.replace(new RegExp('^'+prefix,'i'),''))||0);
  const next=existing.length>0?Math.max(...existing)+1:1;
  return prefix+String(next).padStart(4,'0');
}

function estadoBadge(estado) {
  const map={'Recibido':{bg:'rgba(41,128,185,0.15)',color:'#2980b9'},'En proceso':{bg:'rgba(243,156,18,0.15)',color:'#f39c12'},'Listo':{bg:'rgba(39,174,96,0.15)',color:'#27ae60'},'Entregado':{bg:'rgba(100,100,100,0.15)',color:'var(--cream-3)'},'Cancelado':{bg:'rgba(192,57,43,0.15)',color:'#c0392b'}};
  const s=map[estado]||{bg:'var(--surface)',color:'var(--cream-3)'};
  return {display:'inline-block',padding:'2px 8px',borderRadius:'20px',fontSize:'10px',fontWeight:600,background:s.bg,color:s.color};
}

// ── Configuración de impresoras ───────────────────────
const PRINT_CONFIG = {
  ticket: {
    nombre: 'SPRT SP-POS88',
    anchoPapel: '80mm',
    instruccion: 'Impresora de tickets: SPRT SP-POS88 (papel 80mm)',
  },
  etiqueta: {
    nombre: 'Godex GE300',
    instruccion: 'Impresora de etiquetas: Godex GE300',
    ancho: 55, alto: 12,
  },
  documentos: {
    nombre: 'Epson L3250',
    instruccion: 'Impresora de documentos: Epson L3250 (A4/Carta)',
    formato: 'A4',
  },
};

// Obtener logo guardado
function getLogo() {
  return localStorage.getItem('navarro_logo') || null;
}

// ── Layout por defecto para etiqueta Godex GE300 ─────────────────────────────
// x, y  → posición como % del ancho/alto de la etiqueta (centro del elemento)
// size  → tamaño de fuente como % del alto de la etiqueta
// w, h  → ancho/alto del código de barras como % de la etiqueta
// ── Layout etiqueta — v4 ─────────────────────────────────────────────────────
// Coordenadas en % del canvas (55mm×12mm).
// DISEÑO: barcode compacto (misma área pequeña), solo últimos 3 chars del SKU.
// Menos barras en igual espacio → cada barra más ancha → escaneable con lector.
// El SKU completo sigue visible como texto bajo el código.
const LABEL_V = 6;
// Layout v6: izquierda = precio + SKU texto, derecha = Data Matrix
// x,y en % del canvas. precio centrado horizontalmente en zona izq.
const LABEL_DEFAULT = {
  precio:  { x: 18, y: 42, size: 42, visible: true },
  nombre:  { x: 18, y: 75, size: 18, visible: false },
  barcode: { x: 67, y: 4,  w: 34,   h: 88, dmSize: 10.5, visible: true },
  sku:     { x: 18, y: 88, size: 14, visible: true },
};
function getLabelLayout() {
  try {
    const stored = JSON.parse(localStorage.getItem('godex_label_layout') || 'null') || {};
    // Si el layout guardado es de una versión anterior, ignorar y usar defaults
    if (!stored._v || stored._v < LABEL_V) return { ...LABEL_DEFAULT };
    const { _v, ...rest } = stored;
    return Object.assign({}, LABEL_DEFAULT, rest);
  } catch(e) { return { ...LABEL_DEFAULT }; }
}
function saveLabelLayout(layout) {
  localStorage.setItem('godex_label_layout', JSON.stringify({ ...layout, _v: LABEL_V }));
}

// ── Tipo de código de barras ('datamatrix' | 'code128' | 'qr') ──────────────
function getBarcodeType() {
  try { return localStorage.getItem('godex_barcode_type') || 'datamatrix'; }
  catch(e) { return 'datamatrix'; }
}
function setBarcodeType(t) {
  try { localStorage.setItem('godex_barcode_type', t); } catch(e) {}
}

// ── Code128 Set B — generador puro JS ────────────────────────────────────────
// Encodes full ASCII string. Returns SVG rect string.
function code128Rects(text, xOff, yOff, bW, bH) {
  // Code128 Set B bar patterns (11 modules each)
  const C128B = [
    '11011001100','11001101100','11001100110','10010011000','10010001100','10001001100',
    '10011001000','10011000100','10001100100','11001001000','11001000100','11000100100',
    '10110011100','10011011100','10011001110','10111001100','10011101100','10011100110',
    '11001110010','11001011100','11001001110','11011100100','11001110100','11101101110',
    '11101001100','11100101100','11100100110','11101100100','11100110100','11100110010',
    '11011011000','11011000110','11000110110','10100011000','10001011000','10001000110',
    '10110001000','10001101000','10001100010','11010001000','11000101000','11000100010',
    '10110111000','10110001110','10001101110','10111011000','10111000110','10001110110',
    '11101110110','10111001110','11101011000','11101000110','11100010110','11101101000',
    '11101100010','11100011010','11101111010','11001000010','11110001010','10100110000',
    '10100001100','10010110000','10010000110','10000101100','10000100110','10110010000',
    '10110000100','10011010000','10011000010','10000110100','10000110010','11000010010',
    '11001010000','11110111010','11000010100','10001111010','10100111100','10010111100',
    '10010011110','10111100100','10011110100','10011110010','11110100100','11110010100',
    '11110010010','11011011110','11011110110','11110110110','10101111000','10100011110',
    '10001011110','10111101000','10111100010','11110101000','11110100010','10111011110',
    '10111101110','11101011110','11110101110','11010000100','11010010000','11010011100',
    '11000111010', // START B = 104
  ];
  const START_B = 104;
  const STOP    = '1100011101011';
  // Build code array
  const codes = [START_B];
  let checksum = START_B;
  const chars = text.toUpperCase().replace(/[^\x20-\x7E]/g,'');
  for (let i = 0; i < chars.length; i++) {
    const v = chars.charCodeAt(i) - 32; // Set B: space=0 … ~=94
    codes.push(v);
    checksum += v * (i + 1);
  }
  codes.push(checksum % 103);
  // Build bar string
  let barStr = '';
  for (const c of codes) barStr += C128B[c] || C128B[0];
  barStr += STOP;
  // Render
  const moduleW = bW / barStr.length;
  let x = xOff, out = '';
  for (const bit of barStr) {
    if (bit === '1') out += `<rect x="${x.toFixed(2)}" y="${yOff.toFixed(2)}" width="${moduleW.toFixed(2)}" height="${bH.toFixed(2)}" fill="#000"/>`;
    x += moduleW;
  }
  return out;
}

// ── QR Code — generador puro JS (versiones 1-4, nivel L) ─────────────────────
function qrGenerate(text) {
  // Reed-Solomon GF(256) with generator poly for QR
  const gf_exp = new Uint8Array(512);
  const gf_log = new Uint8Array(256);
  let x = 1;
  for (let i = 0; i < 255; i++) {
    gf_exp[i] = x; gf_log[x] = i;
    x = x << 1; if (x & 0x100) x ^= 0x11d;
  }
  for (let i = 255; i < 512; i++) gf_exp[i] = gf_exp[i - 255];
  function gfMul(a, b) { return (a && b) ? gf_exp[(gf_log[a]+gf_log[b])%255] : 0; }
  function gfPoly(ec) {
    let g = [1];
    for (let i = 0; i < ec; i++) {
      const t = [1, gf_exp[i]];
      const ng = new Array(g.length + t.length - 1).fill(0);
      for (let j = 0; j < g.length; j++) for (let k = 0; k < t.length; k++) ng[j+k] ^= gfMul(g[j],t[k]);
      g = ng;
    }
    return g;
  }
  function rsEncode(data, ec) {
    const gen = gfPoly(ec);
    let r = [...data];
    for (let i = 0; i < ec; i++) r.push(0);
    for (let i = 0; i < data.length; i++) {
      const c = r[i];
      if (c) for (let j = 0; j < gen.length; j++) r[i+j] ^= gfMul(gen[j],c);
    }
    return r.slice(data.length);
  }
  // Version table [version]: [modules, ecCodewords, dataCodewords]
  const VT = [null,
    [21,  7, 19], [25, 10, 34], [29, 15, 55], [33, 20, 80],
    [37, 26,108], [41, 36,136],
  ];
  // Encode bytes
  const bytes = [];
  for (let i = 0; i < text.length; i++) bytes.push(text.charCodeAt(i) & 0xff);
  // Pick version
  let ver = 1;
  for (; ver <= 6; ver++) { if (VT[ver][2] >= bytes.length + 3) break; }
  if (ver > 6) ver = 6;
  const [size, ecN, dataN] = VT[ver];
  // Build data codewords
  const data = [];
  // Mode indicator: byte=0100 (4 bits), char count (8 bits)
  const header = (0x4 << 12) | (bytes.length << 4); // 16 bits, shift into stream
  // Use bit stream
  const bits = [];
  function pushBits(v, n) { for (let i = n-1; i >= 0; i--) bits.push((v>>i)&1); }
  pushBits(4, 4);           // byte mode
  pushBits(bytes.length, 8);
  for (const b of bytes) pushBits(b, 8);
  pushBits(0, 4);            // terminator
  while (bits.length % 8) bits.push(0);
  const cws = [];
  for (let i = 0; i < bits.length; i+=8) {
    let v = 0; for (let j = 0; j < 8; j++) v = (v<<1)|(bits[i+j]||0);
    cws.push(v);
  }
  const pads = [0xEC, 0x11];
  while (cws.length < dataN) cws.push(pads[(cws.length - (bits.length>>3)) % 2]);
  const ec = rsEncode(cws, ecN);
  const stream = [...cws, ...ec];
  // Build matrix
  const N = size;
  const M = Array.from({length:N}, () => new Int8Array(N).fill(-1));
  const F = Array.from({length:N}, () => new Uint8Array(N)); // function pattern mask
  function setModule(r,c,v,fn=false) { if(r>=0&&r<N&&c>=0&&c<N){M[r][c]=v;if(fn)F[r][c]=1;} }
  // Finder pattern
  function finder(r,c) {
    for (let dr=-1;dr<=7;dr++) for (let dc=-1;dc<=7;dc++) {
      const v=(dr>=0&&dr<=6&&dc>=0&&dc<=6)&&
              ((dr===0||dr===6||dc===0||dc===6)||( dr>=2&&dr<=4&&dc>=2&&dc<=4))?1:0;
      setModule(r+dr,c+dc,v,true);
    }
  }
  finder(0,0); finder(0,N-7); finder(N-7,0);
  // Timing
  for (let i=8;i<N-8;i++){setModule(6,i,(i+1)%2,true);setModule(i,6,(i+1)%2,true);}
  // Dark module
  setModule(4*ver+9,8,1,true);
  // Format info placeholder (filled after mask)
  const fmtPos=[[8,0],[8,1],[8,2],[8,3],[8,4],[8,5],[8,7],[8,8],
                [7,8],[5,8],[4,8],[3,8],[2,8],[1,8],[0,8]];
  for (const [r,c] of fmtPos){setModule(r,c,0,true);setModule(N-1-r,N-1-c,0,true);}
  // Data placement
  let bit=0; const streamBits=[];
  for (const cw of stream) for (let i=7;i>=0;i--) streamBits.push((cw>>i)&1);
  let up=true;
  for (let col=N-1;col>=0;col-=2) {
    if(col===6)col--;
    for (let row=0;row<N;row++) {
      const r=up?N-1-row:row;
      for (let k=0;k<2;k++){
        const c=col-k;
        if(!F[r][c]){M[r][c]=streamBits[bit++]||0;}
      }
    }
    up=!up;
  }
  // Apply mask 0: (r+c)%2==0
  for(let r=0;r<N;r++) for(let c=0;c<N;c++) if(!F[r][c]&&(r+c)%2===0) M[r][c]^=1;
  // Format string for EC=L(01), mask=0(000): 010 000 + 15-bit BCH
  const fmt=0b101010000010010; // pre-computed for L,mask0
  const fmtBits=[];for(let i=14;i>=0;i--)fmtBits.push((fmt>>i)&1);
  const fmtXOR=[1,0,1,0,1,0,0,0,0,0,1,0,0,1,0];
  const fb=fmtBits.map((b,i)=>b^fmtXOR[i]);
  for(let i=0;i<15;i++){
    const [r,c]=fmtPos[i];
    setModule(r,c,fb[i],true);setModule(N-1-r,N-1-c,fb[i],true);
  }
  return {matrix:M, size:N};
}

function qrRects(text, xOff, yOff, qSize) {
  const {matrix, size} = qrGenerate(text);
  const mod = qSize / size;
  let out = '';
  for (let r = 0; r < size; r++) {
    for (let c = 0; c < size; c++) {
      if (matrix[r][c] === 1) {
        out += `<rect x="${(xOff + c*mod).toFixed(2)}" y="${(yOff + r*mod).toFixed(2)}" width="${(mod+0.5).toFixed(2)}" height="${(mod+0.5).toFixed(2)}" fill="#000"/>`;
      }
    }
  }
  return out;
}

// ── Data Matrix ECC200 — generador puro JS ───────────────────────────────────
// Implementa ISO 16022 (ECC200). Soporta símbolos cuadrados 10×10 a 26×26.
// Encoda el SKU completo en ASCII con doble dígito para mayor compacidad.
function datamatrixGenerate(text) {
  // GF(256) con polinomio primitivo 0x12D (Data Matrix spec)
  const EXP = new Uint8Array(512), LOG = new Uint8Array(256);
  let _v = 1;
  for (let i = 0; i < 255; i++) {
    EXP[i] = _v; LOG[_v] = i;
    _v = (_v << 1) ^ (_v & 0x80 ? 0x12D : 0);
  }
  for (let i = 255; i < 512; i++) EXP[i] = EXP[i-255];
  const gm = (a, b) => (!a || !b) ? 0 : EXP[LOG[a]+LOG[b]];

  function rsEncode(data, ecLen) {
    let g = [1];
    for (let i = 0; i < ecLen; i++) {
      const ng = new Array(g.length+1).fill(0);
      for (let j = 0; j < g.length; j++) { ng[j] ^= g[j]; ng[j+1] ^= gm(g[j], EXP[i]); }
      g = ng;
    }
    const buf = [...data, ...new Array(ecLen).fill(0)];
    for (let i = 0; i < data.length; i++) {
      const c = buf[i]; if (!c) continue;
      for (let j = 0; j < g.length; j++) buf[i+j] ^= gm(g[j], c);
    }
    return buf.slice(data.length);
  }

  // Tabla de símbolos cuadrados: [rows, cols, dataCW, eccCW]
  const SYMS = [
    [10,10,3,5],[12,12,5,7],[14,14,8,10],[16,16,12,12],
    [18,18,18,14],[20,20,22,18],[22,22,30,20],[24,24,36,24],[26,26,44,28],
  ];

  // Codificación ASCII con doble-dígito
  const cws = [];
  for (let i = 0; i < text.length;) {
    const a = text.charCodeAt(i);
    if (a >= 48 && a <= 57 && i+1 < text.length) {
      const b = text.charCodeAt(i+1);
      if (b >= 48 && b <= 57) { cws.push(130+(a-48)*10+(b-48)); i+=2; continue; }
    }
    cws.push((a & 0x7F) + 1); i++;
  }

  // Seleccionar símbolo mínimo que quepa (data + EOD)
  let sym = SYMS[SYMS.length-1];
  for (const s of SYMS) { if (s[2] >= cws.length+1) { sym = s; break; } }
  const [NR, NC, DCW, ECW] = sym;

  // EOD + relleno aleatorizado
  cws.push(129);
  for (let n=1; cws.length < DCW; n++) {
    let p = 130 + ((n * 149) % 253);
    if (p > 254) p -= 254;
    cws.push(p);
  }

  const allCW = [...cws.slice(0, DCW), ...rsEncode(cws.slice(0, DCW), ECW)];

  // Construir matriz (-1 = sin asignar)
  const M = Array.from({length:NR}, () => new Array(NC).fill(-1));

  // Bordes: izquierda y abajo sólidos (finder L-shape), arriba y derecha alternados (timing)
  for (let i = 0; i < NR; i++) {
    M[i][0]    = 1;                  // col izquierda: todo oscuro
    M[i][NC-1] = (i & 1) ? 1 : 0;  // col derecha: alternado oscuro en impares
  }
  for (let i = 0; i < NC; i++) {
    M[NR-1][i] = 1;                  // fila inferior: todo oscuro
    M[0][i]    = (i & 1) ? 0 : 1;  // fila superior: alternado oscuro en pares
  }

  // Función de colocación con wrap
  function set(r, c, cw, b) {
    if (r < 0) r += NR; if (c < 0) c += NC;
    if (r >= 0 && r < NR && c >= 0 && c < NC && M[r][c] < 0) M[r][c] = (cw >> b) & 1;
  }
  // Utah: esquina inferior-derecha en (r,c)
  function utah(r, c, cw) {
    set(r-2,c-2,cw,7); set(r-2,c-1,cw,6);
    set(r-1,c-2,cw,5); set(r-1,c-1,cw,4); set(r-1,c,cw,3);
    set(r,c-2,cw,2);   set(r,c-1,cw,1);   set(r,c,cw,0);
  }
  // Esquinas especiales para bordes
  function ca1(cw) {
    set(NR-1,0,cw,7); set(NR-1,1,cw,6); set(NR-1,2,cw,5);
    set(0,NC-2,cw,4); set(0,NC-1,cw,3);
    set(1,NC-1,cw,2); set(2,NC-1,cw,1); set(3,NC-1,cw,0);
  }
  function ca2(cw) {
    set(NR-3,0,cw,7); set(NR-2,0,cw,6); set(NR-1,0,cw,5);
    set(0,NC-4,cw,4); set(0,NC-3,cw,3); set(0,NC-2,cw,2); set(0,NC-1,cw,1);
    set(1,NC-1,cw,0);
  }
  function ca3(cw) {
    set(NR-3,0,cw,7); set(NR-2,0,cw,6); set(NR-1,0,cw,5);
    set(0,NC-2,cw,4); set(0,NC-1,cw,3);
    set(1,NC-1,cw,2); set(2,NC-1,cw,1); set(3,NC-1,cw,0);
  }
  function ca4(cw) {
    set(NR-1,0,cw,7); set(NR-1,NC-1,cw,6);
    set(0,NC-3,cw,5); set(0,NC-2,cw,4); set(0,NC-1,cw,3);
    set(1,NC-3,cw,2); set(1,NC-2,cw,1); set(1,NC-1,cw,0);
  }

  // Recorrido serpentino: diagonal arriba-derecha, luego abajo-izquierda
  // IMPORTANTE: verificar TODOS los bounds antes de acceder a M[r][c]
  const inBounds = (r, c) => r >= 0 && r < NR && c >= 0 && c < NC;
  let ci = 0, r = 4, c = 0;
  do {
    if (r === NR   && c === 0)                 ca1(allCW[ci++]);
    if (r === NR-2 && c === 0 && NC%4 !== 0)  ca2(allCW[ci++]);
    if (r === NR-2 && c === 0 && NC%8 === 4)  ca3(allCW[ci++]);
    if (r === NR+4 && c === 2 && NC%8 === 0)  ca4(allCW[ci++]);
    do {
      if (inBounds(r, c) && M[r][c] < 0) utah(r, c, allCW[ci++]);
      r -= 2; c += 2;
    } while (r >= 0 && c < NC);
    r += 1; c += 3;
    do {
      if (inBounds(r, c) && M[r][c] < 0) utah(r, c, allCW[ci++]);
      r += 2; c -= 2;
    } while (r < NR && c >= 0);
    r += 3; c += 1;
  } while (r < NR || c < NC);

  // Rellenar celdas sin asignar con módulos de alineación
  for (let r = 0; r < NR; r++)
    for (let c = 0; c < NC; c++)
      if (M[r][c] < 0) M[r][c] = (r+c)%2 === 0 ? 1 : 0;

  return { matrix: M, rows: NR, cols: NC };
}

// Fallback interno (usado si bwip-js no está disponible)
function _datamatrixRectsFallback(text, xOff, yOff, size) {
  const { matrix, rows, cols } = datamatrixGenerate(text);
  const mod = size / Math.max(rows, cols);
  const qz  = mod * 1.5;
  let out = `<rect x="${(xOff-qz).toFixed(2)}" y="${(yOff-qz).toFixed(2)}" width="${(size+qz*2).toFixed(2)}" height="${(size+qz*2).toFixed(2)}" fill="white"/>`;
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      if (matrix[r][c] === 1) {
        out += `<rect x="${(xOff+c*mod).toFixed(2)}" y="${(yOff+r*mod).toFixed(2)}" width="${(mod+0.5).toFixed(2)}" height="${(mod+0.5).toFixed(2)}" fill="#000"/>`;
      }
    }
  }
  return out;
}

// ── datamatrixRects — usa bwip-js (certificado) con fallback interno ──────────
// bwip-js garantiza ECC200 correcto (ISO 16022). Se carga offline desde el bundle.
// IMPORTANTE: renderizamos como SVG <rect> (NO como PNG embebido) para evitar
// blur por escalado. bwip-js genera la matriz correcta a scale=1 (1px/módulo),
// leemos los pixels y generamos un rect por módulo → nitidez perfecta a cualquier escala.
function datamatrixRects(text, xOff, yOff, size) {
  try {
    if (typeof bwipjs !== 'undefined') {
      const canvas = document.createElement('canvas');
      bwipjs.toCanvas(canvas, {
        bcid:        'datamatrix',
        text:        text,
        scale:       1,   // 1px = 1 módulo → leemos pixel a pixel
        padding:     0,   // sin padding propio; nosotros controlamos quiet zone
        includetext: false,
      });
      const ctx = canvas.getContext('2d');
      const cw = canvas.width;
      const ch = canvas.height;
      const id = ctx.getImageData(0, 0, cw, ch);
      const d  = id.data;
      // Tamaño de cada módulo — redondeado a entero para coordenadas pixel-perfectas
      // CRÍTICO: si size y cw son ambos múltiplos → modW es entero exacto → cero antialiasing
      // Con dmSize=10.5mm → size=84px, cw=14 → modW=6px exacto (= GoLabel moduleSize=6)
      const modW = Math.round(size / cw);
      const modH = Math.round(size / ch);
      const actualW = modW * cw;  // tamaño real (puede diferir levemente de size)
      const actualH = modH * ch;
      // Recentrar si actualW difiere de size
      const ox = Math.round(xOff + (size - actualW) / 2);
      const oy = Math.round(yOff + (size - actualH) / 2);
      // Quiet zone: 2 módulos cada lado (ISO 16022)
      const qzW = modW * 2;
      const qzH = modH * 2;
      const parts = [];
      // Fondo blanco con quiet zone incluida — coordenadas enteras
      parts.push(`<rect x="${ox-qzW}" y="${oy-qzH}" width="${actualW+qzW*2}" height="${actualH+qzH*2}" fill="white"/>`);
      // Un <rect> por módulo negro — todas coordenadas enteras → pixel-perfect, cero blur
      for (let row = 0; row < ch; row++) {
        for (let col = 0; col < cw; col++) {
          const i = (row * cw + col) * 4;
          const lum = 0.299*d[i] + 0.587*d[i+1] + 0.114*d[i+2];
          if (lum < 128) {
            const rx = ox + col * modW;
            const ry = oy + row * modH;
            parts.push(`<rect x="${rx}" y="${ry}" width="${modW}" height="${modH}" fill="black"/>`);
          }
        }
      }
      return parts.join('');
    }
  } catch(e) { /* fallback interno */ }
  return _datamatrixRectsFallback(text, xOff, yOff, size);
}

// ── Etiqueta Godex GE300 — Formato OFICIAL 55×12mm ───────────────────────────
// Layout de 3 zonas (plantilla oficial Navarro Joyería):
//
//   ┌──────────┬───────────────┬──────────┐
//   │  20 mm   │    15 mm      │  20 mm   │
//   │  PRECIO  │  (sin impr.)  │ BARCODE  │
//   │  NOMBRE  │  zona doblez  │   SKU    │
//   └──────────┴───────────────┴──────────┘
//
// sku    → código del producto (Code39)
// precio → número CLP
// nombre → nombre del producto (hasta 2 líneas)
// opts   → { scale:4 } — escala del canvas
// Layout leído de localStorage['godex_label_layout'] vía getLabelLayout()
// Coordenadas x,y en % del canvas (0-100). size en % de altura. barcode w en % de ancho, h en % de alto.
function godexEtiquetaSVG(sku, precio, nombre, opts) {
  const sc  = (opts && opts.scale) || 4;
  const DPI = 203;
  const W   = Math.round(55 / 25.4 * DPI * sc);  // 440px × sc
  const H   = Math.round(12 / 25.4 * DPI * sc);  //  96px × sc

  const L = getLabelLayout();

  // Convertir % → px
  const px = v => v / 100 * W;
  const py = v => v / 100 * H;
  const ps = v => v / 100 * H;  // tamaño de fuente como % de alto

  // ── Barcode — dispatcher por tipo ────────────────────────────────────────
  const _btype = getBarcodeType();
  function barcodeRects(text, xOff, yOff, bW, bH) {
    if (_btype === 'datamatrix') {
      // Data Matrix ECC200: cuadrado, centrado en el área disponible
      const dmSizeMm = (L.barcode.dmSize) || 10.5;
      // Redondear a entero para evitar coordenadas fraccionales → antialiasing en canvas
      // 10.5mm = 84 dots → 84/14 módulos = 6.0 px/mod exacto (igual que GoLabel moduleSize=6)
      const dmSizePx  = Math.round(dmSizeMm / 25.4 * DPI * sc);
      const actualSize = Math.round(Math.min(dmSizePx, Math.round(bW), Math.round(bH)));
      const dmX = Math.round(xOff + (Math.round(bW) - actualSize) / 2);
      const dmY = Math.round(yOff + (Math.round(bH) - actualSize) / 2);
      return datamatrixRects(text, dmX, dmY, actualSize);
    }
    if (_btype === 'qr') {
      const qSize = Math.min(bW, bH);
      const qX = xOff + (bW - qSize) / 2;
      const qY = yOff + (bH - qSize) / 2;
      return qrRects(text, qX, qY, qSize);
    }
    // Code128 (1D)
    return code128Rects(text, xOff, yOff, bW, bH);
  }

  // ── Strings ──────────────────────────────────────────────────────────────
  const priceStr = '$' + (precio||0).toLocaleString('es-CL');
  const skuStr   = (sku||'SKU0000').toUpperCase();
  const nameStr  = ((nombre||'').toUpperCase()).trim();

  // ── Precio ───────────────────────────────────────────────────────────────
  const pFont = ps(L.precio.size);
  const pX    = px(L.precio.x);
  const pY    = py(L.precio.y);

  // ── Nombre — centrado en zona izquierda (0 → inicio barcode) ─────────────
  const nFont      = ps(L.nombre.size);
  const nX         = px(L.barcode.x) / 2;   // siempre centrado en zona izq
  const nY         = py(L.nombre.y);
  const nAvailW    = px(L.barcode.x);        // ancho total de la zona izquierda
  const maxChars   = Math.max(6, Math.floor(nAvailW / (nFont * 0.60)));
  const lineGap    = Math.round(2 * sc);
  function wrap(text, mx) {
    if (!text) return [];
    const words = text.split(' ');
    const lines = []; let cur = '';
    for (const w of words) {
      const test = cur ? cur + ' ' + w : w;
      if (cur && test.length > mx) { lines.push(cur); cur = w; }
      else cur = test;
    }
    if (cur) lines.push(cur);
    return lines.slice(0, 2);
  }
  const nameLines = wrap(nameStr, maxChars);

  // ── Barcode ───────────────────────────────────────────────────────────────
  const bcX = px(L.barcode.x);
  const bcY = py(L.barcode.y);
  const bcW = px(L.barcode.w);   // w es % del ancho total
  const bcH = py(L.barcode.h);   // h es % del alto total

  // ── SKU — centrado bajo el barcode (zona derecha) ────────────────────────
  const skuFont = ps(L.sku.size);
  const skuX    = px(L.barcode.x) + px(L.barcode.w) / 2;  // centro de zona barcode
  const skuY    = py(L.sku.y);

  return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
  <rect width="${W}" height="${H}" fill="white"/>
  ${L.precio.visible !== false ? `<text x="${pX.toFixed(1)}" y="${pY.toFixed(1)}" text-anchor="middle"
    font-family="Arial Black,Impact,sans-serif" font-weight="900"
    font-size="${pFont.toFixed(1)}" fill="#000">${priceStr}</text>` : ''}
  ${L.nombre.visible !== false ? nameLines.map((line, i) =>
    `<text x="${nX.toFixed(1)}" y="${(nY + i*(nFont+lineGap)).toFixed(1)}"
      text-anchor="middle" font-family="Arial Black,Impact,sans-serif" font-weight="900"
      font-size="${nFont.toFixed(1)}" fill="#000">${line}</text>`
  ).join('') : ''}
  ${L.barcode.visible !== false ? barcodeRects(skuStr, bcX, bcY, bcW, bcH) : ''}
  ${L.sku.visible !== false ? `<text x="${skuX.toFixed(1)}" y="${skuY.toFixed(1)}" text-anchor="middle"
    font-family="Courier New,monospace" font-size="${skuFont.toFixed(1)}" fill="#000" font-weight="700">${skuStr}</text>` : ''}
</svg>`;
}

// ── Popup HTML para Godex GE300 ─────────────────────────────────────────────
// Construido con array.join — sin template literals que procesen escapes.
// Script interno usa String.fromCharCode(10) como LF → cero problemas de escape.
// Recibe datos vía win.initPrinter(ezplBlocks) + DOM innerHTML tras document.close().
function _buildGodexPopup(count) {
  // ── Script del popup ─────────────────────────────────────────────────────
  // Reglas:
  //  - Sin escapes \n \uXXXX en literales de string → String.fromCharCode / chars directos
  //  - Sin datos dinámicos inline → recibidos vía window.initPrinter(blocks)
  //  - Una sola imagen de impresión por etiqueta (.etq-print), src se cambia al rotar
  //  - Web Serial API como canal EZPL (funciona en Windows con o sin driver)
  //  - WebUSB como canal alternativo (requiere driver CDC, no el driver de impresora)
  // Script del popup: primero TODAS las definiciones de función,
  // luego los listeners dentro de setTimeout para garantizar DOM listo.
  const S = [
    /* ── Variables globales ── */
    'var EZPL=[];',
    'var IMGS=[];',
    'var LF=String.fromCharCode(10);',
    'var _serial=null,_writer=null;',
    /* ── Helpers ── */
    'function G(id){return document.getElementById(id);}',
    'function st(cls,msg){',
    '  var d=G("dot"),m=G("msg");',
    '  if(d)d.className="dot"+(cls?" "+cls:"");',
    '  if(m&&msg!==undefined)m.innerHTML=msg;',
    '}',
    /* ── initPrinter: llamado desde el padre con los bloques EZPL ── */
    'window.initPrinter=function(blocks,imgs){',
    '  EZPL=blocks;',
    '  IMGS=imgs||[];',
    '  var p=G("ezpre");',
    '  if(p)p.textContent=blocks.join(LF+"---"+LF);',
    '};',
    /* ── Web Serial: Conectar ── */
    'async function serialConn(){',
    '  if(!navigator.serial){alert("Web Serial requiere Chrome o Edge.");return;}',
    '  if(_serial){st("on","Ya conectado.");return;}',
    '  st("","Abriendo selector de puerto...");',
    '  try{',
    '    var port=await navigator.serial.requestPort({filters:[]});',
    '    await port.open({baudRate:9600,dataBits:8,stopBits:1,parity:"none",flowControl:"none"});',
    '    _serial=port; _writer=port.writable.getWriter();',
    '    var info=port.getInfo?port.getInfo():{};',
    '    var lbl=info.usbProductId?"USB 0x"+info.usbProductId.toString(16).toUpperCase():"serial";',
    '    st("on","Conectado ("+lbl+") — listo para enviar EZPL");',
    '  }catch(e){',
    '    if(e.name==="NotFoundError"){st("","Cancelado");}',
    '    else{st("err","Error: "+e.message);}',
    '    _serial=null;_writer=null;',
    '  }',
    '}',
    /* ── Web Serial: Desconectar ── */
    'async function serialDisc(){',
    '  if(!_serial){st("","Ya desconectado");return;}',
    '  try{if(_writer){_writer.releaseLock();}_serial.close();}catch(e){}',
    '  _serial=null;_writer=null;',
    '  st("","Desconectado");',
    '}',
    /* ── Web Serial: Enviar string ── */
    'async function serialSend(str){',
    '  if(!_serial||!_writer){st("err","Primero conecta el puerto USB");return false;}',
    '  try{',
    '    await _writer.write(new TextEncoder().encode(str));',
    '    return true;',
    '  }catch(e){',
    '    st("err","Error envío: "+e.message);',
    '    try{_writer.releaseLock();}catch(ex){}',
    '    try{await _serial.close();}catch(ex){}',
    '    _serial=null;_writer=null;',
    '    return false;',
    '  }',
    '}',
    /* ── Enviar EZPL por serie (Web Serial) ── */
    'async function sendEzpl(){',
    '  if(!EZPL||!EZPL.length){st("err","Sin datos EZPL");return;}',
    '  st("","Enviando "+EZPL.length+" etiqueta(s)...");',
    '  var ok=await serialSend(EZPL.join(LF)+LF);',
    '  if(ok)st("on","OK — "+EZPL.length+" etiqueta(s) enviadas");',
    '}',
    /* ── Bridge EZPL: envío directo a impresora vía Python local ── */
    'var _BSCRIPT="IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwojIC0qLSBjb2Rpbmc6IHV0Zi04IC0qLQoiIiIKTmF2YXJybyBFUlAgLSBCcmlkZ2UgRVpQTCBSQVcgcGFyYSBHb0RFWCBHRTMwMApXaW5kb3dzIDcgLyBDaHJvbWUgLyBWZXJjZWwgYXBwIC0+IGxvY2FsaG9zdDo5MTkxIC0+IGltcHJlc29yYSBSQVcKCk5vIHVzYSBweXdpbjMyLiBObyB1c2EgbGlidXNiLiBObyBkZXBlbmRlIGRlIEdvTGFiZWwuClVzYSBXaW5TcG9vbCBSQVcgKFdyaXRlUHJpbnRlcikgcGFyYSBtYW5kYXIgRVpQTCBkaXJlY3RvIGFsIGRyaXZlciBkZSBXaW5kb3dzLgoKRW5kcG9pbnRzOgogIGh0dHA6Ly8xMjcuMC4wLjE6OTE5MS9waW5nCiAgaHR0cDovLzEyNy4wLjAuMTo5MTkxL3ByaW50ZXJzCiAgaHR0cDovLzEyNy4wLjAuMTo5MTkxL3Rlc3QKICBodHRwOi8vMTI3LjAuMC4xOjkxOTEvdGVzdC10ZXh0CiAgaHR0cDovLzEyNy4wLjAuMTo5MTkxL2xhYmVsP2NvZGU9Q1VBTkxBMDQmcHJpY2U9JDMwLjAwMAogIFBPU1QgaHR0cDovLzEyNy4wLjAuMTo5MTkxL3ByaW50ICAgICAgIC0+IEVaUEwgUkFXIGVuIGVsIGJvZHkKICBQT1NUIGh0dHA6Ly8xMjcuMC4wLjE6OTE5MS9wcmludC1sYWJlbCAtPiBKU09OOiB7ImNvZGUiOiJDVUFOTEEwNCIsInByaWNlIjoiJDMwLjAwMCJ9CiIiIgoKZnJvbSBfX2Z1dHVyZV9fIGltcG9ydCBwcmludF9mdW5jdGlvbgoKaW1wb3J0IGN0eXBlcwppbXBvcnQganNvbgppbXBvcnQgb3MKaW1wb3J0IHJlCmltcG9ydCBzdWJwcm9jZXNzCmltcG9ydCBzeXMKaW1wb3J0IHRpbWUKaW1wb3J0IHRyYWNlYmFjawpmcm9tIGN0eXBlcyBpbXBvcnQgd2ludHlwZXMKZnJvbSBodHRwLnNlcnZlciBpbXBvcnQgQmFzZUhUVFBSZXF1ZXN0SGFuZGxlciwgSFRUUFNlcnZlcgpmcm9tIHVybGxpYi5wYXJzZSBpbXBvcnQgdXJscGFyc2UsIHBhcnNlX3FzCgojID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0KIyBDT05GSUdVUkFDSU9OIFBSSU5DSVBBTAojID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0KClBPUlQgPSA5MTkxCgojIFNpIGVsIGJyaWRnZSBubyBlbmN1ZW50cmEgbGEgaW1wcmVzb3JhIGNvcnJlY3RhLCBlc2NyaWJlIGFxdWkgZWwgbm9tYnJlIEVYQUNUTwojIHF1ZSBhcGFyZWNlIGVuIFBhbmVsIGRlIGNvbnRyb2wgPiBEaXNwb3NpdGl2b3MgZSBpbXByZXNvcmFzLgojIEVqZW1wbG9zOgojIFBSSU5URVJfTkFNRV9NQU5VQUwgPSAiR29kZXggR0UzMDAiCiMgUFJJTlRFUl9OQU1FX01BTlVBTCA9ICJHb2RleCBHRTMwMCAoQ29waWFyIDEpIgpQUklOVEVSX05BTUVfTUFOVUFMID0gIiIKCiMgTm9tYnJlcyBwcmVmZXJpZG9zIGFsIGJ1c2NhciBhdXRvbWF0aWNhbWVudGUKUFJJTlRFUl9QUkVGRVJFTkNFUyA9IFsKICAgICJHb2RleCBHRTMwMCAoQ29waWFyIDEpIiwKICAgICJHb2RleCBHRTMwMCIsCiAgICAiR29ERVggR0UzMDAiLAogICAgIkdFMzAwIiwKICAgICJHb2RleCIsCiAgICAiR29ERVgiLApdCgojIEV0aXF1ZXRhIHRpcG8gaHVlc28vbWFyaXBvc2E6IDU1bW0geCAxMm1tIGNvbiBnYXAgM21tCkxBQkVMX1dJRFRIX01NID0gNTUKTEFCRUxfSEVJR0hUX01NID0gMTIKTEFCRUxfR0FQX01NID0gMwoKREFSS05FU1MgPSAxMCAgICAgICAjIF5IOiBvc2N1cmlkYWQuIFBydWViYSA4LCAxMCwgMTIgc2kgZmFsdGEgY29udHJhc3RlLgpTUEVFRCA9IDIgICAgICAgICAgIyBeUzogdmVsb2NpZGFkLiAyIGVzIGxlbnRvIHkgZXN0YWJsZSBwYXJhIERhdGFNYXRyaXguCkRBVEFNQVRSSVhfWCA9IDMwMCAjIHBvc2ljaW9uIGRlcmVjaGEgYXByb3gKREFUQU1BVFJJWF9ZID0gNgpEQVRBTUFUUklYX1NJWkUgPSA0ICAjIFhSQiBlbmxhcmdlLiA0IG8gNSBzdWVsZSBmdW5jaW9uYXIgYmllbiBlbiBldGlxdWV0YSAxMm1tLgoKTE9HX0ZJTEUgPSBvcy5wYXRoLmpvaW4ob3MucGF0aC5kaXJuYW1lKG9zLnBhdGguYWJzcGF0aChfX2ZpbGVfXykpLCAibmF2YXJyb19icmlkZ2UubG9nIikKCgojID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0KIyBVVElMSURBREVTCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQoKZGVmIGxvZyhtc2cpOgogICAgbGluZSA9IHRpbWUuc3RyZnRpbWUoIlslWS0lbS0lZCAlSDolTTolU10gIikgKyBzdHIobXNnKQogICAgcHJpbnQobGluZSkKICAgIHRyeToKICAgICAgICB3aXRoIG9wZW4oTE9HX0ZJTEUsICJhIikgYXMgZjoKICAgICAgICAgICAgZi53cml0ZShsaW5lICsgIlxuIikKICAgIGV4Y2VwdCBFeGNlcHRpb246CiAgICAgICAgcGFzcwoKCmRlZiBlbnN1cmVfd2luZG93cygpOgogICAgaWYgb3MubmFtZSAhPSAibnQiOgogICAgICAgIHJhaXNlIFJ1bnRpbWVFcnJvcigiRXN0ZSBicmlkZ2UgUkFXIHNvbG8gZnVuY2lvbmEgZW4gV2luZG93cy4iKQoKCmRlZiBub3JtYWxpemVfbGluZV9lbmRpbmdzKHRleHQpOgogICAgaWYgaXNpbnN0YW5jZSh0ZXh0LCBieXRlcyk6CiAgICAgICAgdGV4dCA9IHRleHQuZGVjb2RlKCJ1dGYtOCIsIGVycm9ycz0icmVwbGFjZSIpCiAgICB0ZXh0ID0gdGV4dC5yZXBsYWNlKCJcclxuIiwgIlxuIikucmVwbGFjZSgiXHIiLCAiXG4iKQogICAgcmV0dXJuIHRleHQucmVwbGFjZSgiXG4iLCAiXHJcbiIpCgoKZGVmIGV6cGxfYnl0ZXModGV4dCk6CiAgICAjIEVaUEwgdHJhYmFqYSBiaWVuIGNvbiBsYXRpbi0xIC8gQVNDSUkuIEVsIHBlc28sIFNLVSB5IHByZWNpbyBxdWVkYW4gbGltcGlvcy4KICAgIHJldHVybiBub3JtYWxpemVfbGluZV9lbmRpbmdzKHRleHQpLmVuY29kZSgibGF0aW4tMSIsIGVycm9ycz0icmVwbGFjZSIpCgoKZGVmIGRhdGFfbGVuX2J5dGVzKGRhdGEpOgogICAgcmV0dXJuIGxlbihzdHIoZGF0YSkuZW5jb2RlKCJsYXRpbi0xIiwgZXJyb3JzPSJyZXBsYWNlIikpCgoKZGVmIGNsZWFuX2V6cGxfZnJvbV9hcHAoZXpwbCk6CiAgICAiIiIKICAgIENvcnJpZ2UgZXJyb3JlcyBjb211bmVzIHF1ZSB2ZW5pYW4gZGVzZGUgbGEgYXBwOgogICAgLSBeUTk2LDI0IG8gXlE5NiwxNiAtPiBeUTEyLDMKICAgIC0gXkUgLT4gRQogICAgLSBlIHgseSwwLHNpemUsZGF0YSAtPiBYUkJ4LHksc2l6ZSwwLGxlbiArIGRhdGEKICAgICIiIgogICAgaWYgaXNpbnN0YW5jZShlenBsLCBieXRlcyk6CiAgICAgICAgZXpwbCA9IGV6cGwuZGVjb2RlKCJ1dGYtOCIsIGVycm9ycz0icmVwbGFjZSIpCgogICAgbGluZXMgPSBlenBsLnJlcGxhY2UoIlxyXG4iLCAiXG4iKS5yZXBsYWNlKCJcciIsICJcbiIpLnNwbGl0KCJcbiIpCiAgICBvdXQgPSBbXQogICAgaGFzX3EgPSBGYWxzZQogICAgaGFzX3cgPSBGYWxzZQoKICAgIGZvciByYXcgaW4gbGluZXM6CiAgICAgICAgbGluZSA9IHJhdy5zdHJpcCgpCiAgICAgICAgaWYgbm90IGxpbmU6CiAgICAgICAgICAgIGNvbnRpbnVlCgogICAgICAgIHVwcGVyID0gbGluZS51cHBlcigpCgogICAgICAgIGlmIHVwcGVyLnN0YXJ0c3dpdGgoIl5RIik6CiAgICAgICAgICAgIG91dC5hcHBlbmQoIl5RJWQsJWQiICUgKExBQkVMX0hFSUdIVF9NTSwgTEFCRUxfR0FQX01NKSkKICAgICAgICAgICAgaGFzX3EgPSBUcnVlCiAgICAgICAgICAgIGNvbnRpbnVlCgogICAgICAgIGlmIHVwcGVyLnN0YXJ0c3dpdGgoIl5XIik6CiAgICAgICAgICAgIG91dC5hcHBlbmQoIl5XJWQiICUgTEFCRUxfV0lEVEhfTU0pCiAgICAgICAgICAgIGhhc193ID0gVHJ1ZQogICAgICAgICAgICBjb250aW51ZQoKICAgICAgICAjIEZpbmFsIGNvcnJlY3RvIGRlIGZvcm1hdG8gRVpQTDogRSwgbm8gXkUKICAgICAgICBpZiB1cHBlciA9PSAiXkUiOgogICAgICAgICAgICBvdXQuYXBwZW5kKCJFIikKICAgICAgICAgICAgY29udGludWUKCiAgICAgICAgIyBDb252ZXJ0aXIgY29tYW5kbyBhbnRpZ3VvICJlIC4uLiIgYSBEYXRhTWF0cml4IG9maWNpYWwgWFJCCiAgICAgICAgIyBGb3JtYXRvcyBkZXRlY3RhZG9zOgogICAgICAgICMgICBlIDI4OCw0LDAsNCxBR0NSMzM0NAogICAgICAgICMgICBlIDMwMCw0LDYsVEVTVDAxCiAgICAgICAgaWYgcmUubWF0Y2gociJeW2VFXVxzKyIsIGxpbmUpOgogICAgICAgICAgICBwYXlsb2FkID0gcmUuc3ViKHIiXltlRV1ccysiLCAiIiwgbGluZSkuc3RyaXAoKQogICAgICAgICAgICBwYXJ0cyA9IFtwLnN0cmlwKCkgZm9yIHAgaW4gcGF5bG9hZC5zcGxpdCgiLCIpXQogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICBpZiBsZW4ocGFydHMpID49IDU6CiAgICAgICAgICAgICAgICAgICAgIyB4LHksMCxzaXplLGRhdGEKICAgICAgICAgICAgICAgICAgICB4ID0gaW50KGZsb2F0KHBhcnRzWzBdKSkKICAgICAgICAgICAgICAgICAgICB5ID0gaW50KGZsb2F0KHBhcnRzWzFdKSkKICAgICAgICAgICAgICAgICAgICBzaXplID0gaW50KGZsb2F0KHBhcnRzWzNdKSkKICAgICAgICAgICAgICAgICAgICBkYXRhID0gIiwiLmpvaW4ocGFydHNbNDpdKS5zdHJpcCgpCiAgICAgICAgICAgICAgICBlbGlmIGxlbihwYXJ0cykgPj0gNDoKICAgICAgICAgICAgICAgICAgICAjIHgseSxzaXplLGRhdGEKICAgICAgICAgICAgICAgICAgICB4ID0gaW50KGZsb2F0KHBhcnRzWzBdKSkKICAgICAgICAgICAgICAgICAgICB5ID0gaW50KGZsb2F0KHBhcnRzWzFdKSkKICAgICAgICAgICAgICAgICAgICBzaXplID0gaW50KGZsb2F0KHBhcnRzWzJdKSkKICAgICAgICAgICAgICAgICAgICBkYXRhID0gIiwiLmpvaW4ocGFydHNbMzpdKS5zdHJpcCgpCiAgICAgICAgICAgICAgICBlbHNlOgogICAgICAgICAgICAgICAgICAgIG91dC5hcHBlbmQobGluZSkKICAgICAgICAgICAgICAgICAgICBjb250aW51ZQoKICAgICAgICAgICAgICAgIGlmIHNpemUgPCA0OgogICAgICAgICAgICAgICAgICAgIHNpemUgPSBEQVRBTUFUUklYX1NJWkUKCiAgICAgICAgICAgICAgICAjIEV2aXRhciBxdWUgcXVlZGUgY29ydGFkbyBhbCBib3JkZSBkZXJlY2hvCiAgICAgICAgICAgICAgICBpZiB4ID4gMzMwOgogICAgICAgICAgICAgICAgICAgIHggPSAzMDAKICAgICAgICAgICAgICAgIGlmIHkgPCAwOgogICAgICAgICAgICAgICAgICAgIHkgPSBEQVRBTUFUUklYX1kKCiAgICAgICAgICAgICAgICBvdXQuYXBwZW5kKCJYUkIlZCwlZCwlZCwwLCVkIiAlICh4LCB5LCBzaXplLCBkYXRhX2xlbl9ieXRlcyhkYXRhKSkpCiAgICAgICAgICAgICAgICBvdXQuYXBwZW5kKGRhdGEpCiAgICAgICAgICAgICAgICBjb250aW51ZQogICAgICAgICAgICBleGNlcHQgRXhjZXB0aW9uOgogICAgICAgICAgICAgICAgb3V0LmFwcGVuZChsaW5lKQogICAgICAgICAgICAgICAgY29udGludWUKCiAgICAgICAgb3V0LmFwcGVuZChsaW5lKQoKICAgICMgQXNlZ3VyYXIgZW5jYWJlemFkbyBzaSB2ZW5pYSBpbmNvbXBsZXRvCiAgICBpZiBub3QgaGFzX3E6CiAgICAgICAgb3V0Lmluc2VydCgwLCAiXlElZCwlZCIgJSAoTEFCRUxfSEVJR0hUX01NLCBMQUJFTF9HQVBfTU0pKQogICAgaWYgbm90IGhhc193OgogICAgICAgIGluc2VydF9wb3MgPSAxIGlmIG91dCBhbmQgb3V0WzBdLnVwcGVyKCkuc3RhcnRzd2l0aCgiXlEiKSBlbHNlIDAKICAgICAgICBvdXQuaW5zZXJ0KGluc2VydF9wb3MsICJeVyVkIiAlIExBQkVMX1dJRFRIX01NKQoKICAgIGlmIG5vdCBhbnkoeC5zdHJpcCgpLnVwcGVyKCkgPT0gIl5MIiBmb3IgeCBpbiBvdXQpOgogICAgICAgICMgaW5zZXJ0YXIgXkwgYW50ZXMgZGUgdGV4dG9zL2NvZGlnb3Mgc2kgbm8gdmVuaWEKICAgICAgICBpZHggPSAwCiAgICAgICAgZm9yIGksIGwgaW4gZW51bWVyYXRlKG91dCk6CiAgICAgICAgICAgIGlmIGwuc3RhcnRzd2l0aCgiXiIpOgogICAgICAgICAgICAgICAgaWR4ID0gaSArIDEKICAgICAgICBvdXQuaW5zZXJ0KGlkeCwgIl5MIikKCiAgICBpZiBub3Qgb3V0IG9yIG91dFstMV0uc3RyaXAoKS51cHBlcigpICE9ICJFIjoKICAgICAgICBvdXQuYXBwZW5kKCJFIikKCiAgICByZXR1cm4gIlxyXG4iLmpvaW4ob3V0KSArICJcclxuIgoKCmRlZiBtYWtlX2xhYmVsX2V6cGwoY29kZT0iQ1VBTkxBMDQiLCBwcmljZT0iJDMwLjAwMCIsIHg9REFUQU1BVFJJWF9YLCB5PURBVEFNQVRSSVhfWSwgc2l6ZT1EQVRBTUFUUklYX1NJWkUpOgogICAgY29kZSA9IHN0cihjb2RlKS5zdHJpcCgpIG9yICJDVUFOTEEwNCIKICAgIHByaWNlID0gc3RyKHByaWNlKS5zdHJpcCgpIG9yICIkMzAuMDAwIgoKICAgIHRyeToKICAgICAgICB4ID0gaW50KHgpCiAgICAgICAgeSA9IGludCh5KQogICAgICAgIHNpemUgPSBpbnQoc2l6ZSkKICAgIGV4Y2VwdCBFeGNlcHRpb246CiAgICAgICAgeCwgeSwgc2l6ZSA9IERBVEFNQVRSSVhfWCwgREFUQU1BVFJJWF9ZLCBEQVRBTUFUUklYX1NJWkUKCiAgICBpZiBzaXplIDwgNDoKICAgICAgICBzaXplID0gNAogICAgaWYgeCA+IDMzMDoKICAgICAgICB4ID0gMzAwCgogICAgcmV0dXJuICJcclxuIi5qb2luKFsKICAgICAgICAiXlElZCwlZCIgJSAoTEFCRUxfSEVJR0hUX01NLCBMQUJFTF9HQVBfTU0pLAogICAgICAgICJeVyVkIiAlIExBQkVMX1dJRFRIX01NLAogICAgICAgICJeSCVkIiAlIERBUktORVNTLAogICAgICAgICJeUyVkIiAlIFNQRUVELAogICAgICAgICJeUDEiLAogICAgICAgICJeTCIsCiAgICAgICAgIkE4LDgsMCw0LDIsMixOLCVzIiAlIHByaWNlLAogICAgICAgICJBOCw1OCwwLDEsMSwxLE4sJXMiICUgY29kZSwKICAgICAgICAiWFJCJWQsJWQsJWQsMCwlZCIgJSAoeCwgeSwgc2l6ZSwgZGF0YV9sZW5fYnl0ZXMoY29kZSkpLAogICAgICAgIGNvZGUsCiAgICAgICAgIkUiLAogICAgICAgICIiCiAgICBdKQoKCmRlZiBtYWtlX3RleHRfdGVzdF9lenBsKCk6CiAgICByZXR1cm4gIlxyXG4iLmpvaW4oWwogICAgICAgICJeUSVkLCVkIiAlIChMQUJFTF9IRUlHSFRfTU0sIExBQkVMX0dBUF9NTSksCiAgICAgICAgIl5XJWQiICUgTEFCRUxfV0lEVEhfTU0sCiAgICAgICAgIl5IJWQiICUgREFSS05FU1MsCiAgICAgICAgIl5TJWQiICUgU1BFRUQsCiAgICAgICAgIl5QMSIsCiAgICAgICAgIl5MIiwKICAgICAgICAiQTgsOCwwLDQsMiwyLE4sTkFWQVJSTyIsCiAgICAgICAgIkE4LDUwLDAsMiwxLDEsTixURVNUIFJBVyBPSyIsCiAgICAgICAgIkUiLAogICAgICAgICIiCiAgICBdKQoKCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQojIFdJTkRPV1MgU1BPT0xFUiBSQVcgU0lOIFBZV0luMzIKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CgpjbGFzcyBET0NfSU5GT18xVyhjdHlwZXMuU3RydWN0dXJlKToKICAgIF9maWVsZHNfID0gWwogICAgICAgICgicERvY05hbWUiLCB3aW50eXBlcy5MUFdTVFIpLAogICAgICAgICgicE91dHB1dEZpbGUiLCB3aW50eXBlcy5MUFdTVFIpLAogICAgICAgICgicERhdGF0eXBlIiwgd2ludHlwZXMuTFBXU1RSKSwKICAgIF0KCgpkZWYgbGlzdF9wcmludGVyc193bWljKCk6CiAgICBpZiBvcy5uYW1lICE9ICJudCI6CiAgICAgICAgcmV0dXJuIFtdCgogICAgY21kcyA9IFsKICAgICAgICBbIndtaWMiLCAicHJpbnRlciIsICJnZXQiLCAibmFtZSJdLAogICAgICAgIFsiY21kIiwgIi9jIiwgIndtaWMgcHJpbnRlciBnZXQgbmFtZSJdLAogICAgXQoKICAgIGZvciBjbWQgaW4gY21kczoKICAgICAgICB0cnk6CiAgICAgICAgICAgIHJhdyA9IHN1YnByb2Nlc3MuY2hlY2tfb3V0cHV0KGNtZCwgc3RkZXJyPXN1YnByb2Nlc3MuU1RET1VUKQogICAgICAgICAgICB0ZXh0ID0gcmF3LmRlY29kZSgibWJjcyIsIGVycm9ycz0icmVwbGFjZSIpCiAgICAgICAgICAgIG5hbWVzID0gW10KICAgICAgICAgICAgZm9yIGxpbmUgaW4gdGV4dC5zcGxpdGxpbmVzKCk6CiAgICAgICAgICAgICAgICBsaW5lID0gbGluZS5zdHJpcCgpCiAgICAgICAgICAgICAgICBpZiBub3QgbGluZSBvciBsaW5lLmxvd2VyKCkgPT0gIm5hbWUiOgogICAgICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAgICAgICBuYW1lcy5hcHBlbmQobGluZSkKICAgICAgICAgICAgaWYgbmFtZXM6CiAgICAgICAgICAgICAgICByZXR1cm4gbmFtZXMKICAgICAgICBleGNlcHQgRXhjZXB0aW9uOgogICAgICAgICAgICBwYXNzCiAgICByZXR1cm4gW10KCgpkZWYgZmluZF9wcmludGVyX25hbWUoKToKICAgIGlmIFBSSU5URVJfTkFNRV9NQU5VQUwuc3RyaXAoKToKICAgICAgICByZXR1cm4gUFJJTlRFUl9OQU1FX01BTlVBTC5zdHJpcCgpCgogICAgbmFtZXMgPSBsaXN0X3ByaW50ZXJzX3dtaWMoKQogICAgbG9nKCJJbXByZXNvcmFzIGRldGVjdGFkYXM6ICIgKyAoIiwgIi5qb2luKG5hbWVzKSBpZiBuYW1lcyBlbHNlICIobm8gc2UgcHVkaWVyb24gbGlzdGFyKSIpKQoKICAgIGZvciBwcmVmIGluIFBSSU5URVJfUFJFRkVSRU5DRVM6CiAgICAgICAgZm9yIG5hbWUgaW4gbmFtZXM6CiAgICAgICAgICAgIGlmIHByZWYubG93ZXIoKSA9PSBuYW1lLmxvd2VyKCk6CiAgICAgICAgICAgICAgICByZXR1cm4gbmFtZQoKICAgIGZvciBwcmVmIGluIFBSSU5URVJfUFJFRkVSRU5DRVM6CiAgICAgICAgZm9yIG5hbWUgaW4gbmFtZXM6CiAgICAgICAgICAgIGlmIHByZWYubG93ZXIoKSBpbiBuYW1lLmxvd2VyKCk6CiAgICAgICAgICAgICAgICByZXR1cm4gbmFtZQoKICAgIHJhaXNlIFJ1bnRpbWVFcnJvcigKICAgICAgICAiTm8gZW5jb250cmUgaW1wcmVzb3JhIEdvREVYL0dFMzAwLiBFZGl0YSBQUklOVEVSX05BTUVfTUFOVUFMIGVuIG5hdmFycm9fYnJpZGdlLnB5ICIKICAgICAgICAiY29uIGVsIG5vbWJyZSBleGFjdG8gZGUgV2luZG93cy4iCiAgICApCgoKZGVmIHJhd19wcmludChwcmludGVyX25hbWUsIGRhdGFfYnl0ZXMsIGpvYl9uYW1lPSJOYXZhcnJvIEVaUEwgUkFXIik6CiAgICBlbnN1cmVfd2luZG93cygpCgogICAgd2luc3Bvb2wgPSBjdHlwZXMuV2luRExMKCJ3aW5zcG9vbC5kcnYiKQogICAga2VybmVsMzIgPSBjdHlwZXMuV2luRExMKCJrZXJuZWwzMiIsIHVzZV9sYXN0X2Vycm9yPVRydWUpCgogICAgT3BlblByaW50ZXIgPSB3aW5zcG9vbC5PcGVuUHJpbnRlclcKICAgIE9wZW5QcmludGVyLmFyZ3R5cGVzID0gW3dpbnR5cGVzLkxQV1NUUiwgY3R5cGVzLlBPSU5URVIod2ludHlwZXMuSEFORExFKSwgd2ludHlwZXMuTFBWT0lEXQogICAgT3BlblByaW50ZXIucmVzdHlwZSA9IHdpbnR5cGVzLkJPT0wKCiAgICBDbG9zZVByaW50ZXIgPSB3aW5zcG9vbC5DbG9zZVByaW50ZXIKICAgIENsb3NlUHJpbnRlci5hcmd0eXBlcyA9IFt3aW50eXBlcy5IQU5ETEVdCiAgICBDbG9zZVByaW50ZXIucmVzdHlwZSA9IHdpbnR5cGVzLkJPT0wKCiAgICBTdGFydERvY1ByaW50ZXIgPSB3aW5zcG9vbC5TdGFydERvY1ByaW50ZXJXCiAgICBTdGFydERvY1ByaW50ZXIuYXJndHlwZXMgPSBbd2ludHlwZXMuSEFORExFLCB3aW50eXBlcy5EV09SRCwgY3R5cGVzLlBPSU5URVIoRE9DX0lORk9fMVcpXQogICAgU3RhcnREb2NQcmludGVyLnJlc3R5cGUgPSB3aW50eXBlcy5EV09SRAoKICAgIEVuZERvY1ByaW50ZXIgPSB3aW5zcG9vbC5FbmREb2NQcmludGVyCiAgICBFbmREb2NQcmludGVyLmFyZ3R5cGVzID0gW3dpbnR5cGVzLkhBTkRMRV0KICAgIEVuZERvY1ByaW50ZXIucmVzdHlwZSA9IHdpbnR5cGVzLkJPT0wKCiAgICBTdGFydFBhZ2VQcmludGVyID0gd2luc3Bvb2wuU3RhcnRQYWdlUHJpbnRlcgogICAgU3RhcnRQYWdlUHJpbnRlci5hcmd0eXBlcyA9IFt3aW50eXBlcy5IQU5ETEVdCiAgICBTdGFydFBhZ2VQcmludGVyLnJlc3R5cGUgPSB3aW50eXBlcy5CT09MCgogICAgRW5kUGFnZVByaW50ZXIgPSB3aW5zcG9vbC5FbmRQYWdlUHJpbnRlcgogICAgRW5kUGFnZVByaW50ZXIuYXJndHlwZXMgPSBbd2ludHlwZXMuSEFORExFXQogICAgRW5kUGFnZVByaW50ZXIucmVzdHlwZSA9IHdpbnR5cGVzLkJPT0wKCiAgICBXcml0ZVByaW50ZXIgPSB3aW5zcG9vbC5Xcml0ZVByaW50ZXIKICAgIFdyaXRlUHJpbnRlci5hcmd0eXBlcyA9IFt3aW50eXBlcy5IQU5ETEUsIHdpbnR5cGVzLkxQVk9JRCwgd2ludHlwZXMuRFdPUkQsIGN0eXBlcy5QT0lOVEVSKHdpbnR5cGVzLkRXT1JEKV0KICAgIFdyaXRlUHJpbnRlci5yZXN0eXBlID0gd2ludHlwZXMuQk9PTAoKICAgIGhfcHJpbnRlciA9IHdpbnR5cGVzLkhBTkRMRSgpCiAgICBpZiBub3QgT3BlblByaW50ZXIocHJpbnRlcl9uYW1lLCBjdHlwZXMuYnlyZWYoaF9wcmludGVyKSwgTm9uZSk6CiAgICAgICAgZXJyID0gY3R5cGVzLmdldF9sYXN0X2Vycm9yKCkKICAgICAgICByYWlzZSBjdHlwZXMuV2luRXJyb3IoZXJyKQoKICAgIHRyeToKICAgICAgICBkb2NfaW5mbyA9IERPQ19JTkZPXzFXKGpvYl9uYW1lLCBOb25lLCAiUkFXIikKICAgICAgICBqb2JfaWQgPSBTdGFydERvY1ByaW50ZXIoaF9wcmludGVyLCAxLCBjdHlwZXMuYnlyZWYoZG9jX2luZm8pKQogICAgICAgIGlmIGpvYl9pZCA9PSAwOgogICAgICAgICAgICByYWlzZSBjdHlwZXMuV2luRXJyb3IoY3R5cGVzLmdldF9sYXN0X2Vycm9yKCkpCgogICAgICAgIHRyeToKICAgICAgICAgICAgaWYgbm90IFN0YXJ0UGFnZVByaW50ZXIoaF9wcmludGVyKToKICAgICAgICAgICAgICAgIHJhaXNlIGN0eXBlcy5XaW5FcnJvcihjdHlwZXMuZ2V0X2xhc3RfZXJyb3IoKSkKCiAgICAgICAgICAgIHdyaXR0ZW4gPSB3aW50eXBlcy5EV09SRCgwKQogICAgICAgICAgICBidWYgPSBjdHlwZXMuY3JlYXRlX3N0cmluZ19idWZmZXIoZGF0YV9ieXRlcykKICAgICAgICAgICAgaWYgbm90IFdyaXRlUHJpbnRlcihoX3ByaW50ZXIsIGJ1ZiwgbGVuKGRhdGFfYnl0ZXMpLCBjdHlwZXMuYnlyZWYod3JpdHRlbikpOgogICAgICAgICAgICAgICAgcmFpc2UgY3R5cGVzLldpbkVycm9yKGN0eXBlcy5nZXRfbGFzdF9lcnJvcigpKQoKICAgICAgICAgICAgaWYgd3JpdHRlbi52YWx1ZSAhPSBsZW4oZGF0YV9ieXRlcyk6CiAgICAgICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoIldyaXRlUHJpbnRlciBlc2NyaWJpbyAlZCBkZSAlZCBieXRlcyIgJSAod3JpdHRlbi52YWx1ZSwgbGVuKGRhdGFfYnl0ZXMpKSkKCiAgICAgICAgICAgIGlmIG5vdCBFbmRQYWdlUHJpbnRlcihoX3ByaW50ZXIpOgogICAgICAgICAgICAgICAgcmFpc2UgY3R5cGVzLldpbkVycm9yKGN0eXBlcy5nZXRfbGFzdF9lcnJvcigpKQogICAgICAgIGZpbmFsbHk6CiAgICAgICAgICAgIEVuZERvY1ByaW50ZXIoaF9wcmludGVyKQogICAgZmluYWxseToKICAgICAgICBDbG9zZVByaW50ZXIoaF9wcmludGVyKQoKClBSSU5URVJfTkFNRSA9IE5vbmUKCgpkZWYgc2VuZF9lenBsKGV6cGwsIGNsZWFuPVRydWUpOgogICAgZ2xvYmFsIFBSSU5URVJfTkFNRQogICAgaWYgUFJJTlRFUl9OQU1FIGlzIE5vbmU6CiAgICAgICAgUFJJTlRFUl9OQU1FID0gZmluZF9wcmludGVyX25hbWUoKQogICAgICAgIGxvZygiVXNhbmRvIGltcHJlc29yYTogIiArIFBSSU5URVJfTkFNRSkKCiAgICBmaW5hbF9lenBsID0gY2xlYW5fZXpwbF9mcm9tX2FwcChlenBsKSBpZiBjbGVhbiBlbHNlIG5vcm1hbGl6ZV9saW5lX2VuZGluZ3MoZXpwbCkKICAgIGRhdGEgPSBlenBsX2J5dGVzKGZpbmFsX2V6cGwpCgogICAgbG9nKCJFbnZpYW5kbyAlZCBieXRlcyBSQVcgYSAlcyIgJSAobGVuKGRhdGEpLCBQUklOVEVSX05BTUUpKQogICAgbG9nKCJFWlBMOlxuIiArIGZpbmFsX2V6cGwpCgogICAgcmF3X3ByaW50KFBSSU5URVJfTkFNRSwgZGF0YSkKICAgIHJldHVybiBmaW5hbF9lenBsCgoKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CiMgU0VSVklET1IgTE9DQUwKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CgpjbGFzcyBCcmlkZ2VIYW5kbGVyKEJhc2VIVFRQUmVxdWVzdEhhbmRsZXIpOgoKICAgIGRlZiBfY29ycyhzZWxmKToKICAgICAgICBzZWxmLnNlbmRfaGVhZGVyKCJBY2Nlc3MtQ29udHJvbC1BbGxvdy1PcmlnaW4iLCAiKiIpCiAgICAgICAgc2VsZi5zZW5kX2hlYWRlcigiQWNjZXNzLUNvbnRyb2wtQWxsb3ctTWV0aG9kcyIsICJHRVQsIFBPU1QsIE9QVElPTlMiKQogICAgICAgIHNlbGYuc2VuZF9oZWFkZXIoIkFjY2Vzcy1Db250cm9sLUFsbG93LUhlYWRlcnMiLCAiQ29udGVudC1UeXBlIikKCiAgICBkZWYgX3NlbmRfdGV4dChzZWxmLCBjb2RlLCB0ZXh0KToKICAgICAgICBzZWxmLnNlbmRfcmVzcG9uc2UoY29kZSkKICAgICAgICBzZWxmLl9jb3JzKCkKICAgICAgICBzZWxmLnNlbmRfaGVhZGVyKCJDb250ZW50LVR5cGUiLCAidGV4dC9wbGFpbjsgY2hhcnNldD11dGYtOCIpCiAgICAgICAgc2VsZi5lbmRfaGVhZGVycygpCiAgICAgICAgaWYgaXNpbnN0YW5jZSh0ZXh0LCBzdHIpOgogICAgICAgICAgICB0ZXh0ID0gdGV4dC5lbmNvZGUoInV0Zi04IiwgZXJyb3JzPSJyZXBsYWNlIikKICAgICAgICBzZWxmLndmaWxlLndyaXRlKHRleHQpCgogICAgZGVmIGRvX09QVElPTlMoc2VsZik6CiAgICAgICAgc2VsZi5zZW5kX3Jlc3BvbnNlKDIwMCkKICAgICAgICBzZWxmLl9jb3JzKCkKICAgICAgICBzZWxmLmVuZF9oZWFkZXJzKCkKCiAgICBkZWYgZG9fR0VUKHNlbGYpOgogICAgICAgIHBhcnNlZCA9IHVybHBhcnNlKHNlbGYucGF0aCkKICAgICAgICBwYXRoID0gcGFyc2VkLnBhdGgKICAgICAgICBxcyA9IHBhcnNlX3FzKHBhcnNlZC5xdWVyeSkKCiAgICAgICAgdHJ5OgogICAgICAgICAgICBpZiBwYXRoID09ICIvcGluZyI6CiAgICAgICAgICAgICAgICBzZWxmLl9zZW5kX3RleHQoMjAwLCAibmF2YXJyby1icmlkZ2Utb2siKQogICAgICAgICAgICAgICAgcmV0dXJuCgogICAgICAgICAgICBpZiBwYXRoID09ICIvcHJpbnRlcnMiOgogICAgICAgICAgICAgICAgbmFtZXMgPSBsaXN0X3ByaW50ZXJzX3dtaWMoKQogICAgICAgICAgICAgICAgc2VsZi5fc2VuZF90ZXh0KDIwMCwgIlxuIi5qb2luKG5hbWVzKSBpZiBuYW1lcyBlbHNlICJObyBzZSBwdWRpZXJvbiBsaXN0YXIgaW1wcmVzb3JhcyBwb3IgV01JQy4iKQogICAgICAgICAgICAgICAgcmV0dXJuCgogICAgICAgICAgICBpZiBwYXRoID09ICIvdGVzdC10ZXh0IjoKICAgICAgICAgICAgICAgIHNlbmRfZXpwbChtYWtlX3RleHRfdGVzdF9lenBsKCksIGNsZWFuPUZhbHNlKQogICAgICAgICAgICAgICAgc2VsZi5fc2VuZF90ZXh0KDIwMCwgInRlc3QtdGV4dC1vayIpCiAgICAgICAgICAgICAgICByZXR1cm4KCiAgICAgICAgICAgIGlmIHBhdGggPT0gIi90ZXN0IjoKICAgICAgICAgICAgICAgIHNlbmRfZXpwbChtYWtlX2xhYmVsX2V6cGwoIkNVQU5MQTA0IiwgIiQzMC4wMDAiKSwgY2xlYW49RmFsc2UpCiAgICAgICAgICAgICAgICBzZWxmLl9zZW5kX3RleHQoMjAwLCAidGVzdC1kYXRhbWF0cml4LW9rIikKICAgICAgICAgICAgICAgIHJldHVybgoKICAgICAgICAgICAgaWYgcGF0aCA9PSAiL2xhYmVsIjoKICAgICAgICAgICAgICAgIGNvZGUgPSBxcy5nZXQoImNvZGUiLCBbIkNVQU5MQTA0Il0pWzBdCiAgICAgICAgICAgICAgICBwcmljZSA9IHFzLmdldCgicHJpY2UiLCBbIiQzMC4wMDAiXSlbMF0KICAgICAgICAgICAgICAgIHggPSBxcy5nZXQoIngiLCBbREFUQU1BVFJJWF9YXSlbMF0KICAgICAgICAgICAgICAgIHkgPSBxcy5nZXQoInkiLCBbREFUQU1BVFJJWF9ZXSlbMF0KICAgICAgICAgICAgICAgIHNpemUgPSBxcy5nZXQoInNpemUiLCBbREFUQU1BVFJJWF9TSVpFXSlbMF0KICAgICAgICAgICAgICAgIHNlbmRfZXpwbChtYWtlX2xhYmVsX2V6cGwoY29kZSwgcHJpY2UsIHgsIHksIHNpemUpLCBjbGVhbj1GYWxzZSkKICAgICAgICAgICAgICAgIHNlbGYuX3NlbmRfdGV4dCgyMDAsICJsYWJlbC1vayIpCiAgICAgICAgICAgICAgICByZXR1cm4KCiAgICAgICAgICAgIHNlbGYuX3NlbmRfdGV4dCg0MDQsICJOb3QgZm91bmQiKQogICAgICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToKICAgICAgICAgICAgbG9nKCJFUlJPUiBHRVQgJXM6ICVzIiAlIChwYXRoLCBlKSkKICAgICAgICAgICAgbG9nKHRyYWNlYmFjay5mb3JtYXRfZXhjKCkpCiAgICAgICAgICAgIHNlbGYuX3NlbmRfdGV4dCg1MDAsIHN0cihlKSkKCiAgICBkZWYgZG9fUE9TVChzZWxmKToKICAgICAgICBwYXJzZWQgPSB1cmxwYXJzZShzZWxmLnBhdGgpCiAgICAgICAgcGF0aCA9IHBhcnNlZC5wYXRoCgogICAgICAgIHRyeToKICAgICAgICAgICAgbGVuZ3RoID0gaW50KHNlbGYuaGVhZGVycy5nZXQoIkNvbnRlbnQtTGVuZ3RoIiwgIjAiKSBvciAiMCIpCiAgICAgICAgZXhjZXB0IEV4Y2VwdGlvbjoKICAgICAgICAgICAgbGVuZ3RoID0gMAoKICAgICAgICByYXdfYm9keSA9IHNlbGYucmZpbGUucmVhZChsZW5ndGgpCgogICAgICAgIHRyeToKICAgICAgICAgICAgaWYgcGF0aCA9PSAiL3ByaW50LWxhYmVsIjoKICAgICAgICAgICAgICAgIGJvZHlfdGV4dCA9IHJhd19ib2R5LmRlY29kZSgidXRmLTgiLCBlcnJvcnM9InJlcGxhY2UiKQogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHBheWxvYWQgPSBqc29uLmxvYWRzKGJvZHlfdGV4dCkKICAgICAgICAgICAgICAgIGV4Y2VwdCBFeGNlcHRpb246CiAgICAgICAgICAgICAgICAgICAgcGF5bG9hZCA9IHt9CiAgICAgICAgICAgICAgICAgICAgZm9yIHBhaXIgaW4gYm9keV90ZXh0LnNwbGl0KCImIik6CiAgICAgICAgICAgICAgICAgICAgICAgIGlmICI9IiBpbiBwYWlyOgogICAgICAgICAgICAgICAgICAgICAgICAgICAgaywgdiA9IHBhaXIuc3BsaXQoIj0iLCAxKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgcGF5bG9hZFtrXSA9IHYKCiAgICAgICAgICAgICAgICBjb2RlID0gcGF5bG9hZC5nZXQoImNvZGUiKSBvciBwYXlsb2FkLmdldCgic2t1Iikgb3IgIkNVQU5MQTA0IgogICAgICAgICAgICAgICAgcHJpY2UgPSBwYXlsb2FkLmdldCgicHJpY2UiKSBvciBwYXlsb2FkLmdldCgicHJlY2lvIikgb3IgIiQzMC4wMDAiCiAgICAgICAgICAgICAgICB4ID0gcGF5bG9hZC5nZXQoIngiLCBEQVRBTUFUUklYX1gpCiAgICAgICAgICAgICAgICB5ID0gcGF5bG9hZC5nZXQoInkiLCBEQVRBTUFUUklYX1kpCiAgICAgICAgICAgICAgICBzaXplID0gcGF5bG9hZC5nZXQoInNpemUiLCBEQVRBTUFUUklYX1NJWkUpCgogICAgICAgICAgICAgICAgZmluYWwgPSBtYWtlX2xhYmVsX2V6cGwoY29kZSwgcHJpY2UsIHgsIHksIHNpemUpCiAgICAgICAgICAgICAgICBzZW5kX2V6cGwoZmluYWwsIGNsZWFuPUZhbHNlKQogICAgICAgICAgICAgICAgc2VsZi5fc2VuZF90ZXh0KDIwMCwgIm9rIikKICAgICAgICAgICAgICAgIHJldHVybgoKICAgICAgICAgICAgIyAvcHJpbnQgeSBjdWFscXVpZXIgUE9TVDogc2UgYXN1bWUgRVpQTCBkZXNkZSB0dSBhcHAuCiAgICAgICAgICAgIGJvZHlfdGV4dCA9IHJhd19ib2R5LmRlY29kZSgidXRmLTgiLCBlcnJvcnM9InJlcGxhY2UiKQogICAgICAgICAgICBzZW5kX2V6cGwoYm9keV90ZXh0LCBjbGVhbj1UcnVlKQogICAgICAgICAgICBzZWxmLl9zZW5kX3RleHQoMjAwLCAib2siKQogICAgICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToKICAgICAgICAgICAgbG9nKCJFUlJPUiBQT1NUICVzOiAlcyIgJSAocGF0aCwgZSkpCiAgICAgICAgICAgIGxvZyh0cmFjZWJhY2suZm9ybWF0X2V4YygpKQogICAgICAgICAgICBzZWxmLl9zZW5kX3RleHQoNTAwLCBzdHIoZSkpCgogICAgZGVmIGxvZ19tZXNzYWdlKHNlbGYsICphcmdzKToKICAgICAgICByZXR1cm4KCgpkZWYgbWFpbigpOgogICAgZ2xvYmFsIFBSSU5URVJfTkFNRQoKICAgIHByaW50KCIiKQogICAgcHJpbnQoIj0iICogNjApCiAgICBwcmludCgiIE5BVkFSUk8gRVJQIC0gQlJJREdFIEVaUEwgQ09SUkVHSURPIC8gR09ERVggR0UzMDAiKQogICAgcHJpbnQoIiBQeXRob246ICIgKyBzeXMudmVyc2lvbi5zcGxpdCgpWzBdKQogICAgcHJpbnQoIj0iICogNjApCiAgICBwcmludCgiIikKICAgIHByaW50KCIgVVJMczoiKQogICAgcHJpbnQoIiAgUGluZyAgICAgICA6IGh0dHA6Ly8xMjcuMC4wLjE6JWQvcGluZyIgJSBQT1JUKQogICAgcHJpbnQoIiAgSW1wcmVzb3JhcyA6IGh0dHA6Ly8xMjcuMC4wLjE6JWQvcHJpbnRlcnMiICUgUE9SVCkKICAgIHByaW50KCIgIFRlc3QgdGV4dG8gOiBodHRwOi8vMTI3LjAuMC4xOiVkL3Rlc3QtdGV4dCIgJSBQT1JUKQogICAgcHJpbnQoIiAgVGVzdCBETSAgICA6IGh0dHA6Ly8xMjcuMC4wLjE6JWQvdGVzdCIgJSBQT1JUKQogICAgcHJpbnQoIiAgTGFiZWwgICAgICA6IGh0dHA6Ly8xMjcuMC4wLjE6JWQvbGFiZWw/Y29kZT1DVUFOTEEwNCZwcmljZT0kMzAuMDAwIiAlIFBPUlQpCiAgICBwcmludCgiIikKICAgIHByaW50KCIgQXJjaGl2byBsb2cgOiAiICsgTE9HX0ZJTEUpCiAgICBwcmludCgiIikKCiAgICB0cnk6CiAgICAgICAgUFJJTlRFUl9OQU1FID0gZmluZF9wcmludGVyX25hbWUoKQogICAgICAgIHByaW50KCIgSW1wcmVzb3JhIHNlbGVjY2lvbmFkYTogIiArIFBSSU5URVJfTkFNRSkKICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToKICAgICAgICBwcmludCgiIEFEVkVSVEVOQ0lBOiAiICsgc3RyKGUpKQogICAgICAgIHByaW50KCIgUHVlZGVzIGVkaXRhciBQUklOVEVSX05BTUVfTUFOVUFMIGFycmliYSBlbiBlc3RlIGFyY2hpdm8uIikKICAgICAgICBQUklOVEVSX05BTUUgPSBOb25lCgogICAgcHJpbnQoIiIpCiAgICBwcmludCgiIExpc3RvLiBNYW50w6luIGVzdGEgdmVudGFuYSBhYmllcnRhLiIpCiAgICBwcmludCgiIikKCiAgICBzZXJ2ZXIgPSBIVFRQU2VydmVyKCgiMTI3LjAuMC4xIiwgUE9SVCksIEJyaWRnZUhhbmRsZXIpCiAgICBzZXJ2ZXIuc2VydmVfZm9yZXZlcigpCgoKaWYgX19uYW1lX18gPT0gIl9fbWFpbl9fIjoKICAgIGlmIHN5cy52ZXJzaW9uX2luZm8gPCAoMywgNik6CiAgICAgICAgcHJpbnQoIkVSUk9SOiBTZSByZXF1aWVyZSBQeXRob24gMy42IG8gc3VwZXJpb3IuIikKICAgICAgICBpbnB1dCgiUHJlc2lvbmEgRW50ZXIgcGFyYSBjZXJyYXIuLi4iKQogICAgICAgIHN5cy5leGl0KDEpCgogICAgdHJ5OgogICAgICAgIG1haW4oKQogICAgZXhjZXB0IEtleWJvYXJkSW50ZXJydXB0OgogICAgICAgIHByaW50KCJcbkJyaWRnZSBkZXRlbmlkby4iKQogICAgZXhjZXB0IEV4Y2VwdGlvbiBhcyBlOgogICAgICAgIHByaW50KCJcbkVSUk9SIEZBVEFMOiAiICsgc3RyKGUpKQogICAgICAgIHByaW50KHRyYWNlYmFjay5mb3JtYXRfZXhjKCkpCiAgICAgICAgaW5wdXQoIlByZXNpb25hIEVudGVyIHBhcmEgY2VycmFyLi4uIikKICAgICAgICBzeXMuZXhpdCgxKQo=";',
    'var _BBAT="QGVjaG8gb2ZmDQp0aXRsZSBOYXZhcnJvIEJyaWRnZSBHRTMwMCAtIERFQlVHDQpjb2xvciAwQQ0KY2QgL2QgIiV+ZHAwIg0KDQplY2hvLg0KZWNobyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQplY2hvICBOQVZBUlJPIEJSSURHRSBHRTMwMCAtIERFQlVHDQplY2hvID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCmVjaG8gIENhcnBldGEgYWN0dWFsOg0KZWNobyAgJWNkJQ0KZWNoby4NCg0KZWNobyBCdXNjYW5kbyBQeXRob24uLi4NCndoZXJlIHB5dGhvbiA+bnVsIDI+bnVsDQppZiAlZXJyb3JsZXZlbCU9PTAgKA0KICAgIGVjaG8gT0sgLSB1c2FuZG8gY29tYW5kbyBweXRob24NCiAgICBlY2hvLg0KICAgIHB5dGhvbiAiJX5kcDBuYXZhcnJvX2JyaWRnZS5weSINCiAgICBnb3RvIEZJTg0KKQ0KDQp3aGVyZSBweSA+bnVsIDI+bnVsDQppZiAlZXJyb3JsZXZlbCU9PTAgKA0KICAgIGVjaG8gT0sgLSB1c2FuZG8gY29tYW5kbyBweSAtMw0KICAgIGVjaG8uDQogICAgcHkgLTMgIiV+ZHAwbmF2YXJyb19icmlkZ2UucHkiDQogICAgZ290byBGSU4NCikNCg0KZWNoby4NCmVjaG8gRVJST1I6IE5vIGVuY29udHJlIFB5dGhvbiBlbiBlc3RlIGNvbXB1dGFkb3IuDQplY2hvIEluc3RhbGEgUHl0aG9uIDMuOC4xMCBwYXJhIFdpbmRvd3MgNyB5IG1hcmNhICJBZGQgUHl0aG9uIHRvIFBBVEgiLg0KZWNoby4NCmVjaG8gTGluayByZWNvbWVuZGFkbzoNCmVjaG8gaHR0cHM6Ly93d3cucHl0aG9uLm9yZy9mdHAvcHl0aG9uLzMuOC4xMC9weXRob24tMy44LjEwLWFtZDY0LmV4ZQ0KZWNoby4NCg0KOkZJTg0KZWNoby4NCmVjaG8gPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KZWNobyAgRWwgYnJpZGdlIHRlcm1pbm8gY29uIGNvZGlnbzogJWVycm9ybGV2ZWwlDQplY2hvID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCmVjaG8uDQpwYXVzZQ0K";',
    'async function checkBridge(){',
    '  try{',
    '    var ctrl=new AbortController(),tid=setTimeout(function(){ctrl.abort();},2000);',
    '    var r=await fetch("http://127.0.0.1:9191/ping",{mode:"cors",signal:ctrl.signal});',
    '    clearTimeout(tid);',
    '    return r.ok&&(await r.text()).trim()==="navarro-bridge-ok";',
    '  }catch(e){return false;}',
    '}',
    'async function sendViaBridge(){',
    '  if(!EZPL||!EZPL.length){st("err","Sin datos EZPL");return;}',
    '  st("","Verificando bridge local...");',
    '  var ok=await checkBridge();',
    '  if(!ok){',
    '    st("err","Bridge no detectado — ver instrucciones ↓");',
    '    var bi=G("bridgeInfo");if(bi)bi.style.display="block";',
    '    return;',
    '  }',
    '  st("","Enviando "+EZPL.length+" etiqueta(s) vía bridge...");',
    '  try{',
    '    var body=EZPL.join(LF);',
    '    var r=await fetch("http://127.0.0.1:9191/print",{',
    '      method:"POST",body:body,',
    '      headers:{"Content-Type":"text/plain"},mode:"cors"',
    '    });',
    '    if(r.ok){',
    '      st("on","✓ "+EZPL.length+" etiqueta(s) enviadas — DM firmware GE300");',
    '      var bi=G("bridgeInfo");if(bi)bi.style.display="none";',
    '    } else {',
    '      var t=await r.text();st("err","Bridge error: "+t);',
    '    }',
    '  }catch(e){st("err","Bridge: "+e.message);}',
    '}',
    'function _dlBlob(b64,filename){',
    '  try{',
    '    var bin=atob(b64),arr=new Uint8Array(bin.length);',
    '    for(var i=0;i<bin.length;i++)arr[i]=bin.charCodeAt(i);',
    '    var blob=new Blob([arr],{type:"text/plain"});',
    '    var a=document.createElement("a");',
    '    a.href=URL.createObjectURL(blob);a.download=filename;',
    '    document.body.appendChild(a);a.click();',
    '    setTimeout(function(){URL.revokeObjectURL(a.href);document.body.removeChild(a);},1000);',
    '  }catch(e){alert("Error: "+e.message);}',
    '}',
    'function downloadBridge(){',
    '  _dlBlob(_BSCRIPT,"navarro_bridge.py");',
    '  _dlBlob(_BBAT,"navarro_bridge.bat");',
    '  st("on","navarro_bridge.py + .bat descargados ✓");',
    '}',
    /* ── Comandos GE300 ── */
    'async function sendCmd(cmd){',
    '  var dk="8",sp="2";',
    '  var selD=G("selDark"),selS=G("selSpeed");',
    '  if(selD)dk=selD.value; if(selS)sp=selS.value;',
    '  var s="";',
    '  if(cmd==="feed")  s=["^Q12,3","^W55","^H"+dk,"^S"+sp,"^P1","^L","^E"].join(LF)+LF;',
    '  if(cmd==="cal")   s="~T,100"+LF;',
    '  if(cmd==="test")  s=["^Q12,3","^W55","^H"+dk,"^S"+sp,"^P1","^L","A8,8,0,3,1,1,N,TEST","^E"].join(LF)+LF;',
    '  if(cmd==="reset") s="~R"+LF;',
    '  if(cmd==="params")s=["^Q12,3","^W55","^H"+dk,"^S"+sp].join(LF)+LF;',
    '  if(!s)return;',
    '  var ok=await serialSend(s);',
    '  if(ok)st("on","Comando enviado: "+cmd);',
    '}',
    /* ══════════════════════════════════════════════════════════════════
       REGISTRO DE LISTENERS — dentro de setTimeout para garantizar
       que el DOM esté completamente listo (evita null.addEventListener)
       ══════════════════════════════════════════════════════════════════ */
    'window.setTimeout(function(){',
    '  try{',
    '    function on(id,ev,fn){var el=G(id);if(el)el.addEventListener(ev,fn);}',
    /* btnPrint: ventana limpia con SVG vectorial a 55×12mm — máxima calidad, sin pixelado */
    '    on("btnPrint","click", function(){',
    '      var chk=G("chkRot"),rot=chk&&chk.checked;',
    '      var wraps=document.querySelectorAll(".etq-wrap[data-svgp]");',
    '      if(!wraps.length){alert("Sin etiquetas");return;}',
    '      var tf=rot?"rotate(180deg)":"rotate(0deg)";',
    '      var h="<!DOCTYPE html><html><head><meta charset=\'utf-8\'><style>";',
    '      h+="@page{size:55mm 12mm;margin:0}";',
    '      h+="*{margin:0;padding:0;box-sizing:border-box}";',
    '      h+="html,body{width:55mm;background:white}";',
    '      h+="img{display:block;width:55mm;height:12mm;transform:"+tf+"}";',
    '      h+="</style></head><body>";',
    '      for(var i=0;i<wraps.length;i++){',
    '        var src=wraps[i].getAttribute("data-svgp");',
    '        if(src)h+=\'<img src="\'+src+\'">\';',
    '      }',
    '      h+="</body></html>";',
    '      var pw=window.open("","_pw","width=300,height=200");',
    '      if(!pw){alert("Permite ventanas emergentes");return;}',
    '      pw.document.open();pw.document.write(h);pw.document.close();',
    '      pw.focus();',
    '      pw.print();',
    '      try{pw.addEventListener("afterprint",function(){pw.close();});}catch(e){}',
    '    });',
    '    on("btnBridge","click", sendViaBridge);',
    '    on("btnEzpl", "click", sendEzpl);',
    '    on("btnPanel","click", function(){',
    '      var p=G("gotools");',
    '      if(p)p.style.display=p.style.display==="block"?"none":"block";',
    '    });',
    '    on("btnCopy", "click", function(){',
    '      var txt=EZPL.join(LF+"---"+LF);',
    '      if(navigator.clipboard){navigator.clipboard.writeText(txt).then(function(){st("on","EZPL copiado");});}',
    '      else{alert(txt);}',
    '    });',
    '    on("chkRot","change",function(){',
    '      var r=this.checked;',
    '      document.querySelectorAll(".etq-svg").forEach(function(e){e.style.transform=r?"rotate(180deg)":"";});',
    '      document.querySelectorAll(".etq-print").forEach(function(e){e.src=r?e.getAttribute("data-r"):e.getAttribute("data-n");});',
    '    });',
    '    on("btnConn",  "click", serialConn);',
    '    on("btnDisc",  "click", serialDisc);',
    '    on("btnSend2", "click", sendEzpl);',
    '    on("btnFeed",  "click", function(){sendCmd("feed");});',
    '    on("btnCal",   "click", function(){sendCmd("cal");});',
    '    on("btnTest",  "click", function(){sendCmd("test");});',
    '    on("btnRst",   "click", function(){sendCmd("reset");});',
    '    on("btnParams","click", function(){sendCmd("params");});',
    '    if(!navigator.serial){',
    '      var bc=G("btnConn");if(bc){bc.disabled=true;bc.title="Solo Chrome/Edge";}',
    '    }',
    '    /* Auto-check bridge al cargar */',
    '    checkBridge().then(function(ok){',
    '      if(ok){st("on","Bridge activo (localhost:9191) — clic \u{1F517} Bridge EZPL para imprimir");}',
    '      else{st("","Listo — usa \u{1F517} Bridge EZPL (recomendado) o \u{1F5A8} Imprimir (driver)");}',
    '    });',
    '  }catch(err){',
    '    var div=document.createElement("div");',
    '    div.style.cssText="background:#c00;color:#fff;padding:8px;font-size:11px;border-radius:4px;margin:8px 0";',
    '    div.textContent="Error JS: "+err.message;',
    '    document.body.insertBefore(div,document.body.firstChild);',
    '  }',
    '},0);',
  ].join('\n');

  // ── CSS ──────────────────────────────────────────────────────────────────
  // CRÍTICO: .etq-print es la imagen que va a la impresora (display:none en pantalla)
  // En @media print: display:block!important → siempre se imprime.
  // .etq-svg se muestra en pantalla (SVG vectorial, nitido) y se oculta al imprimir.
  // SIN inline style="display:none" en el HTML → CSS controla todo.
  const css = [
    '/* @page se define dentro de @media print abajo (size:55mm auto) */',
    '*{box-sizing:border-box;margin:0;padding:0}',
    'body{background:#e8e4df;font:11px Arial,sans-serif;padding:10px}',
    '.tb{background:#1a2f1a;color:#fff;padding:8px 12px;border-radius:6px;margin-bottom:8px;display:flex;gap:6px;align-items:center;flex-wrap:wrap}',
    '.ttl{flex:1;font-size:12px}.ttl b{color:#c9a84c}',
    '.btn{padding:7px 16px;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:bold;white-space:nowrap}',
    '.bgold{background:#c9a84c;color:#111}.bgold:hover{background:#e0b84e}.bdark{background:rgba(255,255,255,.18);color:#fff;font-weight:400}',
    '.bbridge{background:#16a34a;color:#fff;animation:pulse 2s infinite}',
    '@keyframes pulse{0%,100%{box-shadow:0 0 0 0 rgba(22,163,74,.4)}50%{box-shadow:0 0 0 5px rgba(22,163,74,0)}}',
    '.sm{padding:4px 9px;border:none;border-radius:3px;cursor:pointer;font-size:10px;font-weight:bold}',
    '.sg{background:#16a34a;color:#fff}.sy{background:#d97706;color:#111}',
    '.sr{background:#dc2626;color:#fff}.ss{background:#6b7280;color:#fff}',
    '.sb{background:#3b82f6;color:#fff}.st{background:#0d9488;color:#fff}',
    /* Barra de estado de conexión */
    '.cb{background:#f0f9ff;border:1px solid #93c5fd;border-radius:5px;padding:6px 10px;margin-bottom:8px;display:flex;align-items:center;gap:8px}',
    '.dot{width:10px;height:10px;border-radius:50%;background:#9ca3af;flex-shrink:0}',
    '.dot.on{background:#16a34a}.dot.err{background:#dc2626}',
    '#msg{flex:1;font-size:10px;color:#1e40af}',
    /* Panel GoTools */
    '.panel{background:#fffbf2;border:1px solid #c9a84c;border-radius:6px;padding:10px;margin-bottom:8px;display:none}',
    '.ph{font-size:11px;color:#7c4f00;margin:0 0 5px;font-weight:800;border-bottom:1px solid #e8d48c;padding-bottom:3px}',
    '.ph2{font-size:10px;color:#5a3a00;margin:7px 0 4px;font-weight:700}',
    '.tr{display:flex;gap:5px;flex-wrap:wrap;margin-bottom:6px}',
    '.pr{display:flex;gap:8px;align-items:center;flex-wrap:wrap;background:#f5edd0;padding:6px;border-radius:4px;margin-bottom:6px}',
    '.pr label{display:flex;align-items:center;gap:3px;font-size:10px;color:#5a3a00}',
    '.pr select{font-size:10px;padding:2px 4px;border:1px solid #c9a84c;border-radius:3px;background:#fffef8}',
    '.ezpre{background:#111;padding:8px;border-radius:4px;font:9px/1.5 monospace;color:#7cfc00;white-space:pre;overflow:auto;max-height:110px;margin:4px 0}',
    '.dtbl{width:100%;border-collapse:collapse;font-size:10px;margin-bottom:6px}',
    '.dtbl td{padding:3px 7px;border:1px solid #ddd}',
    '.dtbl tr:nth-child(odd) td{background:#f5edd0}',
    /* Rotación */
    '.rb{background:#fff3cd;border:1px solid #f59e0b;border-radius:5px;padding:6px 10px;margin-bottom:8px;font-size:11px;display:flex;align-items:center;gap:8px}',
    '.rb label{display:flex;align-items:center;gap:5px;cursor:pointer;font-weight:bold;color:#78350f}',
    /* Etiquetas */
    '.etq-wrap{display:inline-flex;flex-direction:column;align-items:center;margin:6px 8px}',
    '.etq-box{width:462px;height:101px;background:#fff;border:1px solid #ccc;border-radius:2px;overflow:hidden;box-shadow:0 1px 4px rgba(0,0,0,.1);position:relative}',
    /* SVG: visible en pantalla, oculto en impresión */
    '.etq-svg{display:block;width:462px;height:101px;image-rendering:crisp-edges}',
    /* PNG: oculto en pantalla (position absolute para no ocupar espacio), visible en impresión */
    '.etq-print{position:absolute;top:0;left:0;display:none;image-rendering:crisp-edges}',
    '.etq-info{font-size:9px;color:#888;margin-top:3px;max-width:462px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
    /* ── Impresión ── */
    /* CLAVE: layout CONTINUO — sin page-break entre etiquetas.                */
    /* Un solo "documento" de 55mm×(N×12mm). La impresora usa el sensor de GAP */
    /* para avanzar automáticamente de etiqueta en etiqueta.                   */
    /* Esto elimina el problema de "imprime 1, salta 1" causado por el         */
    /* page-break-after:always que generaba páginas en blanco entre etiquetas. */
    '@page{size:55mm auto;margin:0}',
    '@media print{',
    '  html,body{background:white;padding:0;margin:0;width:55mm}',
    '  .tb,.cb,.panel,.rb,.etq-info{display:none!important}',
    /* Sin page-break — layout continuo */
    '  .etq-wrap{display:block;margin:0;padding:0;width:55mm;height:12mm;overflow:hidden}',
    '  .etq-box{width:55mm;height:12mm;border:none;box-shadow:none;overflow:hidden;position:relative}',
    '  .etq-svg{display:none!important}',
    /* PNG a tamaño exacto — ya está a 203 DPI nativo, sin escalado del driver */
    '  .etq-print{display:block!important;position:static;width:55mm!important;height:12mm!important;image-rendering:pixelated}',
    '}',
  ].join('\n');

  // ── HTML del body ─────────────────────────────────────────────────────────
  const body = [
    /* Toolbar: Imprimir es el botón principal (siempre funciona con driver) */
    '<div class="tb">',
    '  <span class="ttl">🖨 <b>Godex GE300</b> &middot; 55&times;12mm &middot; <b>' + count + '</b> etiqueta(s)</span>',
    '  <button class="btn bgold" id="btnBridge" title="Envía EZPL directo — el firmware Godex genera el Data Matrix nativo (igual que GoLabel)">🔗 Bridge EZPL</button>',
    '  <button class="btn bdark" id="btnPrint">🖨 Imprimir</button>',
    '  <button class="btn bdark" id="btnEzpl">📨 Serie</button>',
    '  <button class="btn bdark" id="btnPanel">🔧 GoTools</button>',
    '</div>',
    /* Barra de estado EZPL/Serie */
    '<div class="cb">',
    '  <div class="dot" id="dot"></div>',
    '  <span id="msg">Desconectado — usa <b>Imprimir</b> con el driver, o conecta el puerto serie para enviar EZPL directo</span>',
    '  <button class="sm sb" id="btnConn">🔌 Conectar USB</button>',
    '  <button class="sm ss" id="btnDisc">⏏ Desconectar</button>',
    '</div>',
    /* Panel Bridge EZPL — instrucciones de setup (oculto hasta que falle) */
    '<div id="bridgeInfo" style="display:none;background:#fef3c7;border:1px solid #f59e0b;border-radius:6px;padding:11px 13px;margin-bottom:8px;font-size:10px;color:#78350f;line-height:1.9">',
    '  <b style="font-size:11px;display:block;margin-bottom:4px">🔗 Navarro Bridge — Setup (solo 1 vez)</b>',
    '  Envía EZPL directo al firmware Godex → el Data Matrix lo genera la impresora (igual que GoLabel).<br><br>',
    '  <b>Paso 1:</b> <button onclick="downloadBridge()" style="background:#c9a84c;color:#111;border:none;padding:3px 10px;border-radius:3px;cursor:pointer;font-weight:bold;font-size:10px;margin:2px 4px 2px 0">⬇ Descargar Bridge</button> (descarga navarro_bridge.py + navarro_bridge.bat)<br><br>',
    '  <b>Paso 2 — Instalar Python</b> (si no lo tienes):<br>',
    '  &nbsp;&nbsp;Ve a <u>python.org/downloads</u> → descarga → instala<br>',
    '  &nbsp;&nbsp;<b style="color:#c00">✔ Marca "Add Python to PATH"</b> durante la instalación<br><br>',
    '  <b>Paso 3 — Ejecutar el bridge:</b><br>',
    '  &nbsp;&nbsp;<b>Windows:</b> doble-clic en <b>navarro_bridge.bat</b> (detecta la Godex automáticamente)<br>',
    '  &nbsp;&nbsp;<b>Mac:</b> Terminal → <code style="background:rgba(0,0,0,.08);padding:1px 4px;border-radius:2px">python3 navarro_bridge.py</code><br><br>',
    '  <b>Paso 4:</b> Deja esa ventana abierta → clic <b>🔗 Bridge EZPL</b> aquí.<br><br>',
    '  <span style="color:#b45309;font-size:9px">⚠ Si la impresora no se detecta, el bridge te mostrará una lista para elegir.</span>',
    '</div>',
    /* Panel GoTools */
    '<div class="panel" id="gotools">',
    '  <p class="ph">🔧 GoTools &middot; Godex GE300</p>',
    /* Nota importante sobre conexión */
    '  <div style="padding:7px 9px;background:#fef9c3;border:1px solid #f59e0b;border-radius:5px;font-size:9px;color:#713f12;margin-bottom:8px;line-height:1.6">',
    '    <b style="font-size:10px">📋 Cómo activar USB Virtual COM en GE300</b><br>',
    '    <b>Opción A — GoLabel (recomendado):</b><br>',
    '    1. Instalar GoLabel desde <u>godex.eu → Support → Software</u><br>',
    '    2. Con la impresora conectada: <i>File → Printer Setup → seleccionar GE300</i><br>',
    '    3. Pestaña <b>Interface</b> → seleccionar <b>USB Virtual COM Port</b> → Apply<br>',
    '    4. Windows instala el driver CDC automáticamente<br>',
    '    5. En <b>Administrador de dispositivos → Puertos (COM y LPT)</b> aparece <b>Godex GE300 (COM3)</b><br>',
    '    6. Clic <b>Conectar USB</b> aquí → seleccionar ese COM → <b>Enviar EZPL</b><br>',
    '    <br>',
    '    <b>Opción B — Driver CDC:</b> godex.eu → Support → Drivers → descargar driver CDC GE300<br>',
    '    <br>',
    '    <b style="color:#c00">Nota:</b> Chrome/Edge únicamente. No funciona en Firefox ni Safari.',
    '  </div>',
    '  <p class="ph2">Herramientas</p>',
    '  <div class="tr">',
    '    <button class="sm sg" id="btnFeed">Feed (avanzar)</button>',
    '    <button class="sm sy" id="btnCal">Calibrar GAP</button>',
    '    <button class="sm sb" id="btnTest">Pág. prueba</button>',
    '    <button class="sm sr" id="btnRst">Reset</button>',
    '  </div>',
    '  <p class="ph2">Parámetros GE300</p>',
    '  <div class="pr">',
    '    <label>Oscuridad: <select id="selDark"><option>6</option><option>7</option><option selected>8</option><option>10</option><option>12</option><option>15</option></select></label>',
    '    <label>Velocidad: <select id="selSpeed"><option>1</option><option selected>2</option><option>3</option><option>4</option></select> ips</label>',
    '    <label>Térmica directa ✓</label>',
    '    <button class="sm st" id="btnParams">Enviar</button>',
    '  </div>',
    '  <p class="ph2">Config driver Godex (referencia visual)</p>',
    '  <table class="dtbl">',
    '    <tr><td><b>Impresora</b></td><td style="font-weight:bold;color:#1a3a1a">GODEX GE300</td></tr>',
    '    <tr><td><b>Papel / Label</b></td><td style="font-weight:bold;color:#1a3a1a">55 &times; 12 mm</td></tr>',
    '    <tr><td><b>Ajustar al tama&ntilde;o</b></td><td>&#10060; Desactivar</td></tr>',
    '    <tr><td><b>Solo reducir</b></td><td>&#10060; Desactivar</td></tr>',
    '    <tr><td><b>Image Processing</b></td><td><span style="color:#c00">Cluster</span> &rarr; <b>Threshold</b></td></tr>',
    '    <tr><td><b>Resoluci&oacute;n</b></td><td>203 DPI &#10003;</td></tr>',
    '    <tr><td><b>Separaci&oacute;n</b></td><td>2 mm &#10003;</td></tr>',
    '  </table>',
    '  <p class="ph2">EZPL (comandos exactos — para enviar por serie o GoLabel)</p>',
    '  <div class="ezpre" id="ezpre">(cargando...)</div>',
    '  <div class="tr" style="margin-top:4px">',
    '    <button class="sm sy" id="btnCopy">Copiar EZPL</button>',
    '    <button class="sm sg" id="btnSend2">Enviar por serie</button>',
    '  </div>',
    '</div>',
    /* Rotación */
    '<div class="rb">',
    '  <label><input type="checkbox" id="chkRot"/> 🔄 Rotar 180° si imprime al revés</label>',
    '  <span style="color:#92400e;font-size:10px">Afecta pantalla + impresión por driver</span>',
    '</div>',
    /* Contenedor de etiquetas — llenado por win.document.getElementById("labels").innerHTML */
    '<div id="labels" style="padding:4px 0"></div>',
  ].join('\n');

  return '<!DOCTYPE html>\n<html><head><meta charset="utf-8">' +
    '<title>Godex GE300 &middot; ' + count + ' etiq.</title>' +
    '<style>' + css + '</style>' +
    '</head><body>' + body +
    '<script>' + S + '<' + '/script>' +
    '</body></html>';
}

// ── Imprimir etiqueta Godex GE300 ────────────────────────────────────────────
// EZPL directo vía WebUSB = sin diálogo. Driver PNG = fallback.
// window.open() se llama SÍNCRONAMENTE antes de cualquier await
// para que el navegador no lo bloquee como popup.
function imprimirEtiquetaGodex(productos, config) {
  // ── PNG a DPI NATIVO de la impresora (203 DPI = 440×96 px) ────────────────
  // El driver NO necesita escalar → máxima nitidez.
  // Para la pantalla usamos el SVG vectorial (escala sin pérdida).
  const DPI = 203;
  const PW  = Math.round(55 / 25.4 * DPI);  // 440 px  (exactamente 55mm a 203 DPI)
  const PH  = Math.round(12 / 25.4 * DPI);  //  96 px  (exactamente 12mm a 203 DPI)

  // SVG → PNG en resolución nativa con umbral B&W (threshold)
  // El threshold convierte cada pixel a puro negro o blanco → ideal para termal.
  function svgToPng(svgStr, rotate180) {
    return new Promise(resolve => {
      // Renderizar el SVG a tamaño nativo de impresora
      const blob = new Blob([svgStr], {type:'image/svg+xml;charset=utf-8'});
      const url  = URL.createObjectURL(blob);
      const img  = new Image();
      img.onload = () => {
        const cv  = document.createElement('canvas');
        cv.width  = PW; cv.height = PH;
        const ctx = cv.getContext('2d');
        ctx.imageSmoothingEnabled = false;        // sin antialiasing
        ctx.fillStyle = 'white'; ctx.fillRect(0, 0, PW, PH);
        if (rotate180) {
          ctx.save(); ctx.translate(PW/2, PH/2); ctx.rotate(Math.PI);
          ctx.drawImage(img, -PW/2, -PH/2, PW, PH); ctx.restore();
        } else {
          ctx.drawImage(img, 0, 0, PW, PH);
        }
        // ── Threshold: pixel < 128 → negro puro, ≥ 128 → blanco puro ─────
        // Elimina grises → la impresora térmica imprime solo negro/blanco.
        const id = ctx.getImageData(0, 0, PW, PH);
        const d  = id.data;
        for (let i = 0; i < d.length; i += 4) {
          const lum = 0.299 * d[i] + 0.587 * d[i+1] + 0.114 * d[i+2];
          const v   = lum < 140 ? 0 : 255;   // umbral 140 → texto negro intenso
          d[i] = d[i+1] = d[i+2] = v;
          d[i+3] = 255;
        }
        ctx.putImageData(id, 0, 0);
        URL.revokeObjectURL(url);
        resolve(cv.toDataURL('image/png'));
      };
      img.onerror = () => { URL.revokeObjectURL(url); resolve(null); };
      img.src = url;
    });
  }

  // ── CRÍTICO: abrir popup SÍNCRONAMENTE antes de cualquier await ───────────
  // Los navegadores bloquean window.open() si se llama después de un await
  // (pierde el "user gesture"). Abrimos primero con pantalla de carga.
  const win = window.open('', '_blank', 'width=820,height=660');
  if (!win) {
    alert('Activa las ventanas emergentes para imprimir.\n(Ícono en la barra de dirección del navegador)');
    return;
  }
  win.document.write('<!DOCTYPE html><html><body style="background:#1a2f1a;color:#c9a84c;display:flex;align-items:center;justify-content:center;height:100vh;font:16px Arial;margin:0">Generando etiquetas…</body></html>');
  win.document.close();

  async function generar() {
    // SVG: scale=4 para pantalla (más grande, más detalle visual)
    // PNG: scale=1 (nativo 203 DPI, 440×96px) — sin escalado en driver → máxima calidad
    const items = [];
    for (const p of productos) {
      const sku    = (p.sku||'SKU0000').toUpperCase();
      const precio = p.precio || 0;
      const nombre = p.nombre || '';
      const svgScreen = godexEtiquetaSVG(sku, precio, nombre, {scale: 4}); // pantalla (vectorial)
      const svgPrint  = godexEtiquetaSVG(sku, precio, nombre, {scale: 1}); // impresión (vectorial)
      const svgUrl      = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgScreen);
      const svgPrintUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgPrint);
      // PNG solo se usa para EZPL / previsualización de rotación
      const png    = await svgToPng(svgPrint, false);
      const pngRot = await svgToPng(svgPrint, true);
      if (png) items.push({svgUrl, svgPrintUrl, png, pngRot, sku, nombre, precio});
    }
    if (!items.length) { win.close(); alert('Error al generar etiquetas'); return; }

    // EZPL blocks (un bloque por etiqueta, con newlines reales)
    const _ezplBtype = getBarcodeType();
    const _layout = getLabelLayout();
    const _dmSizeMm = (_layout.barcode && _layout.barcode.dmSize) || 9;
    // Ancho de módulo DM en dots: 9mm / 16 módulos × 203 DPI / 25.4 ≈ 4.5 → usar 4
    const _dmModW  = Math.max(3, Math.round(_dmSizeMm / 25.4 * 203 / 16));
    const ezplBlocks = items.map(i => {
      const cl = i.sku.replace(/[^A-Z0-9\-]/g,'').slice(0, 20);
      const ps = '\$'+(i.precio||0).toLocaleString('es-CL');
      // Nombre: solo ASCII imprimible, máx 20 chars, dividido en 2 líneas centradas
      const nm = ((i.nombre||'').toUpperCase()).replace(/[^A-Z0-9 \-\.]/g,'').slice(0, 20).trim();
      // Dividir nombre en 2 líneas por la mitad de palabras
      const nmWords = nm.split(' ').filter(Boolean);
      let nmL1 = nm, nmL2 = '';
      if (nmWords.length >= 2) {
        const nmMid = Math.ceil(nmWords.length / 2);
        nmL1 = nmWords.slice(0, nmMid).join(' ');
        nmL2 = nmWords.slice(nmMid).join(' ');
      }
      // Nombre x=4, fuente AA xmult=2 (bold simulado), debajo del precio con margen
      // AB ymult=2 ≈ 24 dots alto → termina en y≈28. Nombre empieza en y=50 (22 dots gap)
      const nmCmd = nmL2
        ? 'AA,4,50,1,1,0,0,'+nmL1+'\nAA,4,62,1,1,0,0,'+nmL2
        : 'AA,4,50,1,1,0,0,'+nmL1;
      // GE300 EZPL — formato nativo Godex confirmado con GoLabel
      // ^Q en mm: ^Q12,3 = 12mm etiqueta + 3mm gap (NO en dots)
      // XRB = comando nativo Godex para Data Matrix ECC200
      // E sin ^ = fin correcto de etiqueta GE300
      let barcodeCmd;
      if (_ezplBtype === 'datamatrix') {
        // DM: size=4 (56×56 dots), centrado en flag derecho (x=347, y=6)
        // SKU centrado bajo el DM (y=65, font AA)
        barcodeCmd = 'XRB347,6,4,0,'+cl.length+'\n'+cl+'\nAA,320,65,1,1,0,0,'+cl;
      } else if (_ezplBtype === 'qr') {
        barcodeCmd = 'W300,2,2,4,L,'+cl;
      } else {
        barcodeCmd = 'B160,5,0,6,2,5,66,N,'+cl;
      }
      // EZPL layout: izq = precio(top) + nombre 2 líneas centradas | der = DM + SKU
      // AC ymult=1 y=4 → precio compacto, bien arriba
      // AA 2 líneas centradas → nombre pequeño debajo del precio
      return ['^Q12,3','^W55','^H10','^S2','^P1','^L',
              'AB,4,4,1,2,0,0,'+ps,
              nmCmd,
              barcodeCmd,
              'E'].join('\n');
    });

    // HTML de etiquetas (string concat — sin template literals)
    let labelsHtml = '';
    for (const item of items) {
      const sn = (item.nombre||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
      const sp = '$'+(item.precio||0).toLocaleString('es-CL');
      labelsHtml +=
        '<div class="etq-wrap" data-svgp="'+item.svgPrintUrl+'" data-svgpr="'+item.svgPrintUrl+'">' +
          '<div class="etq-box">' +
            '<img class="etq-svg" src="'+item.svgUrl+'" width="462" height="101"/>' +
            '<img class="etq-print" src="'+item.png+'" data-n="'+item.png+'" data-r="'+item.pngRot+'"/>' +
          '</div>' +
          '<div class="etq-info">'+item.sku+(sn?' &middot; '+sn:'')+' &middot; '+sp+'</div>' +
        '</div>';
    }

    // Escribir popup HTML (estructura estática, sin datos en el script)
    // → datos inyectados DESPUÉS via win.initPrinter() y win.document.getElementById()
    win.document.open();
    win.document.write(_buildGodexPopup(items.length));
    win.document.close();

    // Inyectar labels y EZPL data después de que el DOM y los scripts estén listos
    const labelsEl = win.document.getElementById('labels');
    if (labelsEl) labelsEl.innerHTML = labelsHtml;
    const pngUrls = items.map(i => i.png || '');
    if (typeof win.initPrinter === 'function') win.initPrinter(ezplBlocks, pngUrls);
  }

  generar();
}

// ── Imprimir documento en Epson L3250 (A4/Carta) ────────────────────────────
function imprimirDocumentoEpson(htmlContent, titulo) {
  const logo = getLogo();
  const cfg  = getConfig ? getConfig() : {};
  const win  = window.open('', '_blank', 'width=900,height=700');
  if (!win) return;
  win.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8">
  <title>Epson_L3250_${titulo||'Documento'}</title>
  <style>
    @page { margin:15mm 12mm; size:A4; }
    * { box-sizing:border-box; }
    body { font-family:Arial,sans-serif; font-size:11pt; color:#111; margin:0; }
    .header { display:flex; justify-content:space-between; align-items:flex-start;
      border-bottom:2pt solid #111; padding-bottom:8pt; margin-bottom:14pt; }
    .logo-area { display:flex; align-items:center; gap:10pt; }
    .logo-img { height:45pt; width:auto; }
    .logo-text { font-family:'Times New Roman',serif; }
    .logo-nombre { font-size:18pt; font-weight:bold; letter-spacing:2pt; }
    .logo-sub { font-size:9pt; color:#555; letter-spacing:1pt; }
    .doc-info { text-align:right; font-size:10pt; color:#444; }
    .info { background:#1a3a2a; color:#fff; padding:5px 8px; font-size:9px; margin-bottom:8px; border-radius:3px; }
    .content { margin-top:8pt; }
    @media print { .info { display:none; } }
  </style></head><body>
  <div class="info">🖨 <b style="color:#c9a84c">Epson L3250</b> — Documento A4 — ${titulo||''}</div>
  <div class="header">
    <div class="logo-area">
      ${logo ? `<img src="${logo}" class="logo-img" onerror="this.style.display='none'"/>` : ''}
      <div class="logo-text">
        <div class="logo-nombre">${cfg.tiendaNombre||'NAVARRO'}</div>
        <div class="logo-sub">JOYERÍA · ${cfg.tiendaDireccion||'Santa Cruz, Colchagua'}</div>
        <div class="logo-sub">${cfg.tiendaWeb||'navarro-joyeria.cl'}</div>
      </div>
    </div>
    <div class="doc-info">
      <div>Fecha: ${new Date().toLocaleDateString('es-CL')}</div>
    </div>
  </div>
  <div class="content">${htmlContent}</div>
  <script>
    document.title = 'Epson_L3250_${titulo||"doc"}';
    window.onload = function(){ setTimeout(function(){ window.print(); }, 600); };
  </script>
  </body></html>`);
  win.document.close();
}


// ── Impresión recibo taller — 3 copias con semicorte ────────────────────────
function imprimirReciboTaller(data) {
  const { titulo, folio, fecha, cliente, rut, telefono, tienda, items, total, senal, saldo, notas, tecnico, fechaEntrega } = data;
  const win = window.open('','_blank','width=440,height=700');
  if (!win) return;
  const row = (l,r) => `<div class="row"><span>${l}</span><span>${r||''}</span></div>`;
  const copia = (lbl, cut) => `
<div class="copia">
  <div class="c b lgo">✦ NAVARRO ✦</div>
  <div class="c sub">JOYERÍA &amp; TALLER · Santa Cruz</div>
  <hr class="hr">
  <div class="c b tit">${titulo}</div>
  <div class="c fol">N° ${folio} &nbsp;·&nbsp; ${fecha}</div>
  <hr class="hr">
  ${row('Cliente:','<b>'+( cliente||'—')+'</b>')}
  ${rut ? row('RUT:',rut):''}
  ${telefono ? row('Tel/WA:',telefono):''}
  ${tienda ? row('Tienda:',tienda):''}
  <hr class="hr">
  ${(items||[]).map(i=>`<div class="itm"><b>${i.label}</b>${i.detalle?`<div class="det">${i.detalle}</div>`:''}</div>`).join('')}
  <hr class="hr">
  ${total ? row('<b>TOTAL:</b>',`<b>$${total.toLocaleString('es-CL')}</b>`):''}
  ${senal ? row('Abono:',`$${senal.toLocaleString('es-CL')}`):''}
  ${saldo ? row('Saldo:',`$${saldo.toLocaleString('es-CL')}`):''}
  ${tecnico ? row('Artesano:',tecnico):''}
  ${fechaEntrega ? row('Entrega:',fechaEntrega):''}
  ${notas ? `<hr class="hr"><div class="obs"><b>Obs:</b> ${notas}</div>`:''}
  <hr class="hr">
  <div class="c etq">COPIA: ${lbl}</div>
  <div class="c pie">✦ navarro-joyeria.cl ✦</div>
</div>
${cut ? '<div class="pagecut"></div>' : ''}`;

  win.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8">
  <title>SPRT_${folio}</title>
  <style>
    @page { margin:2mm 3mm; size:80mm auto; }
    *    { box-sizing:border-box; }
    body { font-family:'Courier New',monospace; font-size:11px; width:74mm; margin:0 auto; color:#000; }
    .c   { text-align:center; }
    .b   { font-weight:bold; }
    .row { display:flex; justify-content:space-between; margin:2px 0; font-size:10px; }
    .hr  { border:none; border-top:1px dashed #000; margin:3px 0; }
    .lgo { font-size:14px; letter-spacing:2px; margin:3px 0 1px; }
    .sub { font-size:8px; }
    .tit { font-size:12px; margin:2px 0; }
    .fol { font-size:9px; color:#444; }
    .itm { margin:2px 0; font-size:10px; }
    .det { font-size:9px; color:#555; padding-left:8px; }
    .obs { font-size:9px; }
    .etq { font-size:9px; font-style:italic; margin:2px 0; }
    .pie { font-size:8px; color:#555; margin-bottom:3px; }
    .copia { page-break-after:always; }
    .copia:last-of-type { page-break-after:auto; }
    .pagecut { height:0; margin:0; padding:0; page-break-after:always; visibility:hidden; }
    .info{ background:#1a3a2a; color:#fff; padding:5px 8px; font-size:9px; margin-bottom:5px; border-radius:3px; }
    @media print { .info { display:none; } }
  </style></head><body>
  <div class="info">🖨 <b style="color:#c9a84c">SPRT SP-POS88</b> · 3 copias · Semicorte entre copias</div>
  ${copia('JOYERÍA', true)}
  ${copia('TALLER',  true)}
  ${copia('CLIENTE', false)}
  <script>
    document.title = 'SPRT_${folio}';
    window.onload = function(){ setTimeout(function(){ window.print(); }, 500); };
  <\/script>
  </body></html>`);
  win.document.close();
}


// Estilos compartidos (usados por múltiples módulos)
const posStyles = {
  overlay:{ position:'fixed',inset:0,background:'rgba(0,0,0,0.7)',zIndex:100,display:'flex',alignItems:'center',justifyContent:'center',backdropFilter:'blur(4px)' },
  modalBox:{ background:'var(--bg-card)',border:'1px solid var(--border)',borderRadius:'16px',padding:'22px',width:'100%',maxWidth:'460px',maxHeight:'90vh',overflowY:'auto' },
  modalHeader:{ display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:'4px' },
  clearBtn:{ background:'none',border:'none',color:'var(--cream-3)',cursor:'pointer',fontSize:'16px',padding:'2px 4px',lineHeight:1 },
  cobrarBtn:{ padding:'13px',background:'var(--gold)',border:'none',borderRadius:'10px',color:'#0c0908',fontWeight:700,fontSize:'14px',cursor:'pointer',letterSpacing:'0.5px',fontFamily:'Cormorant Garamond,serif',transition:'all 0.15s',display:'block' },
  descBtn:{ padding:'4px 10px',borderRadius:'5px',border:'1px solid var(--border)',fontSize:'11px',background:'var(--surface)',color:'var(--cream-3)',cursor:'pointer',fontFamily:'Inter,sans-serif' },
  descBtnActive:{ background:'var(--gold-dim)',border:'1px solid var(--gold)',color:'var(--gold)' },
};

const calcStyles = {
  leftCol:{ width:'400px',overflowY:'auto',padding:'18px',borderRight:'1px solid var(--border)' },
  rightCol:{ flex:1,overflowY:'auto',padding:'18px',display:'flex',flexDirection:'column',gap:'12px' },
  grupo:{ marginBottom:'16px' },
  grupoTitle:{ fontSize:'10px',color:'var(--cream-3)',fontWeight:600,letterSpacing:'0.5px',textTransform:'uppercase',marginBottom:'7px' },
  chip:{ padding:'5px 11px',borderRadius:'20px',border:'1px solid var(--border)',background:'var(--surface)',color:'var(--cream-3)',fontSize:'11px',cursor:'pointer',fontFamily:'Inter,sans-serif' },
  chipAct:{ background:'var(--gold-dim)',border:'1px solid var(--gold)',color:'var(--gold)',fontWeight:600 },
  input:{ width:'100%',background:'var(--surface)',border:'1px solid var(--border)',borderRadius:'8px',padding:'8px 11px',color:'var(--cream)',fontSize:'13px',outline:'none' },
  resultCard:{ background:'linear-gradient(135deg,var(--surface) 0%,var(--bg-card) 100%)',border:'1px solid var(--border)',borderRadius:'14px',padding:'22px',marginBottom:'4px' },
  desglose:{ background:'var(--bg-card)',border:'1px solid var(--border)',borderRadius:'12px',padding:'15px' },
};

const invStyles = {
  th:{ padding:'7px 10px',textAlign:'left',fontSize:'10px',color:'var(--cream-3)',fontWeight:600,letterSpacing:'0.5px',borderBottom:'1px solid var(--border)',background:'var(--bg-card)',whiteSpace:'nowrap',position:'sticky',top:0,zIndex:2 },
  tr:{ borderBottom:'1px solid var(--border)',transition:'background 0.1s' },
  td:{ padding:'8px 10px',verticalAlign:'middle' },
  stockBadge:{ display:'inline-block',padding:'2px 7px',borderRadius:'20px',background:'rgba(39,174,96,0.12)',color:'#27ae60',fontSize:'11px',fontWeight:600 },
  stockBajo:{ background:'rgba(192,57,43,0.12)',color:'#c0392b' },
  stockCero:{ background:'rgba(100,100,100,0.2)',color:'var(--cream-3)' },
  btnEdit:{ padding:'4px 9px',background:'var(--surface-2)',border:'1px solid var(--border)',borderRadius:'5px',color:'var(--cream-2)',cursor:'pointer',fontSize:'11px' },
  btnAdd:{ padding:'6px 14px',background:'var(--gold)',border:'none',borderRadius:'7px',color:'#0c0908',fontWeight:700,cursor:'pointer',fontSize:'11px',fontFamily:'Inter,sans-serif' },
  select:{ background:'var(--surface)',border:'1px solid var(--border)',borderRadius:'6px',padding:'7px 9px',color:'var(--cream)',fontSize:'12px',outline:'none',cursor:'pointer' },
};

const inv2Styles = {
  searchInput:{ background:'var(--surface)',border:'1px solid var(--border)',borderRadius:'6px',padding:'7px 10px',color:'var(--cream)',fontSize:'12px',outline:'none' },
  th:{ padding:'6px 10px',textAlign:'left',fontSize:'10px',color:'var(--cream-3)',fontWeight:600,letterSpacing:'0.5px',borderBottom:'1px solid var(--border)',background:'var(--bg-card)',whiteSpace:'nowrap',position:'sticky',top:0,zIndex:2 },
  tr:{ borderBottom:'1px solid var(--border)',transition:'background 0.1s' },
  td:{ padding:'7px 10px',verticalAlign:'middle' },
  btnSec:{ padding:'5px 10px',background:'var(--surface)',border:'1px solid var(--border)',borderRadius:'6px',color:'var(--cream-2)',cursor:'pointer',fontSize:'11px',fontFamily:'Inter,sans-serif' },
  btnGold:{ padding:'6px 12px',background:'var(--gold)',border:'none',borderRadius:'6px',color:'#0c0908',fontWeight:700,cursor:'pointer',fontSize:'11px' },
};

const invStyles2 = {
  select:{ background:'var(--surface)',border:'1px solid var(--border)',borderRadius:'6px',padding:'7px 9px',color:'var(--cream)',fontSize:'12px',outline:'none',cursor:'pointer' }
};

const calcStyles2 = {
  chip:{ padding:'5px 11px',borderRadius:'20px',border:'1px solid var(--border)',background:'var(--surface)',color:'var(--cream-3)',fontSize:'11px',cursor:'pointer',fontFamily:'Inter,sans-serif' },
  chipAct:{ background:'var(--gold-dim)',border:'1px solid var(--gold)',color:'var(--gold)',fontWeight:600 },
  input:{ width:'100%',background:'var(--surface)',border:'1px solid var(--border)',borderRadius:'8px',padding:'8px 11px',color:'var(--cream)',fontSize:'13px',outline:'none' },
};

// Compatibilidad con módulos que aún referencian los viejos arrays
Object.assign(window, {
  getConfig, setConfig,
  TIENDAS, PRECIO_METALES, INDICADORES, METALES, COLECCIONES,
  MEDIOS_PAGO, TECNICOS, CATS_MAESTRAS, METALES_MAESTROS,
  PIEDRAS_MAESTRAS, ESTADOS_PROD, USUARIOS_DEFAULT,
  PRODUCTOS:[], CLIENTES:[], ORDENES:[], VENTAS_RECIENTES:[], COMPRAS_METAL:[],
  clp, desglosarIVA, hoy, generarSKU, estadoBadge,
  getBarcodeType, setBarcodeType,
  imprimirEtiquetaGodex, imprimirReciboTaller, imprimirDocumentoEpson, getLogo, PRINT_CONFIG,
  lsGet, lsSet, sbPullAll, sbPushAll, sbSubirFoto,
  posStyles, calcStyles, calcStyles2, invStyles, inv2Styles, invStyles2,
});
